blob: ccd1ce3bd1fabd4dce9e82bb6e95f390ce51cd02 [file] [log] [blame]
/*
* Copyright (C) 2007 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;
import com.android.internal.telephony.CallerInfo;
import com.android.internal.telephony.ITelephony;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.content.ActivityNotFoundException;
import android.content.AsyncQueryHandler;
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.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.Intents.Insert;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.SpannableStringBuilder;
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;
import java.util.Locale;
/**
* Displays a list of call log entries.
*/
public class RecentCallsListActivity extends ListActivity
implements View.OnCreateContextMenuListener {
private static final String TAG = "RecentCallsList";
/** 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
};
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;
/** 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
};
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;
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;
RecentCallsAdapter mAdapter;
private QueryHandler mQueryHandler;
String mVoiceMailNumber;
static final class ContactInfo {
public long personId;
public String name;
public int type;
public String label;
public String number;
public String formattedNumber;
public static ContactInfo EMPTY = new ContactInfo();
}
public static final class RecentCallsListItemViews {
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;
}
/**
* Shared builder used by {@link #formatPhoneNumber(String)} to minimize
* allocations when formatting phone numbers.
*/
private static final SpannableStringBuilder sEditable = new SpannableStringBuilder();
/**
* Invalid formatting type constant for {@link #sFormattingType}.
*/
private static final int FORMATTING_TYPE_INVALID = -1;
/**
* Cached formatting type for current {@link Locale}, as provided by
* {@link PhoneNumberUtils#getFormatTypeForLocale(Locale)}.
*/
private static int sFormattingType = FORMATTING_TYPE_INVALID;
/** Adapter class to fill in data for the Call Log */
final class RecentCallsAdapter 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)) {
Uri telUri = Uri.fromParts("tel", number, null);
startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri));
}
}
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 RecentCallsAdapter() {
super(RecentCallsListActivity.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 {
RecentCallsListActivity.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 {
Cursor phonesCursor =
RecentCallsListActivity.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);
// 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;
}
phonesCursor.close();
}
}
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.recent_calls_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.recent_calls_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.recent_calls_list_group_item, parent, false);
findAndCacheViews(view);
return view;
}
@Override
protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
boolean expanded) {
final RecentCallsListItemViews views = (RecentCallsListItemViews) 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
RecentCallsListItemViews views = new RecentCallsListItemViews();
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 RecentCallsListItemViews views = (RecentCallsListItemViews) 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);
// 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);
}
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);
}
// 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);
CharSequence 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);
} else {
views.labelView.setVisibility(View.GONE);
}
} 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 (number.equals(mVoiceMailNumber)) {
number = getString(R.string.voicemail);
} else {
// Just a raw number, and no cache, so format it nicely
number = formatPhoneNumber(number);
}
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<RecentCallsListActivity> 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<RecentCallsListActivity>(
(RecentCallsListActivity) context);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
final RecentCallsListActivity activity = mActivity.get();
if (activity != null && !activity.isFinishing()) {
final RecentCallsListActivity.RecentCallsAdapter callsAdapter = activity.mAdapter;
callsAdapter.setLoading(false);
callsAdapter.changeCursor(cursor);
} else {
cursor.close();
}
}
}
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.recent_calls);
// Typing here goes to the dialer
setDefaultKeyMode(DEFAULT_KEYS_DIALER);
mAdapter = new RecentCallsAdapter();
getListView().setOnCreateContextMenuListener(this);
setListAdapter(mAdapter);
mVoiceMailNumber = ((TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE))
.getVoiceMailNumber();
mQueryHandler = new QueryHandler(this);
// Reset locale-based formatting cache
sFormattingType = FORMATTING_TYPE_INVALID;
}
@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);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// Clear notifications only when window gains focus. This activity won't
// immediately receive focus if the keyguard screen is above it.
if (hasFocus) {
try {
ITelephony iTelephony =
ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
if (iTelephony != null) {
iTelephony.cancelMissedCallsNotification();
} else {
Log.w(TAG, "Telephony service is null, can't call " +
"cancelMissedCallsNotification");
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
}
}
}
/**
* Format the given phone number using
* {@link PhoneNumberUtils#formatNumber(android.text.Editable, int)}. This
* helper method uses {@link #sEditable} and {@link #sFormattingType} to
* prevent allocations between multiple calls.
* <p>
* Because of the shared {@link #sEditable} builder, <b>this method is not
* thread safe</b>, and should only be called from the GUI thread.
* <p>
* If the given String object is null or empty, return an empty String.
*/
private String formatPhoneNumber(String number) {
if (TextUtils.isEmpty(number)) {
return "";
}
// Cache formatting type if not already present
if (sFormattingType == FORMATTING_TYPE_INVALID) {
sFormattingType = PhoneNumberUtils.getFormatTypeForLocale(Locale.getDefault());
}
sEditable.clear();
sEditable.append(number);
PhoneNumberUtils.formatNumber(sEditable, sFormattingType);
return sEditable.toString();
}
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;
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 (number.equals(mVoiceMailNumber)) {
number = getString(R.string.voicemail);
numberUri = Uri.parse("voicemail:x");
isVoicemail = 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) {
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)));
}
if (!contactInfoPresent && numberUri != null && !isVoicemail) {
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:
return new AlertDialog.Builder(this)
.setTitle(R.string.clearCallLogConfirmation_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.clearCallLogConfirmation)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
getContentResolver().delete(Calls.CONTENT_URI, null, null);
// 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();
}
})
.setCancelable(false)
.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) {
case KeyEvent.KEYCODE_CALL: {
long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
// Launch voice dialer
Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
}
return true;
}
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_CALL:
try {
ITelephony phone = ITelephony.Stub.asInterface(
ServiceManager.checkService("phone"));
if (phone != null && !phone.isIdle()) {
// Let the super class handle it
break;
}
} catch (RemoteException re) {
// Fall through and try to call the contact
}
callEntry(getListView().getSelectedItemPosition());
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 =
RecentCallsListActivity.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 = mAdapter.getCursor();
if (cursor != null && cursor.moveToPosition(position)) {
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;
}
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 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) {
if (globalSearch) {
super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
} else {
ContactsSearchManager.startSearch(this, initialQuery);
}
}
}