blob: bcffd1c172484de8e9e1092fe24c98099951f0ea [file] [log] [blame]
/*
* 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.ContactPhotoManager;
import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
import com.android.internal.telephony.CallerInfo;
import com.google.common.annotations.VisibleForTesting;
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.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.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,
PhoneLookup.PHOTO_ID};
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;
public static final int PHOTO_ID = 6;
}
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 long photoId;
public static ContactInfo EMPTY = new ContactInfo();
}
public static final class CallerInfoQuery {
public String number;
public int position;
public String name;
public int numberType;
public String numberLabel;
public long photoId;
}
/** 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;
/** Instance of helper class for managing views. */
private final CallLogListItemHelper mCallLogViewsHelper;
/**
* Reusable char array buffers.
*/
private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
/** Helper to set up contact photos. */
private final ContactPhotoManager mContactPhotoManager;
@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;
Drawable drawableIncoming = getResources().getDrawable(
R.drawable.ic_call_log_list_incoming_call);
Drawable drawableOutgoing = getResources().getDrawable(
R.drawable.ic_call_log_list_outgoing_call);
Drawable drawableMissed = getResources().getDrawable(
R.drawable.ic_call_log_list_missed_call);
mContactPhotoManager = ContactPhotoManager.getInstance(getActivity());
mCallLogViewsHelper = new CallLogListItemHelper(getResources(), mVoiceMailNumber,
drawableIncoming, drawableOutgoing, drawableMissed);
}
/**
* 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();
}
/**
* Stops the background thread that processes updates and cancels any pending requests to
* start it.
* <p>
* Should be called from the main thread to prevent a race condition between the request to
* start the thread being processed and stopping the thread.
*/
public void stopRequestProcessing() {
// Remove any pending requests to start the processing thread.
mHandler.removeMessages(START_THREAD);
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
&& ciq.photoId == ci.photoId) {
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, long photoId) {
CallerInfoQuery ciq = new CallerInfoQuery();
ciq.number = number;
ciq.position = position;
ciq.name = name;
ciq.numberType = numberType;
ciq.numberLabel = numberLabel;
ciq.photoId = photoId;
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
info.photoId = dataTableCursor.getLong(
dataTableCursor.getColumnIndex(Data.PHOTO_ID));
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);
info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID);
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
Thread.currentThread().interrupt();
}
}
}
if (!mDone && 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));
}
@VisibleForTesting
@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;
}
@VisibleForTesting
@Override
public void bindStandAloneView(View view, Context context, Cursor cursor) {
bindView(context, view, cursor);
}
@VisibleForTesting
@Override
public 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;
}
@VisibleForTesting
@Override
public void bindChildView(View view, Context context, Cursor cursor) {
bindView(context, view, cursor);
}
@VisibleForTesting
@Override
public 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;
}
@VisibleForTesting
@Override
public 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);
if (views.callView != null) {
views.callView.setOnClickListener(this);
}
views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
views.groupSize = (TextView) view.findViewById(R.id.groupSize);
views.photoView = (ImageView) view.findViewById(R.id.contact_photo);
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
if (views.callView != null) {
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, 0L);
} 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, info.photoId);
}
// 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;
long photoId = info.photoId;
// 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);
}
// Assumes the call back feature is on most of the
// time. For private and unknown numbers: hide it.
if (views.callView != null) {
views.callView.setVisibility(View.VISIBLE);
}
if (!TextUtils.isEmpty(name)) {
mCallLogViewsHelper.setContactNameLabelAndNumber(views, name, number, ntype, label,
formattedNumber);
} else {
// TODO: Do we need to format the number again? Is formattedNumber already storing
// this value?
mCallLogViewsHelper.setContactNumberOnly(views, number,
formatPhoneNumber(number, null, countryIso));
}
mCallLogViewsHelper.setDate(views, c.getLong(CallLogQuery.DATE),
System.currentTimeMillis());
mCallLogViewsHelper.setCallType(views, c.getInt(CallLogQuery.CALL_TYPE));
if (views.photoView != null) {
mContactPhotoManager.loadPhoto(views.photoView, photoId);
}
// 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();
if (fragment != null && fragment.getActivity() != 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);
}
}
@VisibleForTesting
public CallLogAdapter getAdapter() {
return mAdapter;
}
@VisibleForTesting
public String getVoiceMailNumber() {
return mVoiceMailNumber;
}
}