blob: bcffd1c172484de8e9e1092fe24c98099951f0ea [file] [log] [blame]
Daniel Lehmannbcd12272011-05-06 20:31:03 -07001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.calllog;
18
19import com.android.common.widget.GroupingListAdapter;
20import com.android.contacts.CallDetailActivity;
Flavio Lerda264d7782011-06-07 20:13:11 +010021import com.android.contacts.ContactPhotoManager;
Daniel Lehmannbcd12272011-05-06 20:31:03 -070022import com.android.contacts.ContactsUtils;
23import com.android.contacts.R;
24import com.android.internal.telephony.CallerInfo;
Flavio Lerda9ac11ce2011-06-01 13:38:23 +010025import com.google.common.annotations.VisibleForTesting;
Daniel Lehmannbcd12272011-05-06 20:31:03 -070026
27import android.app.ListFragment;
28import android.content.AsyncQueryHandler;
29import android.content.ContentUris;
30import android.content.ContentValues;
31import android.content.Context;
32import android.content.Intent;
33import android.database.CharArrayBuffer;
34import android.database.Cursor;
35import android.database.sqlite.SQLiteDatabaseCorruptException;
36import android.database.sqlite.SQLiteDiskIOException;
37import android.database.sqlite.SQLiteException;
38import android.database.sqlite.SQLiteFullException;
39import android.graphics.drawable.Drawable;
40import android.net.Uri;
41import android.os.Bundle;
42import android.os.Handler;
43import android.os.Looper;
44import android.os.Message;
45import android.provider.CallLog;
46import android.provider.CallLog.Calls;
Daniel Lehmannbcd12272011-05-06 20:31:03 -070047import android.provider.ContactsContract.CommonDataKinds.SipAddress;
48import android.provider.ContactsContract.Contacts;
49import android.provider.ContactsContract.Data;
50import android.provider.ContactsContract.Intents.Insert;
51import android.provider.ContactsContract.PhoneLookup;
52import android.telephony.PhoneNumberUtils;
53import android.telephony.TelephonyManager;
54import android.text.TextUtils;
Daniel Lehmannbcd12272011-05-06 20:31:03 -070055import android.util.Log;
56import android.view.ContextMenu;
57import android.view.ContextMenu.ContextMenuInfo;
58import android.view.LayoutInflater;
59import android.view.Menu;
60import android.view.MenuInflater;
61import android.view.MenuItem;
62import android.view.View;
63import android.view.ViewGroup;
64import android.view.ViewTreeObserver;
65import android.widget.AdapterView;
66import android.widget.ImageView;
67import android.widget.ListView;
68import android.widget.TextView;
69
70import java.lang.ref.WeakReference;
71import java.util.HashMap;
72import java.util.LinkedList;
73
74/**
75 * Displays a list of call log entries.
76 */
77public class CallLogFragment extends ListFragment
78 implements View.OnCreateContextMenuListener {
79 private static final String TAG = "CallLogFragment";
80
81 /** The query for the call log table */
82 private static final class CallLogQuery {
83 public static final String[] _PROJECTION = new String[] {
84 Calls._ID,
85 Calls.NUMBER,
86 Calls.DATE,
87 Calls.DURATION,
88 Calls.TYPE,
89 Calls.CACHED_NAME,
90 Calls.CACHED_NUMBER_TYPE,
91 Calls.CACHED_NUMBER_LABEL,
92 Calls.COUNTRY_ISO};
93
94 public static final int ID = 0;
95 public static final int NUMBER = 1;
96 public static final int DATE = 2;
97 public static final int DURATION = 3;
98 public static final int CALL_TYPE = 4;
99 public static final int CALLER_NAME = 5;
100 public static final int CALLER_NUMBERTYPE = 6;
101 public static final int CALLER_NUMBERLABEL = 7;
102 public static final int COUNTRY_ISO = 8;
103 }
104
105 /** The query to use for the phones table */
106 private static final class PhoneQuery {
107 public static final String[] _PROJECTION = new String[] {
108 PhoneLookup._ID,
109 PhoneLookup.DISPLAY_NAME,
110 PhoneLookup.TYPE,
111 PhoneLookup.LABEL,
112 PhoneLookup.NUMBER,
Flavio Lerda264d7782011-06-07 20:13:11 +0100113 PhoneLookup.NORMALIZED_NUMBER,
114 PhoneLookup.PHOTO_ID};
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700115
116 public static final int PERSON_ID = 0;
117 public static final int NAME = 1;
118 public static final int PHONE_TYPE = 2;
119 public static final int LABEL = 3;
120 public static final int MATCHED_NUMBER = 4;
121 public static final int NORMALIZED_NUMBER = 5;
Flavio Lerda264d7782011-06-07 20:13:11 +0100122 public static final int PHOTO_ID = 6;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700123 }
124
125 private static final class MenuItems {
126 public static final int DELETE = 1;
127 }
128
129 private static final class OptionsMenuItems {
130 public static final int DELETE_ALL = 1;
131 }
132
133 private static final int QUERY_TOKEN = 53;
134 private static final int UPDATE_TOKEN = 54;
135
136 private CallLogAdapter mAdapter;
137 private QueryHandler mQueryHandler;
138 private String mVoiceMailNumber;
139 private String mCurrentCountryIso;
140 private boolean mScrollToTop;
141
142 public static final class ContactInfo {
143 public long personId;
144 public String name;
145 public int type;
146 public String label;
147 public String number;
148 public String formattedNumber;
149 public String normalizedNumber;
Flavio Lerda264d7782011-06-07 20:13:11 +0100150 public long photoId;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700151
152 public static ContactInfo EMPTY = new ContactInfo();
153 }
154
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700155 public static final class CallerInfoQuery {
156 public String number;
157 public int position;
158 public String name;
159 public int numberType;
160 public String numberLabel;
Flavio Lerda264d7782011-06-07 20:13:11 +0100161 public long photoId;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700162 }
163
164 /** Adapter class to fill in data for the Call Log */
165 public final class CallLogAdapter extends GroupingListAdapter
166 implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
167 HashMap<String,ContactInfo> mContactInfo;
168 private final LinkedList<CallerInfoQuery> mRequests;
169 private volatile boolean mDone;
170 private boolean mLoading = true;
171 ViewTreeObserver.OnPreDrawListener mPreDrawListener;
172 private static final int REDRAW = 1;
173 private static final int START_THREAD = 2;
174 private boolean mFirst;
175 private Thread mCallerIdThread;
176
Flavio Lerdad2031e02011-06-06 10:07:19 +0100177 /** Instance of helper class for managing views. */
178 private final CallLogListItemHelper mCallLogViewsHelper;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700179
180 /**
181 * Reusable char array buffers.
182 */
183 private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
184 private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
Flavio Lerda264d7782011-06-07 20:13:11 +0100185 /** Helper to set up contact photos. */
186 private final ContactPhotoManager mContactPhotoManager;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700187
188 @Override
189 public void onClick(View view) {
190 String number = (String) view.getTag();
191 if (!TextUtils.isEmpty(number)) {
192 // Here, "number" can either be a PSTN phone number or a
193 // SIP address. So turn it into either a tel: URI or a
194 // sip: URI, as appropriate.
195 Uri callUri;
196 if (PhoneNumberUtils.isUriNumber(number)) {
197 callUri = Uri.fromParts("sip", number, null);
198 } else {
199 callUri = Uri.fromParts("tel", number, null);
200 }
201 startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri));
202 }
203 }
204
205 @Override
206 public boolean onPreDraw() {
207 if (mFirst) {
208 mHandler.sendEmptyMessageDelayed(START_THREAD, 1000);
209 mFirst = false;
210 }
211 return true;
212 }
213
214 private Handler mHandler = new Handler() {
215 @Override
216 public void handleMessage(Message msg) {
217 switch (msg.what) {
218 case REDRAW:
219 notifyDataSetChanged();
220 break;
221 case START_THREAD:
222 startRequestProcessing();
223 break;
224 }
225 }
226 };
227
228 public CallLogAdapter() {
229 super(getActivity());
230
231 mContactInfo = new HashMap<String,ContactInfo>();
232 mRequests = new LinkedList<CallerInfoQuery>();
233 mPreDrawListener = null;
234
Flavio Lerdad2031e02011-06-06 10:07:19 +0100235 Drawable drawableIncoming = getResources().getDrawable(
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700236 R.drawable.ic_call_log_list_incoming_call);
Flavio Lerdad2031e02011-06-06 10:07:19 +0100237 Drawable drawableOutgoing = getResources().getDrawable(
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700238 R.drawable.ic_call_log_list_outgoing_call);
Flavio Lerdad2031e02011-06-06 10:07:19 +0100239 Drawable drawableMissed = getResources().getDrawable(
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700240 R.drawable.ic_call_log_list_missed_call);
Flavio Lerda264d7782011-06-07 20:13:11 +0100241
242 mContactPhotoManager = ContactPhotoManager.getInstance(getActivity());
Flavio Lerdad2031e02011-06-06 10:07:19 +0100243 mCallLogViewsHelper = new CallLogListItemHelper(getResources(), mVoiceMailNumber,
244 drawableIncoming, drawableOutgoing, drawableMissed);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700245 }
246
247 /**
248 * Requery on background thread when {@link Cursor} changes.
249 */
250 @Override
251 protected void onContentChanged() {
252 // Start async requery
253 startQuery();
254 }
255
256 void setLoading(boolean loading) {
257 mLoading = loading;
258 }
259
260 @Override
261 public boolean isEmpty() {
262 if (mLoading) {
263 // We don't want the empty state to show when loading.
264 return false;
265 } else {
266 return super.isEmpty();
267 }
268 }
269
270 public ContactInfo getContactInfo(String number) {
271 return mContactInfo.get(number);
272 }
273
274 public void startRequestProcessing() {
275 mDone = false;
276 mCallerIdThread = new Thread(this);
277 mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
278 mCallerIdThread.start();
279 }
280
Flavio Lerdae3ea5f72011-06-06 13:04:06 +0100281 /**
282 * Stops the background thread that processes updates and cancels any pending requests to
283 * start it.
284 * <p>
285 * Should be called from the main thread to prevent a race condition between the request to
286 * start the thread being processed and stopping the thread.
287 */
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700288 public void stopRequestProcessing() {
Flavio Lerdae3ea5f72011-06-06 13:04:06 +0100289 // Remove any pending requests to start the processing thread.
290 mHandler.removeMessages(START_THREAD);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700291 mDone = true;
292 if (mCallerIdThread != null) mCallerIdThread.interrupt();
293 }
294
295 public void clearCache() {
296 synchronized (mContactInfo) {
297 mContactInfo.clear();
298 }
299 }
300
301 private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) {
302 // Check if they are different. If not, don't update.
303 if (TextUtils.equals(ciq.name, ci.name)
304 && TextUtils.equals(ciq.numberLabel, ci.label)
Flavio Lerda264d7782011-06-07 20:13:11 +0100305 && ciq.numberType == ci.type
306 && ciq.photoId == ci.photoId) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700307 return;
308 }
309 ContentValues values = new ContentValues(3);
310 values.put(Calls.CACHED_NAME, ci.name);
311 values.put(Calls.CACHED_NUMBER_TYPE, ci.type);
312 values.put(Calls.CACHED_NUMBER_LABEL, ci.label);
313
314 try {
315 getActivity().getContentResolver().update(Calls.CONTENT_URI, values,
316 Calls.NUMBER + "='" + ciq.number + "'", null);
317 } catch (SQLiteDiskIOException e) {
318 Log.w(TAG, "Exception while updating call info", e);
319 } catch (SQLiteFullException e) {
320 Log.w(TAG, "Exception while updating call info", e);
321 } catch (SQLiteDatabaseCorruptException e) {
322 Log.w(TAG, "Exception while updating call info", e);
323 }
324 }
325
326 private void enqueueRequest(String number, int position,
Flavio Lerda264d7782011-06-07 20:13:11 +0100327 String name, int numberType, String numberLabel, long photoId) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700328 CallerInfoQuery ciq = new CallerInfoQuery();
329 ciq.number = number;
330 ciq.position = position;
331 ciq.name = name;
332 ciq.numberType = numberType;
333 ciq.numberLabel = numberLabel;
Flavio Lerda264d7782011-06-07 20:13:11 +0100334 ciq.photoId = photoId;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700335 synchronized (mRequests) {
336 mRequests.add(ciq);
337 mRequests.notifyAll();
338 }
339 }
340
341 private boolean queryContactInfo(CallerInfoQuery ciq) {
342 // First check if there was a prior request for the same number
343 // that was already satisfied
344 ContactInfo info = mContactInfo.get(ciq.number);
345 boolean needNotify = false;
346 if (info != null && info != ContactInfo.EMPTY) {
347 return true;
348 } else {
349 // Ok, do a fresh Contacts lookup for ciq.number.
350 boolean infoUpdated = false;
351
352 if (PhoneNumberUtils.isUriNumber(ciq.number)) {
353 // This "number" is really a SIP address.
354
355 // TODO: This code is duplicated from the
356 // CallerInfoAsyncQuery class. To avoid that, could the
357 // code here just use CallerInfoAsyncQuery, rather than
358 // manually running ContentResolver.query() itself?
359
360 // We look up SIP addresses directly in the Data table:
361 Uri contactRef = Data.CONTENT_URI;
362
363 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
364 //
365 // Also note we use "upper(data1)" in the WHERE clause, and
366 // uppercase the incoming SIP address, in order to do a
367 // case-insensitive match.
368 //
369 // TODO: May also need to normalize by adding "sip:" as a
370 // prefix, if we start storing SIP addresses that way in the
371 // database.
372 String selection = "upper(" + Data.DATA1 + ")=?"
373 + " AND "
374 + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
375 String[] selectionArgs = new String[] { ciq.number.toUpperCase() };
376
377 Cursor dataTableCursor =
378 getActivity().getContentResolver().query(
379 contactRef,
380 null, // projection
381 selection, // selection
382 selectionArgs, // selectionArgs
383 null); // sortOrder
384
385 if (dataTableCursor != null) {
386 if (dataTableCursor.moveToFirst()) {
387 info = new ContactInfo();
388
389 // TODO: we could slightly speed this up using an
390 // explicit projection (and thus not have to do
391 // those getColumnIndex() calls) but the benefit is
392 // very minimal.
393
394 // Note the Data.CONTACT_ID column here is
395 // equivalent to the PERSON_ID_COLUMN_INDEX column
396 // we use with "phonesCursor" below.
397 info.personId = dataTableCursor.getLong(
398 dataTableCursor.getColumnIndex(Data.CONTACT_ID));
399 info.name = dataTableCursor.getString(
400 dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
401 // "type" and "label" are currently unused for SIP addresses
402 info.type = SipAddress.TYPE_OTHER;
403 info.label = null;
404
405 // And "number" is the SIP address.
406 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
407 info.number = dataTableCursor.getString(
408 dataTableCursor.getColumnIndex(Data.DATA1));
409 info.normalizedNumber = null; // meaningless for SIP addresses
Flavio Lerda264d7782011-06-07 20:13:11 +0100410 info.photoId = dataTableCursor.getLong(
411 dataTableCursor.getColumnIndex(Data.PHOTO_ID));
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700412
413 infoUpdated = true;
414 }
415 dataTableCursor.close();
416 }
417 } else {
418 // "number" is a regular phone number, so use the
419 // PhoneLookup table:
420 Cursor phonesCursor =
421 getActivity().getContentResolver().query(
422 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
423 Uri.encode(ciq.number)),
424 PhoneQuery._PROJECTION, null, null, null);
425 if (phonesCursor != null) {
426 if (phonesCursor.moveToFirst()) {
427 info = new ContactInfo();
428 info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
429 info.name = phonesCursor.getString(PhoneQuery.NAME);
430 info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
431 info.label = phonesCursor.getString(PhoneQuery.LABEL);
432 info.number = phonesCursor
433 .getString(PhoneQuery.MATCHED_NUMBER);
434 info.normalizedNumber = phonesCursor
435 .getString(PhoneQuery.NORMALIZED_NUMBER);
Flavio Lerda264d7782011-06-07 20:13:11 +0100436 info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700437
438 infoUpdated = true;
439 }
440 phonesCursor.close();
441 }
442 }
443
444 if (infoUpdated) {
445 // New incoming phone number invalidates our formatted
446 // cache. Any cache fills happen only on the GUI thread.
447 info.formattedNumber = null;
448
449 mContactInfo.put(ciq.number, info);
450
451 // Inform list to update this item, if in view
452 needNotify = true;
453 }
454 }
455 if (info != null) {
456 updateCallLog(ciq, info);
457 }
458 return needNotify;
459 }
460
461 /*
462 * Handles requests for contact name and number type
463 * @see java.lang.Runnable#run()
464 */
465 @Override
466 public void run() {
467 boolean needNotify = false;
468 while (!mDone) {
469 CallerInfoQuery ciq = null;
470 synchronized (mRequests) {
471 if (!mRequests.isEmpty()) {
472 ciq = mRequests.removeFirst();
473 } else {
474 if (needNotify) {
475 needNotify = false;
476 mHandler.sendEmptyMessage(REDRAW);
477 }
478 try {
479 mRequests.wait(1000);
480 } catch (InterruptedException ie) {
481 // Ignore and continue processing requests
Flavio Lerda63223b42011-06-03 11:52:51 +0100482 Thread.currentThread().interrupt();
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700483 }
484 }
485 }
Flavio Lerda63223b42011-06-03 11:52:51 +0100486 if (!mDone && ciq != null && queryContactInfo(ciq)) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700487 needNotify = true;
488 }
489 }
490 }
491
492 @Override
493 protected void addGroups(Cursor cursor) {
494
495 int count = cursor.getCount();
496 if (count == 0) {
497 return;
498 }
499
500 int groupItemCount = 1;
501
502 CharArrayBuffer currentValue = mBuffer1;
503 CharArrayBuffer value = mBuffer2;
504 cursor.moveToFirst();
505 cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue);
506 int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
507 for (int i = 1; i < count; i++) {
508 cursor.moveToNext();
509 cursor.copyStringToBuffer(CallLogQuery.NUMBER, value);
510 boolean sameNumber = equalPhoneNumbers(value, currentValue);
511
512 // Group adjacent calls with the same number. Make an exception
513 // for the latest item if it was a missed call. We don't want
514 // a missed call to be hidden inside a group.
515 if (sameNumber && currentCallType != Calls.MISSED_TYPE) {
516 groupItemCount++;
517 } else {
518 if (groupItemCount > 1) {
519 addGroup(i - groupItemCount, groupItemCount, false);
520 }
521
522 groupItemCount = 1;
523
524 // Swap buffers
525 CharArrayBuffer temp = currentValue;
526 currentValue = value;
527 value = temp;
528
529 // If we have just examined a row following a missed call, make
530 // sure that it is grouped with subsequent calls from the same number
531 // even if it was also missed.
532 if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
533 currentCallType = 0; // "not a missed call"
534 } else {
535 currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
536 }
537 }
538 }
539 if (groupItemCount > 1) {
540 addGroup(count - groupItemCount, groupItemCount, false);
541 }
542 }
543
544 protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
545
546 // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
547 // string allocation
548 return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied),
549 new String(buffer2.data, 0, buffer2.sizeCopied));
550 }
551
552
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100553 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700554 @Override
555 public View newStandAloneView(Context context, ViewGroup parent) {
556 LayoutInflater inflater =
557 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
558 View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
559 findAndCacheViews(view);
560 return view;
561 }
562
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100563 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700564 @Override
565 public void bindStandAloneView(View view, Context context, Cursor cursor) {
566 bindView(context, view, cursor);
567 }
568
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100569 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700570 @Override
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100571 public View newChildView(Context context, ViewGroup parent) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700572 LayoutInflater inflater =
573 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
574 View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false);
575 findAndCacheViews(view);
576 return view;
577 }
578
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100579 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700580 @Override
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100581 public void bindChildView(View view, Context context, Cursor cursor) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700582 bindView(context, view, cursor);
583 }
584
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100585 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700586 @Override
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100587 public View newGroupView(Context context, ViewGroup parent) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700588 LayoutInflater inflater =
589 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
590 View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false);
591 findAndCacheViews(view);
592 return view;
593 }
594
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100595 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700596 @Override
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100597 public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700598 boolean expanded) {
599 final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
600 int groupIndicator = expanded
601 ? com.android.internal.R.drawable.expander_ic_maximized
602 : com.android.internal.R.drawable.expander_ic_minimized;
603 views.groupIndicator.setImageResource(groupIndicator);
604 views.groupSize.setText("(" + groupSize + ")");
605 bindView(context, view, cursor);
606 }
607
608 private void findAndCacheViews(View view) {
609
610 // Get the views to bind to
611 CallLogListItemViews views = new CallLogListItemViews();
612 views.line1View = (TextView) view.findViewById(R.id.line1);
613 views.labelView = (TextView) view.findViewById(R.id.label);
614 views.numberView = (TextView) view.findViewById(R.id.number);
615 views.dateView = (TextView) view.findViewById(R.id.date);
616 views.iconView = (ImageView) view.findViewById(R.id.call_type_icon);
617 views.callView = view.findViewById(R.id.call_icon);
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100618 if (views.callView != null) {
619 views.callView.setOnClickListener(this);
620 }
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700621 views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
622 views.groupSize = (TextView) view.findViewById(R.id.groupSize);
Flavio Lerda264d7782011-06-07 20:13:11 +0100623 views.photoView = (ImageView) view.findViewById(R.id.contact_photo);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700624 view.setTag(views);
625 }
626
627 public void bindView(Context context, View view, Cursor c) {
628 final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
629
630 String number = c.getString(CallLogQuery.NUMBER);
631 String formattedNumber = null;
632 String callerName = c.getString(CallLogQuery.CALLER_NAME);
633 int callerNumberType = c.getInt(CallLogQuery.CALLER_NUMBERTYPE);
634 String callerNumberLabel = c.getString(CallLogQuery.CALLER_NUMBERLABEL);
635 String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
636 // Store away the number so we can call it directly if you click on the call icon
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100637 if (views.callView != null) {
638 views.callView.setTag(number);
639 }
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700640
641 // Lookup contacts with this number
642 ContactInfo info = mContactInfo.get(number);
643 if (info == null) {
644 // Mark it as empty and queue up a request to find the name
645 // The db request should happen on a non-UI thread
646 info = ContactInfo.EMPTY;
647 mContactInfo.put(number, info);
648 enqueueRequest(number, c.getPosition(),
Flavio Lerda264d7782011-06-07 20:13:11 +0100649 callerName, callerNumberType, callerNumberLabel, 0L);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700650 } else if (info != ContactInfo.EMPTY) { // Has been queried
651 // Check if any data is different from the data cached in the
652 // calls db. If so, queue the request so that we can update
653 // the calls db.
654 if (!TextUtils.equals(info.name, callerName)
655 || info.type != callerNumberType
656 || !TextUtils.equals(info.label, callerNumberLabel)) {
657 // Something is amiss, so sync up.
658 enqueueRequest(number, c.getPosition(),
Flavio Lerda264d7782011-06-07 20:13:11 +0100659 callerName, callerNumberType, callerNumberLabel, info.photoId);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700660 }
661
662 // Format and cache phone number for found contact
663 if (info.formattedNumber == null) {
664 info.formattedNumber =
665 formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
666 }
667 formattedNumber = info.formattedNumber;
668 }
669
670 String name = info.name;
671 int ntype = info.type;
672 String label = info.label;
Flavio Lerda264d7782011-06-07 20:13:11 +0100673 long photoId = info.photoId;
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700674 // If there's no name cached in our hashmap, but there's one in the
675 // calls db, use the one in the calls db. Otherwise the name in our
676 // hashmap is more recent, so it has precedence.
677 if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) {
678 name = callerName;
679 ntype = callerNumberType;
680 label = callerNumberLabel;
681
682 // Format the cached call_log phone number
683 formattedNumber = formatPhoneNumber(number, null, countryIso);
684 }
Flavio Lerdad2031e02011-06-06 10:07:19 +0100685
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700686 // Assumes the call back feature is on most of the
687 // time. For private and unknown numbers: hide it.
Flavio Lerda9ac11ce2011-06-01 13:38:23 +0100688 if (views.callView != null) {
689 views.callView.setVisibility(View.VISIBLE);
690 }
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700691
692 if (!TextUtils.isEmpty(name)) {
Flavio Lerdad2031e02011-06-06 10:07:19 +0100693 mCallLogViewsHelper.setContactNameLabelAndNumber(views, name, number, ntype, label,
694 formattedNumber);
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700695 } else {
Flavio Lerdad2031e02011-06-06 10:07:19 +0100696 // TODO: Do we need to format the number again? Is formattedNumber already storing
697 // this value?
698 mCallLogViewsHelper.setContactNumberOnly(views, number,
699 formatPhoneNumber(number, null, countryIso));
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700700 }
Flavio Lerdad2031e02011-06-06 10:07:19 +0100701 mCallLogViewsHelper.setDate(views, c.getLong(CallLogQuery.DATE),
702 System.currentTimeMillis());
703 mCallLogViewsHelper.setCallType(views, c.getInt(CallLogQuery.CALL_TYPE));
Flavio Lerda264d7782011-06-07 20:13:11 +0100704 if (views.photoView != null) {
705 mContactPhotoManager.loadPhoto(views.photoView, photoId);
706 }
707
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700708
709 // Listen for the first draw
710 if (mPreDrawListener == null) {
711 mFirst = true;
712 mPreDrawListener = this;
713 view.getViewTreeObserver().addOnPreDrawListener(this);
714 }
715 }
716 }
717
718 private static final class QueryHandler extends AsyncQueryHandler {
719 private final WeakReference<CallLogFragment> mFragment;
720
721 /**
722 * Simple handler that wraps background calls to catch
723 * {@link SQLiteException}, such as when the disk is full.
724 */
725 protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
726 public CatchingWorkerHandler(Looper looper) {
727 super(looper);
728 }
729
730 @Override
731 public void handleMessage(Message msg) {
732 try {
733 // Perform same query while catching any exceptions
734 super.handleMessage(msg);
735 } catch (SQLiteDiskIOException e) {
736 Log.w(TAG, "Exception on background worker thread", e);
737 } catch (SQLiteFullException e) {
738 Log.w(TAG, "Exception on background worker thread", e);
739 } catch (SQLiteDatabaseCorruptException e) {
740 Log.w(TAG, "Exception on background worker thread", e);
741 }
742 }
743 }
744
745 @Override
746 protected Handler createHandler(Looper looper) {
747 // Provide our special handler that catches exceptions
748 return new CatchingWorkerHandler(looper);
749 }
750
751 public QueryHandler(CallLogFragment fragment) {
752 super(fragment.getActivity().getContentResolver());
753 mFragment = new WeakReference<CallLogFragment>(fragment);
754 }
755
756 @Override
757 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
758 final CallLogFragment fragment = mFragment.get();
Daniel Lehmannfa3c78d2011-05-16 20:22:13 -0700759 if (fragment != null && fragment.getActivity() != null &&
760 !fragment.getActivity().isFinishing()) {
Daniel Lehmannbcd12272011-05-06 20:31:03 -0700761 final CallLogFragment.CallLogAdapter callsAdapter = fragment.mAdapter;
762 callsAdapter.setLoading(false);
763 callsAdapter.changeCursor(cursor);
764 if (fragment.mScrollToTop) {
765 final ListView listView = fragment.getListView();
766 if (listView.getFirstVisiblePosition() > 5) {
767 listView.setSelection(5);
768 }
769 listView.smoothScrollToPosition(0);
770 fragment.mScrollToTop = false;
771 }
772 } else {
773 cursor.close();
774 }
775 }
776 }
777
778 @Override
779 public void onCreate(Bundle state) {
780 super.onCreate(state);
781
782 mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService(
783 Context.TELEPHONY_SERVICE)).getVoiceMailNumber();
784 mQueryHandler = new QueryHandler(this);
785
786 mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
787
788 setHasOptionsMenu(true);
789 }
790
791 @Override
792 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
793 return inflater.inflate(R.layout.call_log_fragment, container, false);
794 }
795
796 @Override
797 public void onViewCreated(View view, Bundle savedInstanceState) {
798 super.onViewCreated(view, savedInstanceState);
799 getListView().setOnCreateContextMenuListener(this);
800 mAdapter = new CallLogAdapter();
801 setListAdapter(mAdapter);
802 }
803
804 @Override
805 public void onStart() {
806 mScrollToTop = true;
807 super.onStart();
808 }
809
810 @Override
811 public void onResume() {
812 // The adapter caches looked up numbers, clear it so they will get
813 // looked up again.
814 if (mAdapter != null) {
815 mAdapter.clearCache();
816 }
817
818 startQuery();
819 resetNewCallsFlag();
820
821 super.onResume();
822
823 mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
824 }
825
826 @Override
827 public void onPause() {
828 super.onPause();
829
830 // Kill the requests thread
831 mAdapter.stopRequestProcessing();
832 }
833
834 @Override
835 public void onDestroy() {
836 super.onDestroy();
837 mAdapter.stopRequestProcessing();
838 mAdapter.changeCursor(null);
839 }
840
841 /**
842 * Format the given phone number
843 *
844 * @param number the number to be formatted.
845 * @param normalizedNumber the normalized number of the given number.
846 * @param countryIso the ISO 3166-1 two letters country code, the country's
847 * convention will be used to format the number if the normalized
848 * phone is null.
849 *
850 * @return the formatted number, or the given number if it was formatted.
851 */
852 private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
853 if (TextUtils.isEmpty(number)) {
854 return "";
855 }
856 // If "number" is really a SIP address, don't try to do any formatting at all.
857 if (PhoneNumberUtils.isUriNumber(number)) {
858 return number;
859 }
860 if (TextUtils.isEmpty(countryIso)) {
861 countryIso = mCurrentCountryIso;
862 }
863 return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
864 }
865
866 private void resetNewCallsFlag() {
867 // Mark all "new" missed calls as not new anymore
868 StringBuilder where = new StringBuilder("type=");
869 where.append(Calls.MISSED_TYPE);
870 where.append(" AND new=1");
871
872 ContentValues values = new ContentValues(1);
873 values.put(Calls.NEW, "0");
874 mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI,
875 values, where.toString(), null);
876 }
877
878 private void startQuery() {
879 mAdapter.setLoading(true);
880
881 // Cancel any pending queries
882 mQueryHandler.cancelOperation(QUERY_TOKEN);
883 mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI,
884 CallLogQuery._PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
885 }
886
887 @Override
888 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
889 super.onCreateOptionsMenu(menu, inflater);
890 menu.add(0, OptionsMenuItems.DELETE_ALL, 0, R.string.recentCalls_deleteAll).setIcon(
891 android.R.drawable.ic_menu_close_clear_cancel);
892 }
893
894 @Override
895 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
896 AdapterView.AdapterContextMenuInfo menuInfo;
897 try {
898 menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn;
899 } catch (ClassCastException e) {
900 Log.e(TAG, "bad menuInfoIn", e);
901 return;
902 }
903
904 Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position);
905
906 String number = cursor.getString(CallLogQuery.NUMBER);
907 Uri numberUri = null;
908 boolean isVoicemail = false;
909 boolean isSipNumber = false;
910 if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
911 number = getString(R.string.unknown);
912 } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
913 number = getString(R.string.private_num);
914 } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
915 number = getString(R.string.payphone);
916 } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) {
917 number = getString(R.string.voicemail);
918 numberUri = Uri.parse("voicemail:x");
919 isVoicemail = true;
920 } else if (PhoneNumberUtils.isUriNumber(number)) {
921 numberUri = Uri.fromParts("sip", number, null);
922 isSipNumber = true;
923 } else {
924 numberUri = Uri.fromParts("tel", number, null);
925 }
926
927 ContactInfo info = mAdapter.getContactInfo(number);
928 boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY);
929 if (contactInfoPresent) {
930 menu.setHeaderTitle(info.name);
931 } else {
932 menu.setHeaderTitle(number);
933 }
934
935 if (numberUri != null) {
936 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri);
937 menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number))
938 .setIntent(intent);
939 }
940
941 if (contactInfoPresent) {
942 menu.add(0, 0, 0, R.string.menu_viewContact)
943 .setIntent(new Intent(Intent.ACTION_VIEW,
944 ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId)));
945 }
946
947 if (numberUri != null && !isVoicemail && !isSipNumber) {
948 menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall)
949 .setIntent(new Intent(Intent.ACTION_DIAL, numberUri));
950 menu.add(0, 0, 0, R.string.menu_sendTextMessage)
951 .setIntent(new Intent(Intent.ACTION_SENDTO,
952 Uri.fromParts("sms", number, null)));
953 }
954
955 // "Add to contacts" item, if this entry isn't already associated with a contact
956 if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) {
957 // TODO: This item is currently disabled for SIP addresses, because
958 // the Insert.PHONE extra only works correctly for PSTN numbers.
959 //
960 // To fix this for SIP addresses, we need to:
961 // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
962 // the current number is a SIP address
963 // - update the contacts UI code to handle Insert.SIP_ADDRESS by
964 // updating the SipAddress field
965 // and then we can remove the "!isSipNumber" check above.
966
967 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
968 intent.setType(Contacts.CONTENT_ITEM_TYPE);
969 intent.putExtra(Insert.PHONE, number);
970 menu.add(0, 0, 0, R.string.recentCalls_addToContact)
971 .setIntent(intent);
972 }
973 menu.add(0, MenuItems.DELETE, 0, R.string.recentCalls_removeFromRecentList);
974 }
975
976 @Override
977 public boolean onOptionsItemSelected(MenuItem item) {
978 switch (item.getItemId()) {
979 case OptionsMenuItems.DELETE_ALL: {
980 ClearCallLogDialog.show(getFragmentManager());
981 return true;
982 }
983 }
984 return super.onOptionsItemSelected(item);
985 }
986
987 @Override
988 public boolean onContextItemSelected(MenuItem item) {
989 // Convert the menu info to the proper type
990 AdapterView.AdapterContextMenuInfo menuInfo;
991 try {
992 menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
993 } catch (ClassCastException e) {
994 Log.e(TAG, "bad menuInfoIn", e);
995 return false;
996 }
997
998 switch (item.getItemId()) {
999 case MenuItems.DELETE: {
1000 Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position);
1001 int groupSize = 1;
1002 if (mAdapter.isGroupHeader(menuInfo.position)) {
1003 groupSize = mAdapter.getGroupSize(menuInfo.position);
1004 }
1005
1006 StringBuilder sb = new StringBuilder();
1007 for (int i = 0; i < groupSize; i++) {
1008 if (i != 0) {
1009 sb.append(",");
1010 cursor.moveToNext();
1011 }
1012 long id = cursor.getLong(CallLogQuery.ID);
1013 sb.append(id);
1014 }
1015
1016 getActivity().getContentResolver().delete(Calls.CONTENT_URI,
1017 Calls._ID + " IN (" + sb + ")", null);
1018 }
1019 }
1020 return super.onContextItemSelected(item);
1021 }
1022
1023 /*
1024 * Get the number from the Contacts, if available, since sometimes
1025 * the number provided by caller id may not be formatted properly
1026 * depending on the carrier (roaming) in use at the time of the
1027 * incoming call.
1028 * Logic : If the caller-id number starts with a "+", use it
1029 * Else if the number in the contacts starts with a "+", use that one
1030 * Else if the number in the contacts is longer, use that one
1031 */
1032 private String getBetterNumberFromContacts(String number) {
1033 String matchingNumber = null;
1034 // Look in the cache first. If it's not found then query the Phones db
1035 ContactInfo ci = mAdapter.mContactInfo.get(number);
1036 if (ci != null && ci != ContactInfo.EMPTY) {
1037 matchingNumber = ci.number;
1038 } else {
1039 try {
1040 Cursor phonesCursor = getActivity().getContentResolver().query(
1041 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
1042 PhoneQuery._PROJECTION, null, null, null);
1043 if (phonesCursor != null) {
1044 if (phonesCursor.moveToFirst()) {
1045 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
1046 }
1047 phonesCursor.close();
1048 }
1049 } catch (Exception e) {
1050 // Use the number from the call log
1051 }
1052 }
1053 if (!TextUtils.isEmpty(matchingNumber) &&
1054 (matchingNumber.startsWith("+")
1055 || matchingNumber.length() > number.length())) {
1056 number = matchingNumber;
1057 }
1058 return number;
1059 }
1060
1061 public void callSelectedEntry() {
1062 int position = getListView().getSelectedItemPosition();
1063 if (position < 0) {
1064 // In touch mode you may often not have something selected, so
1065 // just call the first entry to make sure that [send] [send] calls the
1066 // most recent entry.
1067 position = 0;
1068 }
1069 final Cursor cursor = (Cursor)mAdapter.getItem(position);
1070 if (cursor != null) {
1071 String number = cursor.getString(CallLogQuery.NUMBER);
1072 if (TextUtils.isEmpty(number)
1073 || number.equals(CallerInfo.UNKNOWN_NUMBER)
1074 || number.equals(CallerInfo.PRIVATE_NUMBER)
1075 || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
1076 // This number can't be called, do nothing
1077 return;
1078 }
1079 Intent intent;
1080 // If "number" is really a SIP address, construct a sip: URI.
1081 if (PhoneNumberUtils.isUriNumber(number)) {
1082 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
1083 Uri.fromParts("sip", number, null));
1084 } else {
1085 // We're calling a regular PSTN phone number.
1086 // Construct a tel: URI, but do some other possible cleanup first.
1087 int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
1088 if (!number.startsWith("+") &&
1089 (callType == Calls.INCOMING_TYPE
1090 || callType == Calls.MISSED_TYPE)) {
1091 // If the caller-id matches a contact with a better qualified number, use it
1092 number = getBetterNumberFromContacts(number);
1093 }
1094 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
1095 Uri.fromParts("tel", number, null));
1096 }
1097 intent.setFlags(
1098 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
1099 startActivity(intent);
1100 }
1101 }
1102
1103 @Override
1104 public void onListItemClick(ListView l, View v, int position, long id) {
1105 if (mAdapter.isGroupHeader(position)) {
1106 mAdapter.toggleGroup(position);
1107 } else {
1108 Intent intent = new Intent(getActivity(), CallDetailActivity.class);
1109 intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
1110 startActivity(intent);
1111 }
1112 }
1113
Flavio Lerda37a26842011-06-27 11:36:52 +01001114 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -07001115 public CallLogAdapter getAdapter() {
1116 return mAdapter;
1117 }
1118
Flavio Lerda37a26842011-06-27 11:36:52 +01001119 @VisibleForTesting
Daniel Lehmannbcd12272011-05-06 20:31:03 -07001120 public String getVoiceMailNumber() {
1121 return mVoiceMailNumber;
1122 }
1123}