Merge "Put the call log into a fragment and host it in a simple container activity"
diff --git a/res/layout/call_log_activity.xml b/res/layout/call_log_activity.xml
index f054b70..b391795 100644
--- a/res/layout/call_log_activity.xml
+++ b/res/layout/call_log_activity.xml
@@ -18,17 +18,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
>
- <ListView android:id="@android:id/list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scrollbarStyle="outsideOverlay"
- />
-
- <TextView android:id="@android:id/empty"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:text="@string/recentCalls_empty"
- android:gravity="center"
- android:textAppearance="?android:attr/textAppearanceLarge"
- />
+ <fragment class="com.android.contacts.calllog.CallLogFragment"
+ android:id="@+id/call_log_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
</FrameLayout>
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
new file mode 100644
index 0000000..2a27fcd
--- /dev/null
+++ b/res/layout/call_log_fragment.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+>
+ <ListView android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbarStyle="outsideOverlay"
+ />
+
+ <TextView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/recentCalls_empty"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ />
+</FrameLayout>
diff --git a/src/com/android/contacts/activities/CallLogActivity.java b/src/com/android/contacts/activities/CallLogActivity.java
index 5672af9..13c3209 100644
--- a/src/com/android/contacts/activities/CallLogActivity.java
+++ b/src/com/android/contacts/activities/CallLogActivity.java
@@ -16,874 +16,46 @@
package com.android.contacts.activities;
-import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.CallDetailActivity;
import com.android.contacts.ContactsSearchManager;
-import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
-import com.android.internal.telephony.CallerInfo;
+import com.android.contacts.calllog.CallLogFragment;
import com.android.internal.telephony.ITelephony;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.ListActivity;
-import android.app.ProgressDialog;
+import android.app.Activity;
import android.content.ActivityNotFoundException;
-import android.content.AsyncQueryHandler;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.content.DialogInterface.OnClickListener;
-import android.database.CharArrayBuffer;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabaseCorruptException;
-import android.database.sqlite.SQLiteDiskIOException;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteFullException;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Intents.Insert;
-import android.provider.ContactsContract.PhoneLookup;
-import android.telephony.PhoneNumberUtils;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
import android.util.Log;
-import android.view.ContextMenu;
import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.widget.AdapterView;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import java.lang.ref.WeakReference;
-import java.util.HashMap;
-import java.util.LinkedList;
/**
* Displays a list of call log entries.
*/
-public class CallLogActivity extends ListActivity
- implements View.OnCreateContextMenuListener {
+public class CallLogActivity extends Activity {
private static final String TAG = "CallLogActivity";
- /** The projection to use when querying the call log table */
- static final String[] CALL_LOG_PROJECTION = new String[] {
- Calls._ID,
- Calls.NUMBER,
- Calls.DATE,
- Calls.DURATION,
- Calls.TYPE,
- Calls.CACHED_NAME,
- Calls.CACHED_NUMBER_TYPE,
- Calls.CACHED_NUMBER_LABEL,
- Calls.COUNTRY_ISO};
-
- static final int ID_COLUMN_INDEX = 0;
- static final int NUMBER_COLUMN_INDEX = 1;
- static final int DATE_COLUMN_INDEX = 2;
- static final int DURATION_COLUMN_INDEX = 3;
- static final int CALL_TYPE_COLUMN_INDEX = 4;
- static final int CALLER_NAME_COLUMN_INDEX = 5;
- static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 6;
- static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 7;
- static final int COUNTRY_ISO_COLUMN_INDEX = 8;
-
- /** The projection to use when querying the phones table */
- static final String[] PHONES_PROJECTION = new String[] {
- PhoneLookup._ID,
- PhoneLookup.DISPLAY_NAME,
- PhoneLookup.TYPE,
- PhoneLookup.LABEL,
- PhoneLookup.NUMBER,
- PhoneLookup.NORMALIZED_NUMBER};
-
- static final int PERSON_ID_COLUMN_INDEX = 0;
- static final int NAME_COLUMN_INDEX = 1;
- static final int PHONE_TYPE_COLUMN_INDEX = 2;
- static final int LABEL_COLUMN_INDEX = 3;
- static final int MATCHED_NUMBER_COLUMN_INDEX = 4;
- static final int NORMALIZED_NUMBER_COLUMN_INDEX = 5;
-
- private static final int MENU_ITEM_DELETE = 1;
- private static final int MENU_ITEM_DELETE_ALL = 2;
- private static final int MENU_ITEM_VIEW_CONTACTS = 3;
-
- private static final int QUERY_TOKEN = 53;
- private static final int UPDATE_TOKEN = 54;
-
- private static final int DIALOG_CONFIRM_DELETE_ALL = 1;
-
- CallLogAdapter mAdapter;
- private QueryHandler mQueryHandler;
- String mVoiceMailNumber;
- private String mCurrentCountryIso;
-
- private boolean mScrollToTop;
-
- static final class ContactInfo {
- public long personId;
- public String name;
- public int type;
- public String label;
- public String number;
- public String formattedNumber;
- public String normalizedNumber;
-
- public static ContactInfo EMPTY = new ContactInfo();
- }
-
- public static final class CallLogListItemViews {
- TextView line1View;
- TextView labelView;
- TextView numberView;
- TextView dateView;
- ImageView iconView;
- View callView;
- ImageView groupIndicator;
- TextView groupSize;
- }
-
- static final class CallerInfoQuery {
- String number;
- int position;
- String name;
- int numberType;
- String numberLabel;
- }
-
- /** Adapter class to fill in data for the Call Log */
- final class CallLogAdapter extends GroupingListAdapter
- implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
- HashMap<String,ContactInfo> mContactInfo;
- private final LinkedList<CallerInfoQuery> mRequests;
- private volatile boolean mDone;
- private boolean mLoading = true;
- ViewTreeObserver.OnPreDrawListener mPreDrawListener;
- private static final int REDRAW = 1;
- private static final int START_THREAD = 2;
- private boolean mFirst;
- private Thread mCallerIdThread;
-
- private CharSequence[] mLabelArray;
-
- private Drawable mDrawableIncoming;
- private Drawable mDrawableOutgoing;
- private Drawable mDrawableMissed;
-
- /**
- * Reusable char array buffers.
- */
- private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
- private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
-
- public void onClick(View view) {
- String number = (String) view.getTag();
- if (!TextUtils.isEmpty(number)) {
- // Here, "number" can either be a PSTN phone number or a
- // SIP address. So turn it into either a tel: URI or a
- // sip: URI, as appropriate.
- Uri callUri;
- if (PhoneNumberUtils.isUriNumber(number)) {
- callUri = Uri.fromParts("sip", number, null);
- } else {
- callUri = Uri.fromParts("tel", number, null);
- }
- startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri));
- }
- }
-
- public boolean onPreDraw() {
- if (mFirst) {
- mHandler.sendEmptyMessageDelayed(START_THREAD, 1000);
- mFirst = false;
- }
- return true;
- }
-
- private Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case REDRAW:
- notifyDataSetChanged();
- break;
- case START_THREAD:
- startRequestProcessing();
- break;
- }
- }
- };
-
- public CallLogAdapter() {
- super(CallLogActivity.this);
-
- mContactInfo = new HashMap<String,ContactInfo>();
- mRequests = new LinkedList<CallerInfoQuery>();
- mPreDrawListener = null;
-
- mDrawableIncoming = getResources().getDrawable(
- R.drawable.ic_call_log_list_incoming_call);
- mDrawableOutgoing = getResources().getDrawable(
- R.drawable.ic_call_log_list_outgoing_call);
- mDrawableMissed = getResources().getDrawable(
- R.drawable.ic_call_log_list_missed_call);
- mLabelArray = getResources().getTextArray(com.android.internal.R.array.phoneTypes);
- }
-
- /**
- * Requery on background thread when {@link Cursor} changes.
- */
- @Override
- protected void onContentChanged() {
- // Start async requery
- startQuery();
- }
-
- void setLoading(boolean loading) {
- mLoading = loading;
- }
-
- @Override
- public boolean isEmpty() {
- if (mLoading) {
- // We don't want the empty state to show when loading.
- return false;
- } else {
- return super.isEmpty();
- }
- }
-
- public ContactInfo getContactInfo(String number) {
- return mContactInfo.get(number);
- }
-
- public void startRequestProcessing() {
- mDone = false;
- mCallerIdThread = new Thread(this);
- mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
- mCallerIdThread.start();
- }
-
- public void stopRequestProcessing() {
- mDone = true;
- if (mCallerIdThread != null) mCallerIdThread.interrupt();
- }
-
- public void clearCache() {
- synchronized (mContactInfo) {
- mContactInfo.clear();
- }
- }
-
- private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) {
- // Check if they are different. If not, don't update.
- if (TextUtils.equals(ciq.name, ci.name)
- && TextUtils.equals(ciq.numberLabel, ci.label)
- && ciq.numberType == ci.type) {
- return;
- }
- ContentValues values = new ContentValues(3);
- values.put(Calls.CACHED_NAME, ci.name);
- values.put(Calls.CACHED_NUMBER_TYPE, ci.type);
- values.put(Calls.CACHED_NUMBER_LABEL, ci.label);
-
- try {
- CallLogActivity.this.getContentResolver().update(Calls.CONTENT_URI, values,
- Calls.NUMBER + "='" + ciq.number + "'", null);
- } catch (SQLiteDiskIOException e) {
- Log.w(TAG, "Exception while updating call info", e);
- } catch (SQLiteFullException e) {
- Log.w(TAG, "Exception while updating call info", e);
- } catch (SQLiteDatabaseCorruptException e) {
- Log.w(TAG, "Exception while updating call info", e);
- }
- }
-
- private void enqueueRequest(String number, int position,
- String name, int numberType, String numberLabel) {
- CallerInfoQuery ciq = new CallerInfoQuery();
- ciq.number = number;
- ciq.position = position;
- ciq.name = name;
- ciq.numberType = numberType;
- ciq.numberLabel = numberLabel;
- synchronized (mRequests) {
- mRequests.add(ciq);
- mRequests.notifyAll();
- }
- }
-
- private boolean queryContactInfo(CallerInfoQuery ciq) {
- // First check if there was a prior request for the same number
- // that was already satisfied
- ContactInfo info = mContactInfo.get(ciq.number);
- boolean needNotify = false;
- if (info != null && info != ContactInfo.EMPTY) {
- return true;
- } else {
- // Ok, do a fresh Contacts lookup for ciq.number.
- boolean infoUpdated = false;
-
- if (PhoneNumberUtils.isUriNumber(ciq.number)) {
- // This "number" is really a SIP address.
-
- // TODO: This code is duplicated from the
- // CallerInfoAsyncQuery class. To avoid that, could the
- // code here just use CallerInfoAsyncQuery, rather than
- // manually running ContentResolver.query() itself?
-
- // We look up SIP addresses directly in the Data table:
- Uri contactRef = Data.CONTENT_URI;
-
- // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- //
- // Also note we use "upper(data1)" in the WHERE clause, and
- // uppercase the incoming SIP address, in order to do a
- // case-insensitive match.
- //
- // TODO: May also need to normalize by adding "sip:" as a
- // prefix, if we start storing SIP addresses that way in the
- // database.
- String selection = "upper(" + Data.DATA1 + ")=?"
- + " AND "
- + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
- String[] selectionArgs = new String[] { ciq.number.toUpperCase() };
-
- Cursor dataTableCursor =
- CallLogActivity.this.getContentResolver().query(
- contactRef,
- null, // projection
- selection, // selection
- selectionArgs, // selectionArgs
- null); // sortOrder
-
- if (dataTableCursor != null) {
- if (dataTableCursor.moveToFirst()) {
- info = new ContactInfo();
-
- // TODO: we could slightly speed this up using an
- // explicit projection (and thus not have to do
- // those getColumnIndex() calls) but the benefit is
- // very minimal.
-
- // Note the Data.CONTACT_ID column here is
- // equivalent to the PERSON_ID_COLUMN_INDEX column
- // we use with "phonesCursor" below.
- info.personId = dataTableCursor.getLong(
- dataTableCursor.getColumnIndex(Data.CONTACT_ID));
- info.name = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
- // "type" and "label" are currently unused for SIP addresses
- info.type = SipAddress.TYPE_OTHER;
- info.label = null;
-
- // And "number" is the SIP address.
- // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- info.number = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DATA1));
- info.normalizedNumber = null; // meaningless for SIP addresses
-
- infoUpdated = true;
- }
- dataTableCursor.close();
- }
- } else {
- // "number" is a regular phone number, so use the
- // PhoneLookup table:
- Cursor phonesCursor =
- CallLogActivity.this.getContentResolver().query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
- Uri.encode(ciq.number)),
- PHONES_PROJECTION, null, null, null);
- if (phonesCursor != null) {
- if (phonesCursor.moveToFirst()) {
- info = new ContactInfo();
- info.personId = phonesCursor.getLong(PERSON_ID_COLUMN_INDEX);
- info.name = phonesCursor.getString(NAME_COLUMN_INDEX);
- info.type = phonesCursor.getInt(PHONE_TYPE_COLUMN_INDEX);
- info.label = phonesCursor.getString(LABEL_COLUMN_INDEX);
- info.number = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX);
- info.normalizedNumber =
- phonesCursor.getString(NORMALIZED_NUMBER_COLUMN_INDEX);
-
- infoUpdated = true;
- }
- phonesCursor.close();
- }
- }
-
- if (infoUpdated) {
- // New incoming phone number invalidates our formatted
- // cache. Any cache fills happen only on the GUI thread.
- info.formattedNumber = null;
-
- mContactInfo.put(ciq.number, info);
-
- // Inform list to update this item, if in view
- needNotify = true;
- }
- }
- if (info != null) {
- updateCallLog(ciq, info);
- }
- return needNotify;
- }
-
- /*
- * Handles requests for contact name and number type
- * @see java.lang.Runnable#run()
- */
- public void run() {
- boolean needNotify = false;
- while (!mDone) {
- CallerInfoQuery ciq = null;
- synchronized (mRequests) {
- if (!mRequests.isEmpty()) {
- ciq = mRequests.removeFirst();
- } else {
- if (needNotify) {
- needNotify = false;
- mHandler.sendEmptyMessage(REDRAW);
- }
- try {
- mRequests.wait(1000);
- } catch (InterruptedException ie) {
- // Ignore and continue processing requests
- }
- }
- }
- if (ciq != null && queryContactInfo(ciq)) {
- needNotify = true;
- }
- }
- }
-
- @Override
- protected void addGroups(Cursor cursor) {
-
- int count = cursor.getCount();
- if (count == 0) {
- return;
- }
-
- int groupItemCount = 1;
-
- CharArrayBuffer currentValue = mBuffer1;
- CharArrayBuffer value = mBuffer2;
- cursor.moveToFirst();
- cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, currentValue);
- int currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
- for (int i = 1; i < count; i++) {
- cursor.moveToNext();
- cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, value);
- boolean sameNumber = equalPhoneNumbers(value, currentValue);
-
- // Group adjacent calls with the same number. Make an exception
- // for the latest item if it was a missed call. We don't want
- // a missed call to be hidden inside a group.
- if (sameNumber && currentCallType != Calls.MISSED_TYPE) {
- groupItemCount++;
- } else {
- if (groupItemCount > 1) {
- addGroup(i - groupItemCount, groupItemCount, false);
- }
-
- groupItemCount = 1;
-
- // Swap buffers
- CharArrayBuffer temp = currentValue;
- currentValue = value;
- value = temp;
-
- // If we have just examined a row following a missed call, make
- // sure that it is grouped with subsequent calls from the same number
- // even if it was also missed.
- if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
- currentCallType = 0; // "not a missed call"
- } else {
- currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
- }
- }
- }
- if (groupItemCount > 1) {
- addGroup(count - groupItemCount, groupItemCount, false);
- }
- }
-
- protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
-
- // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
- // string allocation
- return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied),
- new String(buffer2.data, 0, buffer2.sizeCopied));
- }
-
-
- @Override
- protected View newStandAloneView(Context context, ViewGroup parent) {
- LayoutInflater inflater =
- (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
- findAndCacheViews(view);
- return view;
- }
-
- @Override
- protected void bindStandAloneView(View view, Context context, Cursor cursor) {
- bindView(context, view, cursor);
- }
-
- @Override
- protected View newChildView(Context context, ViewGroup parent) {
- LayoutInflater inflater =
- (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false);
- findAndCacheViews(view);
- return view;
- }
-
- @Override
- protected void bindChildView(View view, Context context, Cursor cursor) {
- bindView(context, view, cursor);
- }
-
- @Override
- protected View newGroupView(Context context, ViewGroup parent) {
- LayoutInflater inflater =
- (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false);
- findAndCacheViews(view);
- return view;
- }
-
- @Override
- protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
- boolean expanded) {
- final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
- int groupIndicator = expanded
- ? com.android.internal.R.drawable.expander_ic_maximized
- : com.android.internal.R.drawable.expander_ic_minimized;
- views.groupIndicator.setImageResource(groupIndicator);
- views.groupSize.setText("(" + groupSize + ")");
- bindView(context, view, cursor);
- }
-
- private void findAndCacheViews(View view) {
-
- // Get the views to bind to
- CallLogListItemViews views = new CallLogListItemViews();
- views.line1View = (TextView) view.findViewById(R.id.line1);
- views.labelView = (TextView) view.findViewById(R.id.label);
- views.numberView = (TextView) view.findViewById(R.id.number);
- views.dateView = (TextView) view.findViewById(R.id.date);
- views.iconView = (ImageView) view.findViewById(R.id.call_type_icon);
- views.callView = view.findViewById(R.id.call_icon);
- views.callView.setOnClickListener(this);
- views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
- views.groupSize = (TextView) view.findViewById(R.id.groupSize);
- view.setTag(views);
- }
-
- public void bindView(Context context, View view, Cursor c) {
- final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
-
- String number = c.getString(NUMBER_COLUMN_INDEX);
- String formattedNumber = null;
- String callerName = c.getString(CALLER_NAME_COLUMN_INDEX);
- int callerNumberType = c.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
- String callerNumberLabel = c.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
- String countryIso = c.getString(COUNTRY_ISO_COLUMN_INDEX);
- // Store away the number so we can call it directly if you click on the call icon
- views.callView.setTag(number);
-
- // Lookup contacts with this number
- ContactInfo info = mContactInfo.get(number);
- if (info == null) {
- // Mark it as empty and queue up a request to find the name
- // The db request should happen on a non-UI thread
- info = ContactInfo.EMPTY;
- mContactInfo.put(number, info);
- enqueueRequest(number, c.getPosition(),
- callerName, callerNumberType, callerNumberLabel);
- } else if (info != ContactInfo.EMPTY) { // Has been queried
- // Check if any data is different from the data cached in the
- // calls db. If so, queue the request so that we can update
- // the calls db.
- if (!TextUtils.equals(info.name, callerName)
- || info.type != callerNumberType
- || !TextUtils.equals(info.label, callerNumberLabel)) {
- // Something is amiss, so sync up.
- enqueueRequest(number, c.getPosition(),
- callerName, callerNumberType, callerNumberLabel);
- }
-
- // Format and cache phone number for found contact
- if (info.formattedNumber == null) {
- info.formattedNumber =
- formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
- }
- formattedNumber = info.formattedNumber;
- }
-
- String name = info.name;
- int ntype = info.type;
- String label = info.label;
- // If there's no name cached in our hashmap, but there's one in the
- // calls db, use the one in the calls db. Otherwise the name in our
- // hashmap is more recent, so it has precedence.
- if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) {
- name = callerName;
- ntype = callerNumberType;
- label = callerNumberLabel;
-
- // Format the cached call_log phone number
- formattedNumber = formatPhoneNumber(number, null, countryIso);
- }
- // Set the text lines and call icon.
- // Assumes the call back feature is on most of the
- // time. For private and unknown numbers: hide it.
- views.callView.setVisibility(View.VISIBLE);
-
- if (!TextUtils.isEmpty(name)) {
- views.line1View.setText(name);
- views.labelView.setVisibility(View.VISIBLE);
-
- // "type" and "label" are currently unused for SIP addresses.
- CharSequence numberLabel = null;
- if (!PhoneNumberUtils.isUriNumber(number)) {
- numberLabel = Phone.getDisplayLabel(context, ntype, label,
- mLabelArray);
- }
- views.numberView.setVisibility(View.VISIBLE);
- views.numberView.setText(formattedNumber);
- if (!TextUtils.isEmpty(numberLabel)) {
- views.labelView.setText(numberLabel);
- views.labelView.setVisibility(View.VISIBLE);
-
- // Zero out the numberView's left margin (see below)
- ViewGroup.MarginLayoutParams numberLP =
- (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
- numberLP.leftMargin = 0;
- views.numberView.setLayoutParams(numberLP);
- } else {
- // There's nothing to display in views.labelView, so hide it.
- // We can't set it to View.GONE, since it's the anchor for
- // numberView in the RelativeLayout, so make it INVISIBLE.
- // Also, we need to manually *subtract* some left margin from
- // numberView to compensate for the right margin built in to
- // labelView (otherwise the number will be indented by a very
- // slight amount).
- // TODO: a cleaner fix would be to contain both the label and
- // number inside a LinearLayout, and then set labelView *and*
- // its padding to GONE when there's no label to display.
- views.labelView.setText(null);
- views.labelView.setVisibility(View.INVISIBLE);
-
- ViewGroup.MarginLayoutParams labelLP =
- (ViewGroup.MarginLayoutParams) views.labelView.getLayoutParams();
- ViewGroup.MarginLayoutParams numberLP =
- (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
- // Equivalent to setting android:layout_marginLeft in XML
- numberLP.leftMargin = -labelLP.rightMargin;
- views.numberView.setLayoutParams(numberLP);
- }
- } else {
- if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
- number = getString(R.string.unknown);
- views.callView.setVisibility(View.INVISIBLE);
- } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
- number = getString(R.string.private_num);
- views.callView.setVisibility(View.INVISIBLE);
- } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
- number = getString(R.string.payphone);
- } else if (PhoneNumberUtils.extractNetworkPortion(number)
- .equals(mVoiceMailNumber)) {
- number = getString(R.string.voicemail);
- } else {
- // Just a raw number, and no cache, so format it nicely
- number = formatPhoneNumber(number, null, countryIso);
- }
-
- views.line1View.setText(number);
- views.numberView.setVisibility(View.GONE);
- views.labelView.setVisibility(View.GONE);
- }
-
- long date = c.getLong(DATE_COLUMN_INDEX);
-
- // Set the date/time field by mixing relative and absolute times.
- int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
-
- views.dateView.setText(DateUtils.getRelativeTimeSpanString(date,
- System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags));
-
- if (views.iconView != null) {
- int type = c.getInt(CALL_TYPE_COLUMN_INDEX);
- // Set the icon
- switch (type) {
- case Calls.INCOMING_TYPE:
- views.iconView.setImageDrawable(mDrawableIncoming);
- break;
-
- case Calls.OUTGOING_TYPE:
- views.iconView.setImageDrawable(mDrawableOutgoing);
- break;
-
- case Calls.MISSED_TYPE:
- views.iconView.setImageDrawable(mDrawableMissed);
- break;
- }
- }
-
- // Listen for the first draw
- if (mPreDrawListener == null) {
- mFirst = true;
- mPreDrawListener = this;
- view.getViewTreeObserver().addOnPreDrawListener(this);
- }
- }
- }
-
- private static final class QueryHandler extends AsyncQueryHandler {
- private final WeakReference<CallLogActivity> mActivity;
-
- /**
- * Simple handler that wraps background calls to catch
- * {@link SQLiteException}, such as when the disk is full.
- */
- protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
- public CatchingWorkerHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- try {
- // Perform same query while catching any exceptions
- super.handleMessage(msg);
- } catch (SQLiteDiskIOException e) {
- Log.w(TAG, "Exception on background worker thread", e);
- } catch (SQLiteFullException e) {
- Log.w(TAG, "Exception on background worker thread", e);
- } catch (SQLiteDatabaseCorruptException e) {
- Log.w(TAG, "Exception on background worker thread", e);
- }
- }
- }
-
- @Override
- protected Handler createHandler(Looper looper) {
- // Provide our special handler that catches exceptions
- return new CatchingWorkerHandler(looper);
- }
-
- public QueryHandler(Context context) {
- super(context.getContentResolver());
- mActivity = new WeakReference<CallLogActivity>(
- (CallLogActivity) context);
- }
-
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- final CallLogActivity activity = mActivity.get();
- if (activity != null && !activity.isFinishing()) {
- final CallLogActivity.CallLogAdapter callsAdapter = activity.mAdapter;
- callsAdapter.setLoading(false);
- callsAdapter.changeCursor(cursor);
- if (activity.mScrollToTop) {
- if (activity.mList.getFirstVisiblePosition() > 5) {
- activity.mList.setSelection(5);
- }
- activity.mList.smoothScrollToPosition(0);
- activity.mScrollToTop = false;
- }
- } else {
- cursor.close();
- }
- }
- }
+ private CallLogFragment mFragment;
@Override
- protected void onCreate(Bundle state) {
- super.onCreate(state);
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
setContentView(R.layout.call_log_activity);
// Typing here goes to the dialer
setDefaultKeyMode(DEFAULT_KEYS_DIALER);
- mAdapter = new CallLogAdapter();
- getListView().setOnCreateContextMenuListener(this);
- setListAdapter(mAdapter);
-
- mVoiceMailNumber = ((TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE))
- .getVoiceMailNumber();
- mQueryHandler = new QueryHandler(this);
-
- mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(this);
+ mFragment = (CallLogFragment) getFragmentManager().findFragmentById(
+ R.id.call_log_fragment);
}
- @Override
- protected void onStart() {
- mScrollToTop = true;
- super.onStart();
- }
-
- @Override
- protected void onResume() {
- // The adapter caches looked up numbers, clear it so they will get
- // looked up again.
- if (mAdapter != null) {
- mAdapter.clearCache();
- }
-
- startQuery();
- resetNewCallsFlag();
-
- super.onResume();
-
- mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
- }
-
- @Override
- protected void onPause() {
- super.onPause();
-
- // Kill the requests thread
- mAdapter.stopRequestProcessing();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mAdapter.stopRequestProcessing();
- mAdapter.changeCursor(null);
+ public CallLogFragment getFragment() {
+ return mFragment;
}
@Override
@@ -894,10 +66,10 @@
// immediately receive focus if the keyguard screen is above it.
if (hasFocus) {
try {
- ITelephony iTelephony =
+ ITelephony telephony =
ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
- if (iTelephony != null) {
- iTelephony.cancelMissedCallsNotification();
+ if (telephony != null) {
+ telephony.cancelMissedCallsNotification();
} else {
Log.w(TAG, "Telephony service is null, can't call " +
"cancelMissedCallsNotification");
@@ -908,240 +80,6 @@
}
}
- /**
- * Format the given phone number
- *
- * @param number the number to be formatted.
- * @param normalizedNumber the normalized number of the given number.
- * @param countryIso the ISO 3166-1 two letters country code, the country's
- * convention will be used to format the number if the normalized
- * phone is null.
- *
- * @return the formatted number, or the given number if it was formatted.
- */
- private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
- if (TextUtils.isEmpty(number)) {
- return "";
- }
- // If "number" is really a SIP address, don't try to do any formatting at all.
- if (PhoneNumberUtils.isUriNumber(number)) {
- return number;
- }
- if (TextUtils.isEmpty(countryIso)) {
- countryIso = mCurrentCountryIso;
- }
- return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
- }
-
- private void resetNewCallsFlag() {
- // Mark all "new" missed calls as not new anymore
- StringBuilder where = new StringBuilder("type=");
- where.append(Calls.MISSED_TYPE);
- where.append(" AND new=1");
-
- ContentValues values = new ContentValues(1);
- values.put(Calls.NEW, "0");
- mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI,
- values, where.toString(), null);
- }
-
- private void startQuery() {
- mAdapter.setLoading(true);
-
- // Cancel any pending queries
- mQueryHandler.cancelOperation(QUERY_TOKEN);
- mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI,
- CALL_LOG_PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, MENU_ITEM_DELETE_ALL, 0, R.string.recentCalls_deleteAll)
- .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
- return true;
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
- AdapterView.AdapterContextMenuInfo menuInfo;
- try {
- menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn;
- } catch (ClassCastException e) {
- Log.e(TAG, "bad menuInfoIn", e);
- return;
- }
-
- Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position);
-
- String number = cursor.getString(NUMBER_COLUMN_INDEX);
- Uri numberUri = null;
- boolean isVoicemail = false;
- boolean isSipNumber = false;
- if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
- number = getString(R.string.unknown);
- } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
- number = getString(R.string.private_num);
- } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
- number = getString(R.string.payphone);
- } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) {
- number = getString(R.string.voicemail);
- numberUri = Uri.parse("voicemail:x");
- isVoicemail = true;
- } else if (PhoneNumberUtils.isUriNumber(number)) {
- numberUri = Uri.fromParts("sip", number, null);
- isSipNumber = true;
- } else {
- numberUri = Uri.fromParts("tel", number, null);
- }
-
- ContactInfo info = mAdapter.getContactInfo(number);
- boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY);
- if (contactInfoPresent) {
- menu.setHeaderTitle(info.name);
- } else {
- menu.setHeaderTitle(number);
- }
-
- if (numberUri != null) {
- Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri);
- menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number))
- .setIntent(intent);
- }
-
- if (contactInfoPresent) {
- menu.add(0, 0, 0, R.string.menu_viewContact)
- .setIntent(new Intent(Intent.ACTION_VIEW,
- ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId)));
- }
-
- if (numberUri != null && !isVoicemail && !isSipNumber) {
- menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall)
- .setIntent(new Intent(Intent.ACTION_DIAL, numberUri));
- menu.add(0, 0, 0, R.string.menu_sendTextMessage)
- .setIntent(new Intent(Intent.ACTION_SENDTO,
- Uri.fromParts("sms", number, null)));
- }
-
- // "Add to contacts" item, if this entry isn't already associated with a contact
- if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) {
- // TODO: This item is currently disabled for SIP addresses, because
- // the Insert.PHONE extra only works correctly for PSTN numbers.
- //
- // To fix this for SIP addresses, we need to:
- // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
- // the current number is a SIP address
- // - update the contacts UI code to handle Insert.SIP_ADDRESS by
- // updating the SipAddress field
- // and then we can remove the "!isSipNumber" check above.
-
- Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
- intent.setType(Contacts.CONTENT_ITEM_TYPE);
- intent.putExtra(Insert.PHONE, number);
- menu.add(0, 0, 0, R.string.recentCalls_addToContact)
- .setIntent(intent);
- }
- menu.add(0, MENU_ITEM_DELETE, 0, R.string.recentCalls_removeFromRecentList);
- }
-
- @Override
- protected Dialog onCreateDialog(int id, Bundle args) {
- switch (id) {
- case DIALOG_CONFIRM_DELETE_ALL:
- final ContentResolver resolver = getContentResolver();
- final OnClickListener okListener = new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- final ProgressDialog progressDialog = ProgressDialog.show(
- CallLogActivity.this,
- getString(R.string.clearCallLogProgress_title),
- "", true, false);
- final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- resolver.delete(Calls.CONTENT_URI, null, null);
- return null;
- }
- @Override
- protected void onPostExecute(Void result) {
- progressDialog.dismiss();
- // TODO The change notification should do this automatically, but
- // it isn't working right now. Remove this when the change
- // notification is working properly.
- startQuery();
- }
- };
- // TODO: Once we have the API, we should configure this ProgressDialog
- // to only show up after a certain time (e.g. 150ms)
- progressDialog.show();
- task.execute();
- }
- };
- return new AlertDialog.Builder(this)
- .setTitle(R.string.clearCallLogConfirmation_title)
- .setIconAttribute(android.R.attr.alertDialogIcon)
- .setMessage(R.string.clearCallLogConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, okListener)
- .setCancelable(true)
- .create();
- }
- return null;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_ITEM_DELETE_ALL: {
- showDialog(DIALOG_CONFIRM_DELETE_ALL);
- return true;
- }
-
- case MENU_ITEM_VIEW_CONTACTS: {
- Intent intent = new Intent(Intent.ACTION_VIEW, Contacts.CONTENT_URI);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- return true;
- }
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- // Convert the menu info to the proper type
- AdapterView.AdapterContextMenuInfo menuInfo;
- try {
- menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
- } catch (ClassCastException e) {
- Log.e(TAG, "bad menuInfoIn", e);
- return false;
- }
-
- switch (item.getItemId()) {
- case MENU_ITEM_DELETE: {
- Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position);
- int groupSize = 1;
- if (mAdapter.isGroupHeader(menuInfo.position)) {
- groupSize = mAdapter.getGroupSize(menuInfo.position);
- }
-
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < groupSize; i++) {
- if (i != 0) {
- sb.append(",");
- cursor.moveToNext();
- }
- long id = cursor.getLong(ID_COLUMN_INDEX);
- sb.append(id);
- }
-
- getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + " IN (" + sb + ")",
- null);
- }
- }
- return super.onContextItemSelected(item);
- }
-
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
@@ -1177,104 +115,12 @@
// Fall through and try to call the contact
}
- callEntry(getListView().getSelectedItemPosition());
+ mFragment.callSelectedEntry();
return true;
}
return super.onKeyUp(keyCode, event);
}
- /*
- * Get the number from the Contacts, if available, since sometimes
- * the number provided by caller id may not be formatted properly
- * depending on the carrier (roaming) in use at the time of the
- * incoming call.
- * Logic : If the caller-id number starts with a "+", use it
- * Else if the number in the contacts starts with a "+", use that one
- * Else if the number in the contacts is longer, use that one
- */
- private String getBetterNumberFromContacts(String number) {
- String matchingNumber = null;
- // Look in the cache first. If it's not found then query the Phones db
- ContactInfo ci = mAdapter.mContactInfo.get(number);
- if (ci != null && ci != ContactInfo.EMPTY) {
- matchingNumber = ci.number;
- } else {
- try {
- Cursor phonesCursor =
- CallLogActivity.this.getContentResolver().query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
- number),
- PHONES_PROJECTION, null, null, null);
- if (phonesCursor != null) {
- if (phonesCursor.moveToFirst()) {
- matchingNumber = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX);
- }
- phonesCursor.close();
- }
- } catch (Exception e) {
- // Use the number from the call log
- }
- }
- if (!TextUtils.isEmpty(matchingNumber) &&
- (matchingNumber.startsWith("+")
- || matchingNumber.length() > number.length())) {
- number = matchingNumber;
- }
- return number;
- }
-
- private void callEntry(int position) {
- if (position < 0) {
- // In touch mode you may often not have something selected, so
- // just call the first entry to make sure that [send] [send] calls the
- // most recent entry.
- position = 0;
- }
- final Cursor cursor = (Cursor)mAdapter.getItem(position);
- if (cursor != null) {
- String number = cursor.getString(NUMBER_COLUMN_INDEX);
- if (TextUtils.isEmpty(number)
- || number.equals(CallerInfo.UNKNOWN_NUMBER)
- || number.equals(CallerInfo.PRIVATE_NUMBER)
- || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
- // This number can't be called, do nothing
- return;
- }
- Intent intent;
- // If "number" is really a SIP address, construct a sip: URI.
- if (PhoneNumberUtils.isUriNumber(number)) {
- intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts("sip", number, null));
- } else {
- // We're calling a regular PSTN phone number.
- // Construct a tel: URI, but do some other possible cleanup first.
- int callType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
- if (!number.startsWith("+") &&
- (callType == Calls.INCOMING_TYPE
- || callType == Calls.MISSED_TYPE)) {
- // If the caller-id matches a contact with a better qualified number, use it
- number = getBetterNumberFromContacts(number);
- }
- intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts("tel", number, null));
- }
- intent.setFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- startActivity(intent);
- }
- }
-
- @Override
- protected void onListItemClick(ListView l, View v, int position, long id) {
- if (mAdapter.isGroupHeader(position)) {
- mAdapter.toggleGroup(position);
- } else {
- Intent intent = new Intent(this, CallDetailActivity.class);
- intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
- startActivity(intent);
- }
- }
-
@Override
public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
boolean globalSearch) {
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
new file mode 100644
index 0000000..3850a0b
--- /dev/null
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -0,0 +1,1171 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.android.contacts.CallDetailActivity;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.internal.telephony.CallerInfo;
+
+import android.app.ListFragment;
+import android.content.AsyncQueryHandler;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * Displays a list of call log entries.
+ */
+public class CallLogFragment extends ListFragment
+ implements View.OnCreateContextMenuListener {
+ private static final String TAG = "CallLogFragment";
+
+ /** The query for the call log table */
+ private static final class CallLogQuery {
+ public static final String[] _PROJECTION = new String[] {
+ Calls._ID,
+ Calls.NUMBER,
+ Calls.DATE,
+ Calls.DURATION,
+ Calls.TYPE,
+ Calls.CACHED_NAME,
+ Calls.CACHED_NUMBER_TYPE,
+ Calls.CACHED_NUMBER_LABEL,
+ Calls.COUNTRY_ISO};
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int CALLER_NAME = 5;
+ public static final int CALLER_NUMBERTYPE = 6;
+ public static final int CALLER_NUMBERLABEL = 7;
+ public static final int COUNTRY_ISO = 8;
+ }
+
+ /** The query to use for the phones table */
+ private static final class PhoneQuery {
+ public static final String[] _PROJECTION = new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER};
+
+ public static final int PERSON_ID = 0;
+ public static final int NAME = 1;
+ public static final int PHONE_TYPE = 2;
+ public static final int LABEL = 3;
+ public static final int MATCHED_NUMBER = 4;
+ public static final int NORMALIZED_NUMBER = 5;
+ }
+
+ private static final class MenuItems {
+ public static final int DELETE = 1;
+ }
+
+ private static final class OptionsMenuItems {
+ public static final int DELETE_ALL = 1;
+ }
+
+ private static final int QUERY_TOKEN = 53;
+ private static final int UPDATE_TOKEN = 54;
+
+ private CallLogAdapter mAdapter;
+ private QueryHandler mQueryHandler;
+ private String mVoiceMailNumber;
+ private String mCurrentCountryIso;
+ private boolean mScrollToTop;
+
+ public static final class ContactInfo {
+ public long personId;
+ public String name;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ public String normalizedNumber;
+
+ public static ContactInfo EMPTY = new ContactInfo();
+ }
+
+ public static final class CallLogListItemViews {
+ public TextView line1View;
+ public TextView labelView;
+ public TextView numberView;
+ public TextView dateView;
+ public ImageView iconView;
+ public View callView;
+ public ImageView groupIndicator;
+ public TextView groupSize;
+ }
+
+ public static final class CallerInfoQuery {
+ public String number;
+ public int position;
+ public String name;
+ public int numberType;
+ public String numberLabel;
+ }
+
+ /** Adapter class to fill in data for the Call Log */
+ public final class CallLogAdapter extends GroupingListAdapter
+ implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
+ HashMap<String,ContactInfo> mContactInfo;
+ private final LinkedList<CallerInfoQuery> mRequests;
+ private volatile boolean mDone;
+ private boolean mLoading = true;
+ ViewTreeObserver.OnPreDrawListener mPreDrawListener;
+ private static final int REDRAW = 1;
+ private static final int START_THREAD = 2;
+ private boolean mFirst;
+ private Thread mCallerIdThread;
+
+ private CharSequence[] mLabelArray;
+
+ private Drawable mDrawableIncoming;
+ private Drawable mDrawableOutgoing;
+ private Drawable mDrawableMissed;
+
+ /**
+ * Reusable char array buffers.
+ */
+ private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
+ private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
+
+ @Override
+ public void onClick(View view) {
+ String number = (String) view.getTag();
+ if (!TextUtils.isEmpty(number)) {
+ // Here, "number" can either be a PSTN phone number or a
+ // SIP address. So turn it into either a tel: URI or a
+ // sip: URI, as appropriate.
+ Uri callUri;
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ callUri = Uri.fromParts("sip", number, null);
+ } else {
+ callUri = Uri.fromParts("tel", number, null);
+ }
+ startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri));
+ }
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (mFirst) {
+ mHandler.sendEmptyMessageDelayed(START_THREAD, 1000);
+ mFirst = false;
+ }
+ return true;
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case REDRAW:
+ notifyDataSetChanged();
+ break;
+ case START_THREAD:
+ startRequestProcessing();
+ break;
+ }
+ }
+ };
+
+ public CallLogAdapter() {
+ super(getActivity());
+
+ mContactInfo = new HashMap<String,ContactInfo>();
+ mRequests = new LinkedList<CallerInfoQuery>();
+ mPreDrawListener = null;
+
+ mDrawableIncoming = getResources().getDrawable(
+ R.drawable.ic_call_log_list_incoming_call);
+ mDrawableOutgoing = getResources().getDrawable(
+ R.drawable.ic_call_log_list_outgoing_call);
+ mDrawableMissed = getResources().getDrawable(
+ R.drawable.ic_call_log_list_missed_call);
+ mLabelArray = getResources().getTextArray(com.android.internal.R.array.phoneTypes);
+ }
+
+ /**
+ * Requery on background thread when {@link Cursor} changes.
+ */
+ @Override
+ protected void onContentChanged() {
+ // Start async requery
+ startQuery();
+ }
+
+ void setLoading(boolean loading) {
+ mLoading = loading;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ if (mLoading) {
+ // We don't want the empty state to show when loading.
+ return false;
+ } else {
+ return super.isEmpty();
+ }
+ }
+
+ public ContactInfo getContactInfo(String number) {
+ return mContactInfo.get(number);
+ }
+
+ public void startRequestProcessing() {
+ mDone = false;
+ mCallerIdThread = new Thread(this);
+ mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+ mCallerIdThread.start();
+ }
+
+ public void stopRequestProcessing() {
+ mDone = true;
+ if (mCallerIdThread != null) mCallerIdThread.interrupt();
+ }
+
+ public void clearCache() {
+ synchronized (mContactInfo) {
+ mContactInfo.clear();
+ }
+ }
+
+ private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) {
+ // Check if they are different. If not, don't update.
+ if (TextUtils.equals(ciq.name, ci.name)
+ && TextUtils.equals(ciq.numberLabel, ci.label)
+ && ciq.numberType == ci.type) {
+ return;
+ }
+ ContentValues values = new ContentValues(3);
+ values.put(Calls.CACHED_NAME, ci.name);
+ values.put(Calls.CACHED_NUMBER_TYPE, ci.type);
+ values.put(Calls.CACHED_NUMBER_LABEL, ci.label);
+
+ try {
+ getActivity().getContentResolver().update(Calls.CONTENT_URI, values,
+ Calls.NUMBER + "='" + ciq.number + "'", null);
+ } catch (SQLiteDiskIOException e) {
+ Log.w(TAG, "Exception while updating call info", e);
+ } catch (SQLiteFullException e) {
+ Log.w(TAG, "Exception while updating call info", e);
+ } catch (SQLiteDatabaseCorruptException e) {
+ Log.w(TAG, "Exception while updating call info", e);
+ }
+ }
+
+ private void enqueueRequest(String number, int position,
+ String name, int numberType, String numberLabel) {
+ CallerInfoQuery ciq = new CallerInfoQuery();
+ ciq.number = number;
+ ciq.position = position;
+ ciq.name = name;
+ ciq.numberType = numberType;
+ ciq.numberLabel = numberLabel;
+ synchronized (mRequests) {
+ mRequests.add(ciq);
+ mRequests.notifyAll();
+ }
+ }
+
+ private boolean queryContactInfo(CallerInfoQuery ciq) {
+ // First check if there was a prior request for the same number
+ // that was already satisfied
+ ContactInfo info = mContactInfo.get(ciq.number);
+ boolean needNotify = false;
+ if (info != null && info != ContactInfo.EMPTY) {
+ return true;
+ } else {
+ // Ok, do a fresh Contacts lookup for ciq.number.
+ boolean infoUpdated = false;
+
+ if (PhoneNumberUtils.isUriNumber(ciq.number)) {
+ // This "number" is really a SIP address.
+
+ // TODO: This code is duplicated from the
+ // CallerInfoAsyncQuery class. To avoid that, could the
+ // code here just use CallerInfoAsyncQuery, rather than
+ // manually running ContentResolver.query() itself?
+
+ // We look up SIP addresses directly in the Data table:
+ Uri contactRef = Data.CONTENT_URI;
+
+ // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+ //
+ // Also note we use "upper(data1)" in the WHERE clause, and
+ // uppercase the incoming SIP address, in order to do a
+ // case-insensitive match.
+ //
+ // TODO: May also need to normalize by adding "sip:" as a
+ // prefix, if we start storing SIP addresses that way in the
+ // database.
+ String selection = "upper(" + Data.DATA1 + ")=?"
+ + " AND "
+ + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
+ String[] selectionArgs = new String[] { ciq.number.toUpperCase() };
+
+ Cursor dataTableCursor =
+ getActivity().getContentResolver().query(
+ contactRef,
+ null, // projection
+ selection, // selection
+ selectionArgs, // selectionArgs
+ null); // sortOrder
+
+ if (dataTableCursor != null) {
+ if (dataTableCursor.moveToFirst()) {
+ info = new ContactInfo();
+
+ // TODO: we could slightly speed this up using an
+ // explicit projection (and thus not have to do
+ // those getColumnIndex() calls) but the benefit is
+ // very minimal.
+
+ // Note the Data.CONTACT_ID column here is
+ // equivalent to the PERSON_ID_COLUMN_INDEX column
+ // we use with "phonesCursor" below.
+ info.personId = dataTableCursor.getLong(
+ dataTableCursor.getColumnIndex(Data.CONTACT_ID));
+ info.name = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
+ // "type" and "label" are currently unused for SIP addresses
+ info.type = SipAddress.TYPE_OTHER;
+ info.label = null;
+
+ // And "number" is the SIP address.
+ // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+ info.number = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DATA1));
+ info.normalizedNumber = null; // meaningless for SIP addresses
+
+ infoUpdated = true;
+ }
+ dataTableCursor.close();
+ }
+ } else {
+ // "number" is a regular phone number, so use the
+ // PhoneLookup table:
+ Cursor phonesCursor =
+ getActivity().getContentResolver().query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(ciq.number)),
+ PhoneQuery._PROJECTION, null, null, null);
+ if (phonesCursor != null) {
+ if (phonesCursor.moveToFirst()) {
+ info = new ContactInfo();
+ info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+ info.name = phonesCursor.getString(PhoneQuery.NAME);
+ info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phonesCursor.getString(PhoneQuery.LABEL);
+ info.number = phonesCursor
+ .getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phonesCursor
+ .getString(PhoneQuery.NORMALIZED_NUMBER);
+
+ infoUpdated = true;
+ }
+ phonesCursor.close();
+ }
+ }
+
+ if (infoUpdated) {
+ // New incoming phone number invalidates our formatted
+ // cache. Any cache fills happen only on the GUI thread.
+ info.formattedNumber = null;
+
+ mContactInfo.put(ciq.number, info);
+
+ // Inform list to update this item, if in view
+ needNotify = true;
+ }
+ }
+ if (info != null) {
+ updateCallLog(ciq, info);
+ }
+ return needNotify;
+ }
+
+ /*
+ * Handles requests for contact name and number type
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run() {
+ boolean needNotify = false;
+ while (!mDone) {
+ CallerInfoQuery ciq = null;
+ synchronized (mRequests) {
+ if (!mRequests.isEmpty()) {
+ ciq = mRequests.removeFirst();
+ } else {
+ if (needNotify) {
+ needNotify = false;
+ mHandler.sendEmptyMessage(REDRAW);
+ }
+ try {
+ mRequests.wait(1000);
+ } catch (InterruptedException ie) {
+ // Ignore and continue processing requests
+ }
+ }
+ }
+ if (ciq != null && queryContactInfo(ciq)) {
+ needNotify = true;
+ }
+ }
+ }
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+
+ int count = cursor.getCount();
+ if (count == 0) {
+ return;
+ }
+
+ int groupItemCount = 1;
+
+ CharArrayBuffer currentValue = mBuffer1;
+ CharArrayBuffer value = mBuffer2;
+ cursor.moveToFirst();
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue);
+ int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ for (int i = 1; i < count; i++) {
+ cursor.moveToNext();
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, value);
+ boolean sameNumber = equalPhoneNumbers(value, currentValue);
+
+ // Group adjacent calls with the same number. Make an exception
+ // for the latest item if it was a missed call. We don't want
+ // a missed call to be hidden inside a group.
+ if (sameNumber && currentCallType != Calls.MISSED_TYPE) {
+ groupItemCount++;
+ } else {
+ if (groupItemCount > 1) {
+ addGroup(i - groupItemCount, groupItemCount, false);
+ }
+
+ groupItemCount = 1;
+
+ // Swap buffers
+ CharArrayBuffer temp = currentValue;
+ currentValue = value;
+ value = temp;
+
+ // If we have just examined a row following a missed call, make
+ // sure that it is grouped with subsequent calls from the same number
+ // even if it was also missed.
+ if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
+ currentCallType = 0; // "not a missed call"
+ } else {
+ currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ }
+ }
+ }
+ if (groupItemCount > 1) {
+ addGroup(count - groupItemCount, groupItemCount, false);
+ }
+ }
+
+ protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+
+ // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
+ // string allocation
+ return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied),
+ new String(buffer2.data, 0, buffer2.sizeCopied));
+ }
+
+
+ @Override
+ public View newStandAloneView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ public void bindStandAloneView(View view, Context context, Cursor cursor) {
+ bindView(context, view, cursor);
+ }
+
+ @Override
+ protected View newChildView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor) {
+ bindView(context, view, cursor);
+ }
+
+ @Override
+ protected View newGroupView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+ int groupIndicator = expanded
+ ? com.android.internal.R.drawable.expander_ic_maximized
+ : com.android.internal.R.drawable.expander_ic_minimized;
+ views.groupIndicator.setImageResource(groupIndicator);
+ views.groupSize.setText("(" + groupSize + ")");
+ bindView(context, view, cursor);
+ }
+
+ private void findAndCacheViews(View view) {
+
+ // Get the views to bind to
+ CallLogListItemViews views = new CallLogListItemViews();
+ views.line1View = (TextView) view.findViewById(R.id.line1);
+ views.labelView = (TextView) view.findViewById(R.id.label);
+ views.numberView = (TextView) view.findViewById(R.id.number);
+ views.dateView = (TextView) view.findViewById(R.id.date);
+ views.iconView = (ImageView) view.findViewById(R.id.call_type_icon);
+ views.callView = view.findViewById(R.id.call_icon);
+ views.callView.setOnClickListener(this);
+ views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
+ views.groupSize = (TextView) view.findViewById(R.id.groupSize);
+ view.setTag(views);
+ }
+
+ public void bindView(Context context, View view, Cursor c) {
+ final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+
+ String number = c.getString(CallLogQuery.NUMBER);
+ String formattedNumber = null;
+ String callerName = c.getString(CallLogQuery.CALLER_NAME);
+ int callerNumberType = c.getInt(CallLogQuery.CALLER_NUMBERTYPE);
+ String callerNumberLabel = c.getString(CallLogQuery.CALLER_NUMBERLABEL);
+ String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+ // Store away the number so we can call it directly if you click on the call icon
+ views.callView.setTag(number);
+
+ // Lookup contacts with this number
+ ContactInfo info = mContactInfo.get(number);
+ if (info == null) {
+ // Mark it as empty and queue up a request to find the name
+ // The db request should happen on a non-UI thread
+ info = ContactInfo.EMPTY;
+ mContactInfo.put(number, info);
+ enqueueRequest(number, c.getPosition(),
+ callerName, callerNumberType, callerNumberLabel);
+ } else if (info != ContactInfo.EMPTY) { // Has been queried
+ // Check if any data is different from the data cached in the
+ // calls db. If so, queue the request so that we can update
+ // the calls db.
+ if (!TextUtils.equals(info.name, callerName)
+ || info.type != callerNumberType
+ || !TextUtils.equals(info.label, callerNumberLabel)) {
+ // Something is amiss, so sync up.
+ enqueueRequest(number, c.getPosition(),
+ callerName, callerNumberType, callerNumberLabel);
+ }
+
+ // Format and cache phone number for found contact
+ if (info.formattedNumber == null) {
+ info.formattedNumber =
+ formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
+ }
+ formattedNumber = info.formattedNumber;
+ }
+
+ String name = info.name;
+ int ntype = info.type;
+ String label = info.label;
+ // If there's no name cached in our hashmap, but there's one in the
+ // calls db, use the one in the calls db. Otherwise the name in our
+ // hashmap is more recent, so it has precedence.
+ if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) {
+ name = callerName;
+ ntype = callerNumberType;
+ label = callerNumberLabel;
+
+ // Format the cached call_log phone number
+ formattedNumber = formatPhoneNumber(number, null, countryIso);
+ }
+ // Set the text lines and call icon.
+ // Assumes the call back feature is on most of the
+ // time. For private and unknown numbers: hide it.
+ views.callView.setVisibility(View.VISIBLE);
+
+ if (!TextUtils.isEmpty(name)) {
+ views.line1View.setText(name);
+ views.labelView.setVisibility(View.VISIBLE);
+
+ // "type" and "label" are currently unused for SIP addresses.
+ CharSequence numberLabel = null;
+ if (!PhoneNumberUtils.isUriNumber(number)) {
+ numberLabel = Phone.getDisplayLabel(context, ntype, label,
+ mLabelArray);
+ }
+ views.numberView.setVisibility(View.VISIBLE);
+ views.numberView.setText(formattedNumber);
+ if (!TextUtils.isEmpty(numberLabel)) {
+ views.labelView.setText(numberLabel);
+ views.labelView.setVisibility(View.VISIBLE);
+
+ // Zero out the numberView's left margin (see below)
+ ViewGroup.MarginLayoutParams numberLP =
+ (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
+ numberLP.leftMargin = 0;
+ views.numberView.setLayoutParams(numberLP);
+ } else {
+ // There's nothing to display in views.labelView, so hide it.
+ // We can't set it to View.GONE, since it's the anchor for
+ // numberView in the RelativeLayout, so make it INVISIBLE.
+ // Also, we need to manually *subtract* some left margin from
+ // numberView to compensate for the right margin built in to
+ // labelView (otherwise the number will be indented by a very
+ // slight amount).
+ // TODO: a cleaner fix would be to contain both the label and
+ // number inside a LinearLayout, and then set labelView *and*
+ // its padding to GONE when there's no label to display.
+ views.labelView.setText(null);
+ views.labelView.setVisibility(View.INVISIBLE);
+
+ ViewGroup.MarginLayoutParams labelLP =
+ (ViewGroup.MarginLayoutParams) views.labelView.getLayoutParams();
+ ViewGroup.MarginLayoutParams numberLP =
+ (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
+ // Equivalent to setting android:layout_marginLeft in XML
+ numberLP.leftMargin = -labelLP.rightMargin;
+ views.numberView.setLayoutParams(numberLP);
+ }
+ } else {
+ if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
+ number = getString(R.string.unknown);
+ views.callView.setVisibility(View.INVISIBLE);
+ } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
+ number = getString(R.string.private_num);
+ views.callView.setVisibility(View.INVISIBLE);
+ } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+ number = getString(R.string.payphone);
+ } else if (PhoneNumberUtils.extractNetworkPortion(number)
+ .equals(mVoiceMailNumber)) {
+ number = getString(R.string.voicemail);
+ } else {
+ // Just a raw number, and no cache, so format it nicely
+ number = formatPhoneNumber(number, null, countryIso);
+ }
+
+ views.line1View.setText(number);
+ views.numberView.setVisibility(View.GONE);
+ views.labelView.setVisibility(View.GONE);
+ }
+
+ long date = c.getLong(CallLogQuery.DATE);
+
+ // Set the date/time field by mixing relative and absolute times.
+ int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
+
+ views.dateView.setText(DateUtils.getRelativeTimeSpanString(date,
+ System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags));
+
+ if (views.iconView != null) {
+ int type = c.getInt(CallLogQuery.CALL_TYPE);
+ // Set the icon
+ switch (type) {
+ case Calls.INCOMING_TYPE:
+ views.iconView.setImageDrawable(mDrawableIncoming);
+ break;
+
+ case Calls.OUTGOING_TYPE:
+ views.iconView.setImageDrawable(mDrawableOutgoing);
+ break;
+
+ case Calls.MISSED_TYPE:
+ views.iconView.setImageDrawable(mDrawableMissed);
+ break;
+ }
+ }
+
+ // Listen for the first draw
+ if (mPreDrawListener == null) {
+ mFirst = true;
+ mPreDrawListener = this;
+ view.getViewTreeObserver().addOnPreDrawListener(this);
+ }
+ }
+ }
+
+ private static final class QueryHandler extends AsyncQueryHandler {
+ private final WeakReference<CallLogFragment> mFragment;
+
+ /**
+ * Simple handler that wraps background calls to catch
+ * {@link SQLiteException}, such as when the disk is full.
+ */
+ protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+ public CatchingWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ try {
+ // Perform same query while catching any exceptions
+ super.handleMessage(msg);
+ } catch (SQLiteDiskIOException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ } catch (SQLiteFullException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ } catch (SQLiteDatabaseCorruptException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ }
+ }
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ // Provide our special handler that catches exceptions
+ return new CatchingWorkerHandler(looper);
+ }
+
+ public QueryHandler(CallLogFragment fragment) {
+ super(fragment.getActivity().getContentResolver());
+ mFragment = new WeakReference<CallLogFragment>(fragment);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ final CallLogFragment fragment = mFragment.get();
+ // TODO: Do we still need the isFinishing check if we use a fragmented-Tabs?
+ if (fragment != null && !fragment.getActivity().isFinishing()) {
+ final CallLogFragment.CallLogAdapter callsAdapter = fragment.mAdapter;
+ callsAdapter.setLoading(false);
+ callsAdapter.changeCursor(cursor);
+ if (fragment.mScrollToTop) {
+ final ListView listView = fragment.getListView();
+ if (listView.getFirstVisiblePosition() > 5) {
+ listView.setSelection(5);
+ }
+ listView.smoothScrollToPosition(0);
+ fragment.mScrollToTop = false;
+ }
+ } else {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService(
+ Context.TELEPHONY_SERVICE)).getVoiceMailNumber();
+ mQueryHandler = new QueryHandler(this);
+
+ mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ return inflater.inflate(R.layout.call_log_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setOnCreateContextMenuListener(this);
+ mAdapter = new CallLogAdapter();
+ setListAdapter(mAdapter);
+ }
+
+ @Override
+ public void onStart() {
+ mScrollToTop = true;
+ super.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ // The adapter caches looked up numbers, clear it so they will get
+ // looked up again.
+ if (mAdapter != null) {
+ mAdapter.clearCache();
+ }
+
+ startQuery();
+ resetNewCallsFlag();
+
+ super.onResume();
+
+ mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ // Kill the requests thread
+ mAdapter.stopRequestProcessing();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mAdapter.stopRequestProcessing();
+ mAdapter.changeCursor(null);
+ }
+
+ /**
+ * Format the given phone number
+ *
+ * @param number the number to be formatted.
+ * @param normalizedNumber the normalized number of the given number.
+ * @param countryIso the ISO 3166-1 two letters country code, the country's
+ * convention will be used to format the number if the normalized
+ * phone is null.
+ *
+ * @return the formatted number, or the given number if it was formatted.
+ */
+ private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ // If "number" is really a SIP address, don't try to do any formatting at all.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ return number;
+ }
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = mCurrentCountryIso;
+ }
+ return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+ }
+
+ private void resetNewCallsFlag() {
+ // Mark all "new" missed calls as not new anymore
+ StringBuilder where = new StringBuilder("type=");
+ where.append(Calls.MISSED_TYPE);
+ where.append(" AND new=1");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+ mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI,
+ values, where.toString(), null);
+ }
+
+ private void startQuery() {
+ mAdapter.setLoading(true);
+
+ // Cancel any pending queries
+ mQueryHandler.cancelOperation(QUERY_TOKEN);
+ mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI,
+ CallLogQuery._PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.add(0, OptionsMenuItems.DELETE_ALL, 0, R.string.recentCalls_deleteAll).setIcon(
+ android.R.drawable.ic_menu_close_clear_cancel);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+ AdapterView.AdapterContextMenuInfo menuInfo;
+ try {
+ menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn;
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfoIn", e);
+ return;
+ }
+
+ Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position);
+
+ String number = cursor.getString(CallLogQuery.NUMBER);
+ Uri numberUri = null;
+ boolean isVoicemail = false;
+ boolean isSipNumber = false;
+ if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
+ number = getString(R.string.unknown);
+ } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
+ number = getString(R.string.private_num);
+ } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+ number = getString(R.string.payphone);
+ } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) {
+ number = getString(R.string.voicemail);
+ numberUri = Uri.parse("voicemail:x");
+ isVoicemail = true;
+ } else if (PhoneNumberUtils.isUriNumber(number)) {
+ numberUri = Uri.fromParts("sip", number, null);
+ isSipNumber = true;
+ } else {
+ numberUri = Uri.fromParts("tel", number, null);
+ }
+
+ ContactInfo info = mAdapter.getContactInfo(number);
+ boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY);
+ if (contactInfoPresent) {
+ menu.setHeaderTitle(info.name);
+ } else {
+ menu.setHeaderTitle(number);
+ }
+
+ if (numberUri != null) {
+ Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri);
+ menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number))
+ .setIntent(intent);
+ }
+
+ if (contactInfoPresent) {
+ menu.add(0, 0, 0, R.string.menu_viewContact)
+ .setIntent(new Intent(Intent.ACTION_VIEW,
+ ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId)));
+ }
+
+ if (numberUri != null && !isVoicemail && !isSipNumber) {
+ menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall)
+ .setIntent(new Intent(Intent.ACTION_DIAL, numberUri));
+ menu.add(0, 0, 0, R.string.menu_sendTextMessage)
+ .setIntent(new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts("sms", number, null)));
+ }
+
+ // "Add to contacts" item, if this entry isn't already associated with a contact
+ if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) {
+ // TODO: This item is currently disabled for SIP addresses, because
+ // the Insert.PHONE extra only works correctly for PSTN numbers.
+ //
+ // To fix this for SIP addresses, we need to:
+ // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
+ // the current number is a SIP address
+ // - update the contacts UI code to handle Insert.SIP_ADDRESS by
+ // updating the SipAddress field
+ // and then we can remove the "!isSipNumber" check above.
+
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra(Insert.PHONE, number);
+ menu.add(0, 0, 0, R.string.recentCalls_addToContact)
+ .setIntent(intent);
+ }
+ menu.add(0, MenuItems.DELETE, 0, R.string.recentCalls_removeFromRecentList);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case OptionsMenuItems.DELETE_ALL: {
+ ClearCallLogDialog.show(getFragmentManager());
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // Convert the menu info to the proper type
+ AdapterView.AdapterContextMenuInfo menuInfo;
+ try {
+ menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ } catch (ClassCastException e) {
+ Log.e(TAG, "bad menuInfoIn", e);
+ return false;
+ }
+
+ switch (item.getItemId()) {
+ case MenuItems.DELETE: {
+ Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position);
+ int groupSize = 1;
+ if (mAdapter.isGroupHeader(menuInfo.position)) {
+ groupSize = mAdapter.getGroupSize(menuInfo.position);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < groupSize; i++) {
+ if (i != 0) {
+ sb.append(",");
+ cursor.moveToNext();
+ }
+ long id = cursor.getLong(CallLogQuery.ID);
+ sb.append(id);
+ }
+
+ getActivity().getContentResolver().delete(Calls.CONTENT_URI,
+ Calls._ID + " IN (" + sb + ")", null);
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /*
+ * Get the number from the Contacts, if available, since sometimes
+ * the number provided by caller id may not be formatted properly
+ * depending on the carrier (roaming) in use at the time of the
+ * incoming call.
+ * Logic : If the caller-id number starts with a "+", use it
+ * Else if the number in the contacts starts with a "+", use that one
+ * Else if the number in the contacts is longer, use that one
+ */
+ private String getBetterNumberFromContacts(String number) {
+ String matchingNumber = null;
+ // Look in the cache first. If it's not found then query the Phones db
+ ContactInfo ci = mAdapter.mContactInfo.get(number);
+ if (ci != null && ci != ContactInfo.EMPTY) {
+ matchingNumber = ci.number;
+ } else {
+ try {
+ Cursor phonesCursor = getActivity().getContentResolver().query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
+ PhoneQuery._PROJECTION, null, null, null);
+ if (phonesCursor != null) {
+ if (phonesCursor.moveToFirst()) {
+ matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ }
+ phonesCursor.close();
+ }
+ } catch (Exception e) {
+ // Use the number from the call log
+ }
+ }
+ if (!TextUtils.isEmpty(matchingNumber) &&
+ (matchingNumber.startsWith("+")
+ || matchingNumber.length() > number.length())) {
+ number = matchingNumber;
+ }
+ return number;
+ }
+
+ public void callSelectedEntry() {
+ int position = getListView().getSelectedItemPosition();
+ if (position < 0) {
+ // In touch mode you may often not have something selected, so
+ // just call the first entry to make sure that [send] [send] calls the
+ // most recent entry.
+ position = 0;
+ }
+ final Cursor cursor = (Cursor)mAdapter.getItem(position);
+ if (cursor != null) {
+ String number = cursor.getString(CallLogQuery.NUMBER);
+ if (TextUtils.isEmpty(number)
+ || number.equals(CallerInfo.UNKNOWN_NUMBER)
+ || number.equals(CallerInfo.PRIVATE_NUMBER)
+ || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+ // This number can't be called, do nothing
+ return;
+ }
+ Intent intent;
+ // If "number" is really a SIP address, construct a sip: URI.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("sip", number, null));
+ } else {
+ // We're calling a regular PSTN phone number.
+ // Construct a tel: URI, but do some other possible cleanup first.
+ int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ if (!number.startsWith("+") &&
+ (callType == Calls.INCOMING_TYPE
+ || callType == Calls.MISSED_TYPE)) {
+ // If the caller-id matches a contact with a better qualified number, use it
+ number = getBetterNumberFromContacts(number);
+ }
+ intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", number, null));
+ }
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ startActivity(intent);
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ if (mAdapter.isGroupHeader(position)) {
+ mAdapter.toggleGroup(position);
+ } else {
+ Intent intent = new Intent(getActivity(), CallDetailActivity.class);
+ intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
+ startActivity(intent);
+ }
+ }
+
+ public CallLogAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public String getVoiceMailNumber() {
+ return mVoiceMailNumber;
+ }
+}
diff --git a/src/com/android/contacts/calllog/ClearCallLogDialog.java b/src/com/android/contacts/calllog/ClearCallLogDialog.java
new file mode 100644
index 0000000..426732a
--- /dev/null
+++ b/src/com/android/contacts/calllog/ClearCallLogDialog.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.calllog;
+
+import com.android.contacts.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+
+/**
+ * Dialog that clears the call log after confirming with the user
+ */
+public class ClearCallLogDialog extends DialogFragment {
+ /** Preferred way to show this dialog */
+ public static void show(FragmentManager fragmentManager) {
+ ClearCallLogDialog dialog = new ClearCallLogDialog();
+ dialog.show(fragmentManager, "deleteCallLog");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final OnClickListener okListener = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(),
+ getString(R.string.clearCallLogProgress_title),
+ "", true, false);
+ final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ resolver.delete(Calls.CONTENT_URI, null, null);
+ return null;
+ }
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ }
+ };
+ // TODO: Once we have the API, we should configure this ProgressDialog
+ // to only show up after a certain time (e.g. 150ms)
+ progressDialog.show();
+ task.execute();
+ }
+ };
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clearCallLogConfirmation_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.clearCallLogConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, okListener)
+ .setCancelable(true)
+ .create();
+ }
+}
diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
index c6b3ea5..3eedef0 100644
--- a/tests/src/com/android/contacts/activities/CallLogActivityTests.java
+++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
@@ -16,6 +16,7 @@
package com.android.contacts.activities;
+import com.android.contacts.calllog.CallLogFragment;
import com.android.internal.telephony.CallerInfo;
import android.content.res.Resources;
@@ -66,8 +67,9 @@
// CallLogActivity to build the rows (view) in the call
// list. We reuse it with our own in-mem DB.
private CallLogActivity mActivity;
+ private CallLogFragment mFragment;
private FrameLayout mParentView;
- private CallLogActivity.CallLogAdapter mAdapter;
+ private CallLogFragment.CallLogAdapter mAdapter;
private String mVoicemail;
// In memory array to hold the rows corresponding to the 'calls' table.
@@ -82,7 +84,7 @@
private HashMap<Integer, Bitmap> mCallTypeIcons;
// An item in the call list. All the methods performing checks use it.
- private CallLogActivity.CallLogListItemViews mItem;
+ private CallLogFragment.CallLogListItemViews mItem;
// The list of views representing the data in the DB. View are in
// reverse order compare to the DB.
private View[] mList;
@@ -96,8 +98,9 @@
@Override
public void setUp() {
mActivity = getActivity();
- mVoicemail = mActivity.mVoiceMailNumber;
- mAdapter = mActivity.mAdapter;
+ mFragment = mActivity.getFragment();
+ mVoicemail = mFragment.getVoiceMailNumber();
+ mAdapter = mFragment.getAdapter();
mParentView = new FrameLayout(mActivity);
mCursor = new MatrixCursor(CALL_LOG_PROJECTION);
buildIconMap();
@@ -167,7 +170,7 @@
if (null == mList[i]) {
break;
}
- mItem = (CallLogActivity.CallLogListItemViews) mList[i].getTag();
+ mItem = (CallLogFragment.CallLogListItemViews) mList[i].getTag();
// callView tag is the phone number.
String number = (String) mItem.callView.getTag();