summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/calllog
diff options
context:
space:
mode:
authorChiao Cheng <chiaocheng@google.com>2012-08-17 16:59:12 -0700
committerChiao Cheng <chiaocheng@google.com>2012-08-21 13:31:19 -0700
commit94b10b530c0fc297e2974e57e094c500d3ee6003 (patch)
treeb74d663c2663b5db2f6da888081648ce054480f5 /src/com/android/dialer/calllog
parentdab5cd8890c0d0ca9001a13c2197114a4002338a (diff)
Initial move of dialer features from contacts app.
Bug: 6993891 Change-Id: I758ce359ca7e87a1d184303822979318be171921
Diffstat (limited to 'src/com/android/dialer/calllog')
-rw-r--r--src/com/android/dialer/calllog/CallDetailHistoryAdapter.java175
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapter.java802
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java549
-rw-r--r--src/com/android/dialer/calllog/CallLogGroupBuilder.java159
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemHelper.java109
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemView.java46
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemViews.java83
-rw-r--r--src/com/android/dialer/calllog/CallLogNotificationsService.java82
-rw-r--r--src/com/android/dialer/calllog/CallLogQuery.java103
-rw-r--r--src/com/android/dialer/calllog/CallLogQueryHandler.java364
-rw-r--r--src/com/android/dialer/calllog/CallLogReceiver.java50
-rw-r--r--src/com/android/dialer/calllog/CallTypeHelper.java92
-rw-r--r--src/com/android/dialer/calllog/CallTypeIconsView.java126
-rw-r--r--src/com/android/dialer/calllog/ClearCallLogDialog.java78
-rw-r--r--src/com/android/dialer/calllog/ContactInfo.java71
-rw-r--r--src/com/android/dialer/calllog/ContactInfoHelper.java215
-rw-r--r--src/com/android/dialer/calllog/DefaultVoicemailNotifier.java340
-rw-r--r--src/com/android/dialer/calllog/ExtendedCursor.java154
-rw-r--r--src/com/android/dialer/calllog/IntentProvider.java102
-rw-r--r--src/com/android/dialer/calllog/PhoneNumberHelper.java93
-rw-r--r--src/com/android/dialer/calllog/PhoneQuery.java45
-rw-r--r--src/com/android/dialer/calllog/VoicemailNotifier.java38
22 files changed, 3876 insertions, 0 deletions
diff --git a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
new file mode 100644
index 000000000..38dc72722
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
@@ -0,0 +1,175 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.android.dialer.PhoneCallDetails;
+import com.android.contacts.R;
+
+/**
+ * Adapter for a ListView containing history items from the details of a call.
+ */
+public class CallDetailHistoryAdapter extends BaseAdapter {
+ /** The top element is a blank header, which is hidden under the rest of the UI. */
+ private static final int VIEW_TYPE_HEADER = 0;
+ /** Each history item shows the detail of a call. */
+ private static final int VIEW_TYPE_HISTORY_ITEM = 1;
+
+ private final Context mContext;
+ private final LayoutInflater mLayoutInflater;
+ private final CallTypeHelper mCallTypeHelper;
+ private final PhoneCallDetails[] mPhoneCallDetails;
+ /** Whether the voicemail controls are shown. */
+ private final boolean mShowVoicemail;
+ /** Whether the call and SMS controls are shown. */
+ private final boolean mShowCallAndSms;
+ /** The controls that are shown on top of the history list. */
+ private final View mControls;
+ /** The listener to changes of focus of the header. */
+ private View.OnFocusChangeListener mHeaderFocusChangeListener =
+ new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ // When the header is focused, focus the controls above it instead.
+ if (hasFocus) {
+ mControls.requestFocus();
+ }
+ }
+ };
+
+ public CallDetailHistoryAdapter(Context context, LayoutInflater layoutInflater,
+ CallTypeHelper callTypeHelper, PhoneCallDetails[] phoneCallDetails,
+ boolean showVoicemail, boolean showCallAndSms, View controls) {
+ mContext = context;
+ mLayoutInflater = layoutInflater;
+ mCallTypeHelper = callTypeHelper;
+ mPhoneCallDetails = phoneCallDetails;
+ mShowVoicemail = showVoicemail;
+ mShowCallAndSms = showCallAndSms;
+ mControls = controls;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ // None of history will be clickable.
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mPhoneCallDetails.length + 1;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ if (position == 0) {
+ return null;
+ }
+ return mPhoneCallDetails[position - 1];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (position == 0) {
+ return -1;
+ }
+ return position - 1;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0) {
+ return VIEW_TYPE_HEADER;
+ }
+ return VIEW_TYPE_HISTORY_ITEM;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (position == 0) {
+ final View header = convertView == null
+ ? mLayoutInflater.inflate(R.layout.call_detail_history_header, parent, false)
+ : convertView;
+ // Voicemail controls are only shown in the main UI if there is a voicemail.
+ View voicemailContainer = header.findViewById(R.id.header_voicemail_container);
+ voicemailContainer.setVisibility(mShowVoicemail ? View.VISIBLE : View.GONE);
+ // Call and SMS controls are only shown in the main UI if there is a known number.
+ View callAndSmsContainer = header.findViewById(R.id.header_call_and_sms_container);
+ callAndSmsContainer.setVisibility(mShowCallAndSms ? View.VISIBLE : View.GONE);
+ header.setFocusable(true);
+ header.setOnFocusChangeListener(mHeaderFocusChangeListener);
+ return header;
+ }
+
+ // Make sure we have a valid convertView to start with
+ final View result = convertView == null
+ ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false)
+ : convertView;
+
+ PhoneCallDetails details = mPhoneCallDetails[position - 1];
+ CallTypeIconsView callTypeIconView =
+ (CallTypeIconsView) result.findViewById(R.id.call_type_icon);
+ TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
+ TextView dateView = (TextView) result.findViewById(R.id.date);
+ TextView durationView = (TextView) result.findViewById(R.id.duration);
+
+ int callType = details.callTypes[0];
+ callTypeIconView.clear();
+ callTypeIconView.add(callType);
+ callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType));
+ // Set the date.
+ CharSequence dateValue = DateUtils.formatDateRange(mContext, details.date, details.date,
+ DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR);
+ dateView.setText(dateValue);
+ // Set the duration
+ if (callType == Calls.MISSED_TYPE || callType == Calls.VOICEMAIL_TYPE) {
+ durationView.setVisibility(View.GONE);
+ } else {
+ durationView.setVisibility(View.VISIBLE);
+ durationView.setText(formatDuration(details.duration));
+ }
+
+ return result;
+ }
+
+ private String formatDuration(long elapsedSeconds) {
+ long minutes = 0;
+ long seconds = 0;
+
+ if (elapsedSeconds >= 60) {
+ minutes = elapsedSeconds / 60;
+ elapsedSeconds -= minutes * 60;
+ }
+ seconds = elapsedSeconds;
+
+ return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds);
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
new file mode 100644
index 000000000..217f59765
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -0,0 +1,802 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.android.contacts.ContactPhotoManager;
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+import com.android.dialer.util.ExpirableCache;
+import com.android.contacts.util.UriUtils;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+
+import java.util.LinkedList;
+
+/**
+ * Adapter class to fill in data for the Call Log.
+ */
+/*package*/ class CallLogAdapter extends GroupingListAdapter
+ implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+ /** Interface used to initiate a refresh of the content. */
+ public interface CallFetcher {
+ public void fetchCalls();
+ }
+
+ /**
+ * Stores a phone number of a call with the country code where it originally occurred.
+ * <p>
+ * Note the country does not necessarily specifies the country of the phone number itself, but
+ * it is the country in which the user was in when the call was placed or received.
+ */
+ private static final class NumberWithCountryIso {
+ public final String number;
+ public final String countryIso;
+
+ public NumberWithCountryIso(String number, String countryIso) {
+ this.number = number;
+ this.countryIso = countryIso;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (!(o instanceof NumberWithCountryIso)) return false;
+ NumberWithCountryIso other = (NumberWithCountryIso) o;
+ return TextUtils.equals(number, other.number)
+ && TextUtils.equals(countryIso, other.countryIso);
+ }
+
+ @Override
+ public int hashCode() {
+ return (number == null ? 0 : number.hashCode())
+ ^ (countryIso == null ? 0 : countryIso.hashCode());
+ }
+ }
+
+ /** The time in millis to delay starting the thread processing requests. */
+ private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
+
+ /** The size of the cache of contact info. */
+ private static final int CONTACT_INFO_CACHE_SIZE = 100;
+
+ private final Context mContext;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final CallFetcher mCallFetcher;
+ private ViewTreeObserver mViewTreeObserver = null;
+
+ /**
+ * A cache of the contact details for the phone numbers in the call log.
+ * <p>
+ * The content of the cache is expired (but not purged) whenever the application comes to
+ * the foreground.
+ * <p>
+ * The key is number with the country in which the call was placed or received.
+ */
+ private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
+
+ /**
+ * A request for contact details for the given number.
+ */
+ private static final class ContactInfoRequest {
+ /** The number to look-up. */
+ public final String number;
+ /** The country in which a call to or from this number was placed or received. */
+ public final String countryIso;
+ /** The cached contact information stored in the call log. */
+ public final ContactInfo callLogInfo;
+
+ public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
+ this.number = number;
+ this.countryIso = countryIso;
+ this.callLogInfo = callLogInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof ContactInfoRequest)) return false;
+
+ ContactInfoRequest other = (ContactInfoRequest) obj;
+
+ if (!TextUtils.equals(number, other.number)) return false;
+ if (!TextUtils.equals(countryIso, other.countryIso)) return false;
+ if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
+ result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
+ result = prime * result + ((number == null) ? 0 : number.hashCode());
+ return result;
+ }
+ }
+
+ /**
+ * List of requests to update contact details.
+ * <p>
+ * Each request is made of a phone number to look up, and the contact info currently stored in
+ * the call log for this number.
+ * <p>
+ * The requests are added when displaying the contacts and are processed by a background
+ * thread.
+ */
+ private final LinkedList<ContactInfoRequest> mRequests;
+
+ private boolean mLoading = true;
+ private static final int REDRAW = 1;
+ private static final int START_THREAD = 2;
+
+ private QueryThread mCallerIdThread;
+
+ /** Instance of helper class for managing views. */
+ private final CallLogListItemHelper mCallLogViewsHelper;
+
+ /** Helper to set up contact photos. */
+ private final ContactPhotoManager mContactPhotoManager;
+ /** Helper to parse and process phone numbers. */
+ private PhoneNumberHelper mPhoneNumberHelper;
+ /** Helper to group call log entries. */
+ private final CallLogGroupBuilder mCallLogGroupBuilder;
+
+ /** Can be set to true by tests to disable processing of requests. */
+ private volatile boolean mRequestProcessingDisabled = false;
+
+ /** Listener for the primary action in the list, opens the call details. */
+ private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ mContext.startActivity(intentProvider.getIntent(mContext));
+ }
+ }
+ };
+ /** Listener for the secondary action in the list, either call or play. */
+ private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ mContext.startActivity(intentProvider.getIntent(mContext));
+ }
+ }
+ };
+
+ @Override
+ public boolean onPreDraw() {
+ // We only wanted to listen for the first draw (and this is it).
+ unregisterPreDrawListener();
+
+ // Only schedule a thread-creation message if the thread hasn't been
+ // created yet. This is purely an optimization, to queue fewer messages.
+ if (mCallerIdThread == null) {
+ mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
+ }
+
+ 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;
+ }
+ }
+ };
+
+ CallLogAdapter(Context context, CallFetcher callFetcher,
+ ContactInfoHelper contactInfoHelper) {
+ super(context);
+
+ mContext = context;
+ mCallFetcher = callFetcher;
+ mContactInfoHelper = contactInfoHelper;
+
+ mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+ mRequests = new LinkedList<ContactInfoRequest>();
+
+ Resources resources = mContext.getResources();
+ CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+
+ mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
+ mPhoneNumberHelper = new PhoneNumberHelper(resources);
+ PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
+ resources, callTypeHelper, mPhoneNumberHelper);
+ mCallLogViewsHelper =
+ new CallLogListItemHelper(
+ phoneCallDetailsHelper, mPhoneNumberHelper, resources);
+ mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+ }
+
+ /**
+ * Requery on background thread when {@link Cursor} changes.
+ */
+ @Override
+ protected void onContentChanged() {
+ mCallFetcher.fetchCalls();
+ }
+
+ 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();
+ }
+ }
+
+ /**
+ * Starts a background thread to process contact-lookup requests, unless one
+ * has already been started.
+ */
+ private synchronized void startRequestProcessing() {
+ // For unit-testing.
+ if (mRequestProcessingDisabled) return;
+
+ // Idempotence... if a thread is already started, don't start another.
+ if (mCallerIdThread != null) return;
+
+ mCallerIdThread = new QueryThread();
+ mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+ mCallerIdThread.start();
+ }
+
+ /**
+ * Stops the background thread that processes updates and cancels any
+ * pending requests to start it.
+ */
+ public synchronized void stopRequestProcessing() {
+ // Remove any pending requests to start the processing thread.
+ mHandler.removeMessages(START_THREAD);
+ if (mCallerIdThread != null) {
+ // Stop the thread; we are finished with it.
+ mCallerIdThread.stopProcessing();
+ mCallerIdThread.interrupt();
+ mCallerIdThread = null;
+ }
+ }
+
+ /**
+ * Stop receiving onPreDraw() notifications.
+ */
+ private void unregisterPreDrawListener() {
+ if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
+ mViewTreeObserver.removeOnPreDrawListener(this);
+ }
+ mViewTreeObserver = null;
+ }
+
+ public void invalidateCache() {
+ mContactInfoCache.expireAll();
+
+ // Restart the request-processing thread after the next draw.
+ stopRequestProcessing();
+ unregisterPreDrawListener();
+ }
+
+ /**
+ * Enqueues a request to look up the contact details for the given phone number.
+ * <p>
+ * It also provides the current contact info stored in the call log for this number.
+ * <p>
+ * If the {@code immediate} parameter is true, it will start immediately the thread that looks
+ * up the contact information (if it has not been already started). Otherwise, it will be
+ * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
+ */
+ @VisibleForTesting
+ void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+ boolean immediate) {
+ ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
+ synchronized (mRequests) {
+ if (!mRequests.contains(request)) {
+ mRequests.add(request);
+ mRequests.notifyAll();
+ }
+ }
+ if (immediate) startRequestProcessing();
+ }
+
+ /**
+ * Queries the appropriate content provider for the contact associated with the number.
+ * <p>
+ * Upon completion it also updates the cache in the call log, if it is different from
+ * {@code callLogInfo}.
+ * <p>
+ * The number might be either a SIP address or a phone number.
+ * <p>
+ * It returns true if it updated the content of the cache and we should therefore tell the
+ * view to update its content.
+ */
+ private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
+ final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+
+ if (info == null) {
+ // The lookup failed, just return without requesting to update the view.
+ return false;
+ }
+
+ // Check the existing entry in the cache: only if it has changed we should update the
+ // view.
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+ boolean updated = (existingInfo != ContactInfo.EMPTY) && !info.equals(existingInfo);
+
+ // Store the data in the cache so that the UI thread can use to display it. Store it
+ // even if it has not changed so that it is marked as not expired.
+ mContactInfoCache.put(numberCountryIso, info);
+ // Update the call log even if the cache it is up-to-date: it is possible that the cache
+ // contains the value from a different call log entry.
+ updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
+ return updated;
+ }
+
+ /*
+ * Handles requests for contact name and number type.
+ */
+ private class QueryThread extends Thread {
+ private volatile boolean mDone = false;
+
+ public QueryThread() {
+ super("CallLogAdapter.QueryThread");
+ }
+
+ public void stopProcessing() {
+ mDone = true;
+ }
+
+ @Override
+ public void run() {
+ boolean needRedraw = false;
+ while (true) {
+ // Check if thread is finished, and if so return immediately.
+ if (mDone) return;
+
+ // Obtain next request, if any is available.
+ // Keep synchronized section small.
+ ContactInfoRequest req = null;
+ synchronized (mRequests) {
+ if (!mRequests.isEmpty()) {
+ req = mRequests.removeFirst();
+ }
+ }
+
+ if (req != null) {
+ // Process the request. If the lookup succeeds, schedule a
+ // redraw.
+ needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
+ } else {
+ // Throttle redraw rate by only sending them when there are
+ // more requests.
+ if (needRedraw) {
+ needRedraw = false;
+ mHandler.sendEmptyMessage(REDRAW);
+ }
+
+ // Wait until another request is available, or until this
+ // thread is no longer needed (as indicated by being
+ // interrupted).
+ try {
+ synchronized (mRequests) {
+ mRequests.wait(1000);
+ }
+ } catch (InterruptedException ie) {
+ // Ignore, and attempt to continue processing requests.
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+ mCallLogGroupBuilder.addGroups(cursor);
+ }
+
+ @Override
+ protected View newStandAloneView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindStandAloneView(View view, Context context, Cursor cursor) {
+ bindView(view, cursor, 1);
+ }
+
+ @Override
+ protected View newChildView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor) {
+ bindView(view, cursor, 1);
+ }
+
+ @Override
+ protected View newGroupView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ bindView(view, cursor, groupSize);
+ }
+
+ private void findAndCacheViews(View view) {
+ // Get the views to bind to.
+ CallLogListItemViews views = CallLogListItemViews.fromView(view);
+ views.primaryActionView.setOnClickListener(mPrimaryActionListener);
+ views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
+ view.setTag(views);
+ }
+
+ /**
+ * Binds the views in the entry to the data in the call log.
+ *
+ * @param view the view corresponding to this entry
+ * @param c the cursor pointing to the entry in the call log
+ * @param count the number of entries in the current item, greater than 1 if it is a group
+ */
+ private void bindView(View view, Cursor c, int count) {
+ final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+ final int section = c.getInt(CallLogQuery.SECTION);
+
+ // This might be a header: check the value of the section column in the cursor.
+ if (section == CallLogQuery.SECTION_NEW_HEADER
+ || section == CallLogQuery.SECTION_OLD_HEADER) {
+ views.primaryActionView.setVisibility(View.GONE);
+ views.bottomDivider.setVisibility(View.GONE);
+ views.listHeaderTextView.setVisibility(View.VISIBLE);
+ views.listHeaderTextView.setText(
+ section == CallLogQuery.SECTION_NEW_HEADER
+ ? R.string.call_log_new_header
+ : R.string.call_log_old_header);
+ // Nothing else to set up for a header.
+ return;
+ }
+ // Default case: an item in the call log.
+ views.primaryActionView.setVisibility(View.VISIBLE);
+ views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE);
+ views.listHeaderTextView.setVisibility(View.GONE);
+
+ final String number = c.getString(CallLogQuery.NUMBER);
+ final long date = c.getLong(CallLogQuery.DATE);
+ final long duration = c.getLong(CallLogQuery.DURATION);
+ final int callType = c.getInt(CallLogQuery.CALL_TYPE);
+ final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+
+ final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
+
+ views.primaryActionView.setTag(
+ IntentProvider.getCallDetailIntentProvider(
+ this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
+ // Store away the voicemail information so we can play it directly.
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+ final long rowId = c.getLong(CallLogQuery.ID);
+ views.secondaryActionView.setTag(
+ IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
+ } else if (!TextUtils.isEmpty(number)) {
+ // Store away the number so we can call it directly if you click on the call icon.
+ views.secondaryActionView.setTag(
+ IntentProvider.getReturnCallIntentProvider(number));
+ } else {
+ // No action enabled.
+ views.secondaryActionView.setTag(null);
+ }
+
+ // Lookup contacts with this number
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+ mContactInfoCache.getCachedValue(numberCountryIso);
+ ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+ if (!mPhoneNumberHelper.canPlaceCallsTo(number)
+ || mPhoneNumberHelper.isVoicemailNumber(number)) {
+ // If this is a number that cannot be dialed, there is no point in looking up a contact
+ // for it.
+ info = ContactInfo.EMPTY;
+ } else if (cachedInfo == null) {
+ mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
+ // Use the cached contact info from the call log.
+ info = cachedContactInfo;
+ // The db request should happen on a non-UI thread.
+ // Request the contact details immediately since they are currently missing.
+ enqueueRequest(number, countryIso, cachedContactInfo, true);
+ // We will format the phone number when we make the background request.
+ } else {
+ if (cachedInfo.isExpired()) {
+ // The contact info is no longer up to date, we should request it. However, we
+ // do not need to request them immediately.
+ enqueueRequest(number, countryIso, cachedContactInfo, false);
+ } else if (!callLogInfoMatches(cachedContactInfo, info)) {
+ // The call log information does not match the one we have, look it up again.
+ // We could simply update the call log directly, but that needs to be done in a
+ // background thread, so it is easier to simply request a new lookup, which will, as
+ // a side-effect, update the call log.
+ enqueueRequest(number, countryIso, cachedContactInfo, false);
+ }
+
+ if (info == ContactInfo.EMPTY) {
+ // Use the cached contact info from the call log.
+ info = cachedContactInfo;
+ }
+ }
+
+ final Uri lookupUri = info.lookupUri;
+ final String name = info.name;
+ final int ntype = info.type;
+ final String label = info.label;
+ final long photoId = info.photoId;
+ CharSequence formattedNumber = info.formattedNumber;
+ final int[] callTypes = getCallTypes(c, count);
+ final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
+ final PhoneCallDetails details;
+ if (TextUtils.isEmpty(name)) {
+ details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+ callTypes, date, duration);
+ } else {
+ // We do not pass a photo id since we do not need the high-res picture.
+ details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+ callTypes, date, duration, name, ntype, label, lookupUri, null);
+ }
+
+ final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
+ // New items also use the highlighted version of the text.
+ final boolean isHighlighted = isNew;
+ mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
+ setPhoto(views, photoId, lookupUri);
+
+ // Listen for the first draw
+ if (mViewTreeObserver == null) {
+ mViewTreeObserver = view.getViewTreeObserver();
+ mViewTreeObserver.addOnPreDrawListener(this);
+ }
+ }
+
+ /** Returns true if this is the last item of a section. */
+ private boolean isLastOfSection(Cursor c) {
+ if (c.isLast()) return true;
+ final int section = c.getInt(CallLogQuery.SECTION);
+ if (!c.moveToNext()) return true;
+ final int nextSection = c.getInt(CallLogQuery.SECTION);
+ c.moveToPrevious();
+ return section != nextSection;
+ }
+
+ /** Checks whether the contact info from the call log matches the one from the contacts db. */
+ private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+ // The call log only contains a subset of the fields in the contacts db.
+ // Only check those.
+ return TextUtils.equals(callLogInfo.name, info.name)
+ && callLogInfo.type == info.type
+ && TextUtils.equals(callLogInfo.label, info.label);
+ }
+
+ /** Stores the updated contact info in the call log if it is different from the current one. */
+ private void updateCallLogContactInfoCache(String number, String countryIso,
+ ContactInfo updatedInfo, ContactInfo callLogInfo) {
+ final ContentValues values = new ContentValues();
+ boolean needsUpdate = false;
+
+ if (callLogInfo != null) {
+ if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.type != callLogInfo.type) {
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ needsUpdate = true;
+ }
+ if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ needsUpdate = true;
+ }
+ if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ needsUpdate = true;
+ }
+ if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ needsUpdate = true;
+ }
+ if (updatedInfo.photoId != callLogInfo.photoId) {
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ needsUpdate = true;
+ }
+ if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+ } else {
+ // No previous values, store all of them.
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+
+ if (!needsUpdate) return;
+
+ if (countryIso == null) {
+ mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+ new String[]{ number });
+ } else {
+ mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+ new String[]{ number, countryIso });
+ }
+ }
+
+ /** Returns the contact information as stored in the call log. */
+ private ContactInfo getContactInfoFromCallLog(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+ info.name = c.getString(CallLogQuery.CACHED_NAME);
+ info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+ String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+ info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
+ info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+ info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+ info.photoUri = null; // We do not cache the photo URI.
+ info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+ return info;
+ }
+
+ /**
+ * Returns the call types for the given number of items in the cursor.
+ * <p>
+ * It uses the next {@code count} rows in the cursor to extract the types.
+ * <p>
+ * It position in the cursor is unchanged by this function.
+ */
+ private int[] getCallTypes(Cursor cursor, int count) {
+ int position = cursor.getPosition();
+ int[] callTypes = new int[count];
+ for (int index = 0; index < count; ++index) {
+ callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(position);
+ return callTypes;
+ }
+
+ private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
+ views.quickContactView.assignContactUri(contactUri);
+ mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, true);
+ }
+
+ /**
+ * Sets whether processing of requests for contact details should be enabled.
+ * <p>
+ * This method should be called in tests to disable such processing of requests when not
+ * needed.
+ */
+ @VisibleForTesting
+ void disableRequestProcessingForTest() {
+ mRequestProcessingDisabled = true;
+ }
+
+ @VisibleForTesting
+ void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ mContactInfoCache.put(numberCountryIso, contactInfo);
+ }
+
+ @Override
+ public void addGroup(int cursorPosition, int size, boolean expanded) {
+ super.addGroup(cursorPosition, size, expanded);
+ }
+
+ /*
+ * 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
+ */
+ public String getBetterNumberFromContacts(String number, String countryIso) {
+ String matchingNumber = null;
+ // Look in the cache first. If it's not found then query the Phones db
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+ if (ci != null && ci != ContactInfo.EMPTY) {
+ matchingNumber = ci.number;
+ } else {
+ try {
+ Cursor phonesCursor = mContext.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;
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
new file mode 100644
index 000000000..4b3113403
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -0,0 +1,549 @@
+/*
+ * 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.dialer.calllog;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.preference.PreferenceManager;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+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.widget.ListView;
+import android.widget.TextView;
+
+import com.android.common.io.MoreCloseables;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.EmptyLoader;
+import com.android.dialer.voicemail.VoicemailStatusHelper;
+import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.ITelephony;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * Displays a list of call log entries.
+ */
+public class CallLogFragment extends ListFragment
+ implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
+ private static final String TAG = "CallLogFragment";
+
+ /**
+ * ID of the empty loader to defer other fragments.
+ */
+ private static final int EMPTY_LOADER_ID = 0;
+
+ private static final String PREF_CALL_LOG_FILTER_LAST_CALL_TYPE = "CallLogFragment_last_filter";
+
+ private CallLogAdapter mAdapter;
+ private CallLogQueryHandler mCallLogQueryHandler;
+ private boolean mScrollToTop;
+
+ /** Whether there is at least one voicemail source installed. */
+ private boolean mVoicemailSourcesAvailable = false;
+ /** Whether we are currently filtering over voicemail. */
+ private boolean mShowingVoicemailOnly = false;
+
+ private VoicemailStatusHelper mVoicemailStatusHelper;
+ private View mStatusMessageView;
+ private TextView mStatusMessageText;
+ private TextView mStatusMessageAction;
+ private TextView mFilterStatusView;
+ private KeyguardManager mKeyguardManager;
+
+ private boolean mEmptyLoaderRunning;
+ private boolean mCallLogFetched;
+ private boolean mVoicemailStatusFetched;
+
+ private final Handler mHandler = new Handler();
+
+ private class CustomContentObserver extends ContentObserver {
+ public CustomContentObserver() {
+ super(mHandler);
+ }
+ @Override
+ public void onChange(boolean selfChange) {
+ mRefreshDataRequired = true;
+ }
+ }
+
+ // See issue 6363009
+ private final ContentObserver mCallLogObserver = new CustomContentObserver();
+ private final ContentObserver mContactsObserver = new CustomContentObserver();
+ private boolean mRefreshDataRequired = true;
+
+ // Exactly same variable is in Fragment as a package private.
+ private boolean mMenuVisible = true;
+
+ // Default to all calls.
+ private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
+ mKeyguardManager =
+ (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
+ getActivity().getContentResolver().registerContentObserver(
+ CallLog.CONTENT_URI, true, mCallLogObserver);
+ getActivity().getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
+ setHasOptionsMenu(true);
+
+ // Load the last filter used.
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ mCallTypeFilter = prefs.getInt(PREF_CALL_LOG_FILTER_LAST_CALL_TYPE,
+ CallLogQueryHandler.CALL_TYPE_ALL);
+ }
+
+ /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
+ @Override
+ public void onCallsFetched(Cursor cursor) {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+ mAdapter.setLoading(false);
+ mAdapter.changeCursor(cursor);
+ // This will update the state of the "Clear call log" menu item.
+ getActivity().invalidateOptionsMenu();
+ if (mScrollToTop) {
+ final ListView listView = getListView();
+ // The smooth-scroll animation happens over a fixed time period.
+ // As a result, if it scrolls through a large portion of the list,
+ // each frame will jump so far from the previous one that the user
+ // will not experience the illusion of downward motion. Instead,
+ // if we're not already near the top of the list, we instantly jump
+ // near the top, and animate from there.
+ if (listView.getFirstVisiblePosition() > 5) {
+ listView.setSelection(5);
+ }
+ // Workaround for framework issue: the smooth-scroll doesn't
+ // occur if setSelection() is called immediately before.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+ listView.smoothScrollToPosition(0);
+ }
+ });
+
+ mScrollToTop = false;
+ }
+ mCallLogFetched = true;
+ destroyEmptyLoaderIfAllDataFetched();
+ }
+
+ /**
+ * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
+ */
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+ updateVoicemailStatusMessage(statusCursor);
+
+ int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
+ setVoicemailSourcesAvailable(activeSources != 0);
+ MoreCloseables.closeQuietly(statusCursor);
+ mVoicemailStatusFetched = true;
+ destroyEmptyLoaderIfAllDataFetched();
+ }
+
+ private void destroyEmptyLoaderIfAllDataFetched() {
+ if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
+ mEmptyLoaderRunning = false;
+ getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
+ }
+ }
+
+ /** Sets whether there are any voicemail sources available in the platform. */
+ private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
+ if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
+ mVoicemailSourcesAvailable = voicemailSourcesAvailable;
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ // This is so that the options menu content is updated.
+ activity.invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+ mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+ mStatusMessageView = view.findViewById(R.id.voicemail_status);
+ mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
+ mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
+ mFilterStatusView = (TextView) view.findViewById(R.id.filter_status);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
+ mAdapter = new CallLogAdapter(getActivity(), this,
+ new ContactInfoHelper(getActivity(), currentCountryIso));
+ setListAdapter(mAdapter);
+ getListView().setItemsCanFocus(true);
+
+ updateFilterHeader();
+ }
+
+ /**
+ * Based on the new intent, decide whether the list should be configured
+ * to scroll up to display the first item.
+ */
+ public void configureScreenFromIntent(Intent newIntent) {
+ // Typically, when switching to the call-log we want to show the user
+ // the same section of the list that they were most recently looking
+ // at. However, under some circumstances, we want to automatically
+ // scroll to the top of the list to present the newest call items.
+ // For example, immediately after a call is finished, we want to
+ // display information about that call.
+ mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
+ }
+
+ @Override
+ public void onStart() {
+ // Start the empty loader now to defer other fragments. We destroy it when both calllog
+ // and the voicemail status are fetched.
+ getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
+ new EmptyLoader.Callback(getActivity()));
+ mEmptyLoaderRunning = true;
+ super.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshData();
+ }
+
+ private void updateVoicemailStatusMessage(Cursor statusCursor) {
+ List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
+ if (messages.size() == 0) {
+ mStatusMessageView.setVisibility(View.GONE);
+ } else {
+ mStatusMessageView.setVisibility(View.VISIBLE);
+ // TODO: Change the code to show all messages. For now just pick the first message.
+ final StatusMessage message = messages.get(0);
+ if (message.showInCallLog()) {
+ mStatusMessageText.setText(message.callLogMessageId);
+ }
+ if (message.actionMessageId != -1) {
+ mStatusMessageAction.setText(message.actionMessageId);
+ }
+ if (message.actionUri != null) {
+ mStatusMessageAction.setVisibility(View.VISIBLE);
+ mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getActivity().startActivity(
+ new Intent(Intent.ACTION_VIEW, message.actionUri));
+ }
+ });
+ } else {
+ mStatusMessageAction.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Kill the requests thread
+ mAdapter.stopRequestProcessing();
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ prefs.edit()
+ .putInt(PREF_CALL_LOG_FILTER_LAST_CALL_TYPE, mCallTypeFilter)
+ .apply();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ updateOnExit();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mAdapter.stopRequestProcessing();
+ mAdapter.changeCursor(null);
+ getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
+ getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
+ }
+
+ @Override
+ public void fetchCalls() {
+ mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
+ }
+
+ public void startCallsQuery() {
+ mAdapter.setLoading(true);
+ mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
+ if (mShowingVoicemailOnly) {
+ mShowingVoicemailOnly = false;
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+
+ private void startVoicemailStatusQuery() {
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.call_log_options, menu);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
+ // Check if all the menu items are inflated correctly. As a shortcut, we assume all
+ // menu items are ready if the first item is non-null.
+ if (itemDeleteAll != null) {
+ itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
+ menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.delete_all:
+ ClearCallLogDialog.show(getFragmentManager());
+ return true;
+
+ case R.id.show_outgoing_only:
+ mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE);
+ mCallTypeFilter = Calls.OUTGOING_TYPE;
+ updateFilterHeader();
+ return true;
+
+ case R.id.show_incoming_only:
+ mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE);
+ mCallTypeFilter = Calls.INCOMING_TYPE;
+ updateFilterHeader();
+ return true;
+
+ case R.id.show_missed_only:
+ mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE);
+ mCallTypeFilter = Calls.MISSED_TYPE;
+ updateFilterHeader();
+ return true;
+
+ case R.id.show_voicemails_only:
+ mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
+ mCallTypeFilter = Calls.VOICEMAIL_TYPE;
+ updateFilterHeader();
+ mShowingVoicemailOnly = true;
+ return true;
+
+ case R.id.show_all_calls:
+ mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
+ mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+ updateFilterHeader();
+ mShowingVoicemailOnly = false;
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private void updateFilterHeader() {
+ switch (mCallTypeFilter) {
+ case CallLogQueryHandler.CALL_TYPE_ALL:
+ mFilterStatusView.setVisibility(View.GONE);
+ break;
+ case Calls.INCOMING_TYPE:
+ showFilterStatus(R.string.call_log_incoming_header);
+ break;
+ case Calls.OUTGOING_TYPE:
+ showFilterStatus(R.string.call_log_outgoing_header);
+ break;
+ case Calls.MISSED_TYPE:
+ showFilterStatus(R.string.call_log_missed_header);
+ break;
+ case Calls.VOICEMAIL_TYPE:
+ showFilterStatus(R.string.call_log_voicemail_header);
+ break;
+ }
+ }
+
+ private void showFilterStatus(int resId) {
+ mFilterStatusView.setText(resId);
+ mFilterStatusView.setVisibility(View.VISIBLE);
+ }
+
+ 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 = ContactsUtils.getCallIntent(
+ Uri.fromParts(Constants.SCHEME_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
+ String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
+ number = mAdapter.getBetterNumberFromContacts(number, countryIso);
+ }
+ intent = ContactsUtils.getCallIntent(
+ Uri.fromParts(Constants.SCHEME_TEL, number, null));
+ }
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ startActivity(intent);
+ }
+ }
+
+ @VisibleForTesting
+ CallLogAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public void setMenuVisibility(boolean menuVisible) {
+ super.setMenuVisibility(menuVisible);
+ if (mMenuVisible != menuVisible) {
+ mMenuVisible = menuVisible;
+ if (!menuVisible) {
+ updateOnExit();
+ } else if (isResumed()) {
+ refreshData();
+ }
+ }
+ }
+
+ /** Requests updates to the data to be shown. */
+ private void refreshData() {
+ // Prevent unnecessary refresh.
+ if (mRefreshDataRequired) {
+ // Mark all entries in the contact info cache as out of date, so they will be looked up
+ // again once being shown.
+ mAdapter.invalidateCache();
+ startCallsQuery();
+ startVoicemailStatusQuery();
+ updateOnEntry();
+ mRefreshDataRequired = false;
+ }
+ }
+
+ /** Removes the missed call notifications. */
+ private void removeMissedCallNotifications() {
+ try {
+ ITelephony telephony =
+ ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
+ if (telephony != null) {
+ telephony.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");
+ }
+ }
+
+ /** Updates call data and notification state while leaving the call log tab. */
+ private void updateOnExit() {
+ updateOnTransition(false);
+ }
+
+ /** Updates call data and notification state while entering the call log tab. */
+ private void updateOnEntry() {
+ updateOnTransition(true);
+ }
+
+ private void updateOnTransition(boolean onEntry) {
+ // We don't want to update any call data when keyguard is on because the user has likely not
+ // seen the new calls yet.
+ // This might be called before onCreate() and thus we need to check null explicitly.
+ if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
+ // On either of the transitions we reset the new flag and update the notifications.
+ // While exiting we additionally consume all missed calls (by marking them as read).
+ // This will ensure that they no more appear in the "new" section when we return back.
+ mCallLogQueryHandler.markNewCallsAsOld();
+ if (!onEntry) {
+ mCallLogQueryHandler.markMissedCallsAsRead();
+ }
+ removeMissedCallNotifications();
+ updateVoicemailNotifications();
+ }
+ }
+
+ private void updateVoicemailNotifications() {
+ Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ getActivity().startService(serviceIntent);
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
new file mode 100644
index 000000000..bf472bd7a
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -0,0 +1,159 @@
+/*
+ * 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.dialer.calllog;
+
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Groups together calls in the call log.
+ * <p>
+ * This class is meant to be used in conjunction with {@link GroupingListAdapter}.
+ */
+public class CallLogGroupBuilder {
+ public interface GroupCreator {
+ public void addGroup(int cursorPosition, int size, boolean expanded);
+ }
+
+ /** The object on which the groups are created. */
+ private final GroupCreator mGroupCreator;
+
+ public CallLogGroupBuilder(GroupCreator groupCreator) {
+ mGroupCreator = groupCreator;
+ }
+
+ /**
+ * Finds all groups of adjacent entries in the call log which should be grouped together and
+ * calls {@link GroupCreator#addGroup(int, int, boolean)} on {@link #mGroupCreator} for each of
+ * them.
+ * <p>
+ * For entries that are not grouped with others, we do not need to create a group of size one.
+ * <p>
+ * It assumes that the cursor will not change during its execution.
+ *
+ * @see GroupingListAdapter#addGroups(Cursor)
+ */
+ public void addGroups(Cursor cursor) {
+ final int count = cursor.getCount();
+ if (count == 0) {
+ return;
+ }
+
+ int currentGroupSize = 1;
+ cursor.moveToFirst();
+ // The number of the first entry in the group.
+ String firstNumber = cursor.getString(CallLogQuery.NUMBER);
+ // This is the type of the first call in the group.
+ int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ while (cursor.moveToNext()) {
+ // The number of the current row in the cursor.
+ final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
+ final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
+ final boolean shouldGroup;
+
+ if (CallLogQuery.isSectionHeader(cursor)) {
+ // Cannot group headers.
+ shouldGroup = false;
+ } else if (!sameNumber) {
+ // Should only group with calls from the same number.
+ shouldGroup = false;
+ } else if (firstCallType == Calls.VOICEMAIL_TYPE) {
+ // never group voicemail.
+ shouldGroup = false;
+ } else {
+ // Incoming, outgoing, and missed calls group together.
+ shouldGroup = (callType == Calls.INCOMING_TYPE || callType == Calls.OUTGOING_TYPE ||
+ callType == Calls.MISSED_TYPE);
+ }
+
+ if (shouldGroup) {
+ // Increment the size of the group to include the current call, but do not create
+ // the group until we find a call that does not match.
+ currentGroupSize++;
+ } else {
+ // Create a group for the previous set of calls, excluding the current one, but do
+ // not create a group for a single call.
+ if (currentGroupSize > 1) {
+ addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
+ }
+ // Start a new group; it will include at least the current call.
+ currentGroupSize = 1;
+ // The current entry is now the first in the group.
+ firstNumber = currentNumber;
+ firstCallType = callType;
+ }
+ }
+ // If the last set of calls at the end of the call log was itself a group, create it now.
+ if (currentGroupSize > 1) {
+ addGroup(count - currentGroupSize, currentGroupSize);
+ }
+ }
+
+ /**
+ * Creates a group of items in the cursor.
+ * <p>
+ * The group is always unexpanded.
+ *
+ * @see CallLogAdapter#addGroup(int, int, boolean)
+ */
+ private void addGroup(int cursorPosition, int size) {
+ mGroupCreator.addGroup(cursorPosition, size, false);
+ }
+
+ @VisibleForTesting
+ boolean equalNumbers(String number1, String number2) {
+ if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) {
+ return compareSipAddresses(number1, number2);
+ } else {
+ return PhoneNumberUtils.compare(number1, number2);
+ }
+ }
+
+ @VisibleForTesting
+ boolean compareSipAddresses(String number1, String number2) {
+ if (number1 == null || number2 == null) return number1 == number2;
+
+ int index1 = number1.indexOf('@');
+ final String userinfo1;
+ final String rest1;
+ if (index1 != -1) {
+ userinfo1 = number1.substring(0, index1);
+ rest1 = number1.substring(index1);
+ } else {
+ userinfo1 = number1;
+ rest1 = "";
+ }
+
+ int index2 = number2.indexOf('@');
+ final String userinfo2;
+ final String rest2;
+ if (index2 != -1) {
+ userinfo2 = number2.substring(0, index2);
+ rest2 = number2.substring(index2);
+ } else {
+ userinfo2 = number2;
+ rest2 = "";
+ }
+
+ return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
new file mode 100644
index 000000000..7862a5679
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -0,0 +1,109 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+
+/**
+ * Helper class to fill in the views of a call log entry.
+ */
+/*package*/ class CallLogListItemHelper {
+ /** Helper for populating the details of a phone call. */
+ private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
+ /** Helper for handling phone numbers. */
+ private final PhoneNumberHelper mPhoneNumberHelper;
+ /** Resources to look up strings. */
+ private final Resources mResources;
+
+ /**
+ * Creates a new helper instance.
+ *
+ * @param phoneCallDetailsHelper used to set the details of a phone call
+ * @param phoneNumberHelper used to process phone number
+ */
+ public CallLogListItemHelper(PhoneCallDetailsHelper phoneCallDetailsHelper,
+ PhoneNumberHelper phoneNumberHelper, Resources resources) {
+ mPhoneCallDetailsHelper = phoneCallDetailsHelper;
+ mPhoneNumberHelper = phoneNumberHelper;
+ mResources = resources;
+ }
+
+ /**
+ * Sets the name, label, and number for a contact.
+ *
+ * @param views the views to populate
+ * @param details the details of a phone call needed to fill in the data
+ * @param isHighlighted whether to use the highlight text for the call
+ */
+ public void setPhoneCallDetails(CallLogListItemViews views, PhoneCallDetails details,
+ boolean isHighlighted) {
+ mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details,
+ isHighlighted);
+ boolean canCall = mPhoneNumberHelper.canPlaceCallsTo(details.number);
+ boolean canPlay = details.callTypes[0] == Calls.VOICEMAIL_TYPE;
+
+ if (canPlay) {
+ // Playback action takes preference.
+ configurePlaySecondaryAction(views, isHighlighted);
+ views.dividerView.setVisibility(View.VISIBLE);
+ } else if (canCall) {
+ // Call is the secondary action.
+ configureCallSecondaryAction(views, details);
+ views.dividerView.setVisibility(View.VISIBLE);
+ } else {
+ // No action available.
+ views.secondaryActionView.setVisibility(View.GONE);
+ views.dividerView.setVisibility(View.GONE);
+ }
+ }
+
+ /** Sets the secondary action to correspond to the call button. */
+ private void configureCallSecondaryAction(CallLogListItemViews views,
+ PhoneCallDetails details) {
+ views.secondaryActionView.setVisibility(View.VISIBLE);
+ views.secondaryActionView.setImageResource(R.drawable.ic_ab_dialer_holo_dark);
+ views.secondaryActionView.setContentDescription(getCallActionDescription(details));
+ }
+
+ /** Returns the description used by the call action for this phone call. */
+ private CharSequence getCallActionDescription(PhoneCallDetails details) {
+ final CharSequence recipient;
+ if (!TextUtils.isEmpty(details.name)) {
+ recipient = details.name;
+ } else {
+ recipient = mPhoneNumberHelper.getDisplayNumber(
+ details.number, details.formattedNumber);
+ }
+ return mResources.getString(R.string.description_call, recipient);
+ }
+
+ /** Sets the secondary action to correspond to the play button. */
+ private void configurePlaySecondaryAction(CallLogListItemViews views, boolean isHighlighted) {
+ views.secondaryActionView.setVisibility(View.VISIBLE);
+ views.secondaryActionView.setImageResource(
+ isHighlighted ? R.drawable.ic_play_active_holo_dark : R.drawable.ic_play_holo_dark);
+ views.secondaryActionView.setContentDescription(
+ mResources.getString(R.string.description_call_log_play_button));
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemView.java b/src/com/android/dialer/calllog/CallLogListItemView.java
new file mode 100644
index 000000000..113b02a5b
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemView.java
@@ -0,0 +1,46 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+/**
+ * An entry in the call log.
+ */
+public class CallLogListItemView extends LinearLayout {
+ public CallLogListItemView(Context context) {
+ super(context);
+ }
+
+ public CallLogListItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CallLogListItemView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void requestLayout() {
+ // We will assume that once measured this will not need to resize
+ // itself, so there is no need to pass the layout request to the parent
+ // view (ListView).
+ forceLayout();
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java
new file mode 100644
index 000000000..5b860efcb
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemViews.java
@@ -0,0 +1,83 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.PhoneCallDetailsViews;
+import com.android.contacts.R;
+import com.android.contacts.test.NeededForTesting;
+
+/**
+ * Simple value object containing the various views within a call log entry.
+ */
+public final class CallLogListItemViews {
+ /** The quick contact badge for the contact. */
+ public final QuickContactBadge quickContactView;
+ /** The primary action view of the entry. */
+ public final View primaryActionView;
+ /** The secondary action button on the entry. */
+ public final ImageView secondaryActionView;
+ /** The divider between the primary and secondary actions. */
+ public final View dividerView;
+ /** The details of the phone call. */
+ public final PhoneCallDetailsViews phoneCallDetailsViews;
+ /** The text of the header of a section. */
+ public final TextView listHeaderTextView;
+ /** The divider to be shown below items. */
+ public final View bottomDivider;
+
+ private CallLogListItemViews(QuickContactBadge quickContactView, View primaryActionView,
+ ImageView secondaryActionView, View dividerView,
+ PhoneCallDetailsViews phoneCallDetailsViews,
+ TextView listHeaderTextView, View bottomDivider) {
+ this.quickContactView = quickContactView;
+ this.primaryActionView = primaryActionView;
+ this.secondaryActionView = secondaryActionView;
+ this.dividerView = dividerView;
+ this.phoneCallDetailsViews = phoneCallDetailsViews;
+ this.listHeaderTextView = listHeaderTextView;
+ this.bottomDivider = bottomDivider;
+ }
+
+ public static CallLogListItemViews fromView(View view) {
+ return new CallLogListItemViews(
+ (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+ view.findViewById(R.id.primary_action_view),
+ (ImageView) view.findViewById(R.id.secondary_action_icon),
+ view.findViewById(R.id.divider),
+ PhoneCallDetailsViews.fromView(view),
+ (TextView) view.findViewById(R.id.call_log_header),
+ view.findViewById(R.id.call_log_divider));
+ }
+
+ @NeededForTesting
+ public static CallLogListItemViews createForTest(Context context) {
+ return new CallLogListItemViews(
+ new QuickContactBadge(context),
+ new View(context),
+ new ImageView(context),
+ new View(context),
+ PhoneCallDetailsViews.createForTest(context),
+ new TextView(context),
+ new View(context));
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsService.java b/src/com/android/dialer/calllog/CallLogNotificationsService.java
new file mode 100644
index 000000000..3270963cc
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogNotificationsService.java
@@ -0,0 +1,82 @@
+/*
+ * 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.dialer.calllog;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Provides operations for managing notifications.
+ * <p>
+ * It handles the following actions:
+ * <ul>
+ * <li>{@link #ACTION_MARK_NEW_VOICEMAILS_AS_OLD}: marks all the new voicemails in the call log as
+ * old; this is called when a notification is dismissed.</li>
+ * <li>{@link #ACTION_UPDATE_NOTIFICATIONS}: updates the content of the new items notification; it
+ * may include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}, containing the URI of the new
+ * voicemail that has triggered this update (if any).</li>
+ * </ul>
+ */
+public class CallLogNotificationsService extends IntentService {
+ private static final String TAG = "CallLogNotificationsService";
+
+ /** Action to mark all the new voicemails as old. */
+ public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD =
+ "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
+
+ /**
+ * Action to update the notifications.
+ * <p>
+ * May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
+ */
+ public static final String ACTION_UPDATE_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_NOTIFICATIONS";
+
+ /**
+ * Extra to included with {@link #ACTION_UPDATE_NOTIFICATIONS} to identify the new voicemail
+ * that triggered an update.
+ * <p>
+ * It must be a {@link Uri}.
+ */
+ public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
+
+ private CallLogQueryHandler mCallLogQueryHandler;
+
+ public CallLogNotificationsService() {
+ super("CallLogNotificationsService");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mCallLogQueryHandler = new CallLogQueryHandler(getContentResolver(), null /*listener*/);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (ACTION_MARK_NEW_VOICEMAILS_AS_OLD.equals(intent.getAction())) {
+ mCallLogQueryHandler.markNewVoicemailsAsOld();
+ } else if (ACTION_UPDATE_NOTIFICATIONS.equals(intent.getAction())) {
+ Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+ DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+ } else {
+ Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+ }
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogQuery.java b/src/com/android/dialer/calllog/CallLogQuery.java
new file mode 100644
index 000000000..5f7b27b93
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogQuery.java
@@ -0,0 +1,103 @@
+/*
+ * 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.dialer.calllog;
+
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+
+/**
+ * The query for the call log table.
+ */
+public final class CallLogQuery {
+ // If you alter this, you must also alter the method that inserts a fake row to the headers
+ // in the CallLogQueryHandler class called createHeaderCursorFor().
+ public static final String[] _PROJECTION = new String[] {
+ Calls._ID, // 0
+ Calls.NUMBER, // 1
+ Calls.DATE, // 2
+ Calls.DURATION, // 3
+ Calls.TYPE, // 4
+ Calls.COUNTRY_ISO, // 5
+ Calls.VOICEMAIL_URI, // 6
+ Calls.GEOCODED_LOCATION, // 7
+ Calls.CACHED_NAME, // 8
+ Calls.CACHED_NUMBER_TYPE, // 9
+ Calls.CACHED_NUMBER_LABEL, // 10
+ Calls.CACHED_LOOKUP_URI, // 11
+ Calls.CACHED_MATCHED_NUMBER, // 12
+ Calls.CACHED_NORMALIZED_NUMBER, // 13
+ Calls.CACHED_PHOTO_ID, // 14
+ Calls.CACHED_FORMATTED_NUMBER, // 15
+ Calls.IS_READ, // 16
+ };
+
+ 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 COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
+ public static final int GEOCODED_LOCATION = 7;
+ public static final int CACHED_NAME = 8;
+ public static final int CACHED_NUMBER_TYPE = 9;
+ public static final int CACHED_NUMBER_LABEL = 10;
+ public static final int CACHED_LOOKUP_URI = 11;
+ public static final int CACHED_MATCHED_NUMBER = 12;
+ public static final int CACHED_NORMALIZED_NUMBER = 13;
+ public static final int CACHED_PHOTO_ID = 14;
+ public static final int CACHED_FORMATTED_NUMBER = 15;
+ public static final int IS_READ = 16;
+ /** The index of the synthetic "section" column in the extended projection. */
+ public static final int SECTION = 17;
+
+ /**
+ * The name of the synthetic "section" column.
+ * <p>
+ * This column identifies whether a row is a header or an actual item, and whether it is
+ * part of the new or old calls.
+ */
+ public static final String SECTION_NAME = "section";
+ /** The value of the "section" column for the header of the new section. */
+ public static final int SECTION_NEW_HEADER = 0;
+ /** The value of the "section" column for the items of the new section. */
+ public static final int SECTION_NEW_ITEM = 1;
+ /** The value of the "section" column for the header of the old section. */
+ public static final int SECTION_OLD_HEADER = 2;
+ /** The value of the "section" column for the items of the old section. */
+ public static final int SECTION_OLD_ITEM = 3;
+
+ /** The call log projection including the section name. */
+ public static final String[] EXTENDED_PROJECTION;
+ static {
+ EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
+ System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
+ EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
+ }
+
+ public static boolean isSectionHeader(Cursor cursor) {
+ int section = cursor.getInt(CallLogQuery.SECTION);
+ return section == CallLogQuery.SECTION_NEW_HEADER
+ || section == CallLogQuery.SECTION_OLD_HEADER;
+ }
+
+ public static boolean isNewSection(Cursor cursor) {
+ int section = cursor.getInt(CallLogQuery.SECTION);
+ return section == CallLogQuery.SECTION_NEW_ITEM
+ || section == CallLogQuery.SECTION_NEW_HEADER;
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
new file mode 100644
index 000000000..2e67e5a01
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -0,0 +1,364 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract.Status;
+import android.util.Log;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+import com.google.common.collect.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/** Handles asynchronous queries to the call log. */
+/*package*/ class CallLogQueryHandler extends AsyncQueryHandler {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final String TAG = "CallLogQueryHandler";
+ private static final int NUM_LOGS_TO_DISPLAY = 1000;
+
+ /** The token for the query to fetch the new entries from the call log. */
+ private static final int QUERY_NEW_CALLS_TOKEN = 53;
+ /** The token for the query to fetch the old entries from the call log. */
+ private static final int QUERY_OLD_CALLS_TOKEN = 54;
+ /** The token for the query to mark all missed calls as old after seeing the call log. */
+ private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
+ /** The token for the query to mark all new voicemails as old. */
+ private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
+ /** The token for the query to mark all missed calls as read after seeing the call log. */
+ private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
+ /** The token for the query to fetch voicemail status messages. */
+ private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
+
+ /**
+ * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
+ * type.
+ */
+ public static final int CALL_TYPE_ALL = -1;
+
+ /**
+ * The time window from the current time within which an unread entry will be added to the new
+ * section.
+ */
+ private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7);
+
+ private final WeakReference<Listener> mListener;
+
+ /** The cursor containing the new calls, or null if they have not yet been fetched. */
+ @GuardedBy("this") private Cursor mNewCallsCursor;
+ /** The cursor containing the old calls, or null if they have not yet been fetched. */
+ @GuardedBy("this") private Cursor mOldCallsCursor;
+ /**
+ * The identifier of the latest calls request.
+ * <p>
+ * A request for the list of calls requires two queries and hence the two cursor
+ * {@link #mNewCallsCursor} and {@link #mOldCallsCursor} above, corresponding to
+ * {@link #QUERY_NEW_CALLS_TOKEN} and {@link #QUERY_OLD_CALLS_TOKEN}.
+ * <p>
+ * When a new request is about to be started, existing cursors are closed. However, it is
+ * possible that one of the queries completes after the new request has started. This means that
+ * we might merge two cursors that do not correspond to the same request. Moreover, this may
+ * lead to a resource leak if the same query completes and we override the cursor without
+ * closing it first.
+ * <p>
+ * To make sure we only join two cursors from the same request, we use this variable to store
+ * the request id of the latest request and make sure we only process cursors corresponding to
+ * the this request.
+ */
+ @GuardedBy("this") private int mCallsRequestId;
+
+ /**
+ * 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 CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
+ super(contentResolver);
+ mListener = new WeakReference<Listener>(listener);
+ }
+
+ /** Creates a cursor that contains a single row and maps the section to the given value. */
+ private Cursor createHeaderCursorFor(int section) {
+ MatrixCursor matrixCursor =
+ new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+ // The values in this row correspond to default values for _PROJECTION from CallLogQuery
+ // plus the section value.
+ matrixCursor.addRow(new Object[]{
+ 0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0,
+ section
+ });
+ return matrixCursor;
+ }
+
+ /** Returns a cursor for the old calls header. */
+ private Cursor createOldCallsHeaderCursor() {
+ return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
+ }
+
+ /** Returns a cursor for the new calls header. */
+ private Cursor createNewCallsHeaderCursor() {
+ return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER);
+ }
+
+ /**
+ * Fetches the list of calls from the call log for a given type.
+ * <p>
+ * It will asynchronously update the content of the list view when the fetch completes.
+ */
+ public void fetchCalls(int callType) {
+ cancelFetch();
+ int requestId = newCallsRequest();
+ fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, callType);
+ fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, callType);
+ }
+
+ public void fetchVoicemailStatus() {
+ startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
+ VoicemailStatusHelperImpl.PROJECTION, null, null, null);
+ }
+
+ /** Fetches the list of calls in the call log, either the new one or the old ones. */
+ private void fetchCalls(int token, int requestId, boolean isNew, int callType) {
+ // We need to check for NULL explicitly otherwise entries with where READ is NULL
+ // may not match either the query or its negation.
+ // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
+ String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?",
+ Calls.IS_READ, Calls.IS_READ, Calls.DATE);
+ List<String> selectionArgs = Lists.newArrayList(
+ Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW));
+ if (!isNew) {
+ // Negate the query.
+ selection = String.format("NOT (%s)", selection);
+ }
+ if (callType > CALL_TYPE_ALL) {
+ // Add a clause to fetch only items of type voicemail.
+ selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
+ selectionArgs.add(Integer.toString(callType));
+ }
+ Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
+ .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(NUM_LOGS_TO_DISPLAY))
+ .build();
+ startQuery(token, requestId, uri,
+ CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
+ Calls.DEFAULT_SORT_ORDER);
+ }
+
+ /** Cancel any pending fetch request. */
+ private void cancelFetch() {
+ cancelOperation(QUERY_NEW_CALLS_TOKEN);
+ cancelOperation(QUERY_OLD_CALLS_TOKEN);
+ }
+
+ /** Updates all new calls to mark them as old. */
+ public void markNewCallsAsOld() {
+ // Mark all "new" calls as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+
+ startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+ values, where.toString(), null);
+ }
+
+ /** Updates all new voicemails to mark them as old. */
+ public void markNewVoicemailsAsOld() {
+ // Mark all "new" voicemails as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+
+ startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+ values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
+ }
+
+ /** Updates all missed calls to mark them as read. */
+ public void markMissedCallsAsRead() {
+ // Mark all "new" calls as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.IS_READ).append(" = 0");
+ where.append(" AND ");
+ where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.IS_READ, "1");
+
+ startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
+ where.toString(), null);
+ }
+
+ /**
+ * Start a new request and return its id. The request id will be used as the cookie for the
+ * background request.
+ * <p>
+ * Closes any open cursor that has not yet been sent to the requester.
+ */
+ private synchronized int newCallsRequest() {
+ MoreCloseables.closeQuietly(mNewCallsCursor);
+ MoreCloseables.closeQuietly(mOldCallsCursor);
+ mNewCallsCursor = null;
+ mOldCallsCursor = null;
+ return ++mCallsRequestId;
+ }
+
+ @Override
+ protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (token == QUERY_NEW_CALLS_TOKEN) {
+ int requestId = ((Integer) cookie).intValue();
+ if (requestId != mCallsRequestId) {
+ // Ignore this query since it does not correspond to the latest request.
+ return;
+ }
+
+ // Store the returned cursor.
+ MoreCloseables.closeQuietly(mNewCallsCursor);
+ mNewCallsCursor = new ExtendedCursor(
+ cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
+ } else if (token == QUERY_OLD_CALLS_TOKEN) {
+ int requestId = ((Integer) cookie).intValue();
+ if (requestId != mCallsRequestId) {
+ // Ignore this query since it does not correspond to the latest request.
+ return;
+ }
+
+ // Store the returned cursor.
+ MoreCloseables.closeQuietly(mOldCallsCursor);
+ mOldCallsCursor = new ExtendedCursor(
+ cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
+ } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
+ updateVoicemailStatus(cursor);
+ return;
+ } else {
+ Log.w(TAG, "Unknown query completed: ignoring: " + token);
+ return;
+ }
+
+ if (mNewCallsCursor != null && mOldCallsCursor != null) {
+ updateAdapterData(createMergedCursor());
+ }
+ }
+
+ /** Creates the merged cursor representing the data to show in the call log. */
+ @GuardedBy("this")
+ private Cursor createMergedCursor() {
+ try {
+ final boolean hasNewCalls = mNewCallsCursor.getCount() != 0;
+ final boolean hasOldCalls = mOldCallsCursor.getCount() != 0;
+
+ if (!hasNewCalls) {
+ // Return only the old calls, without the header.
+ MoreCloseables.closeQuietly(mNewCallsCursor);
+ return mOldCallsCursor;
+ }
+
+ if (!hasOldCalls) {
+ // Return only the new calls.
+ MoreCloseables.closeQuietly(mOldCallsCursor);
+ return new MergeCursor(
+ new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor });
+ }
+
+ return new MergeCursor(new Cursor[]{
+ createNewCallsHeaderCursor(), mNewCallsCursor,
+ createOldCallsHeaderCursor(), mOldCallsCursor});
+ } finally {
+ // Any cursor still open is now owned, directly or indirectly, by the caller.
+ mNewCallsCursor = null;
+ mOldCallsCursor = null;
+ }
+ }
+
+ /**
+ * Updates the adapter in the call log fragment to show the new cursor data.
+ */
+ private void updateAdapterData(Cursor combinedCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onCallsFetched(combinedCursor);
+ }
+ }
+
+ private void updateVoicemailStatus(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailStatusFetched(statusCursor);
+ }
+ }
+
+ /** Listener to completion of various queries. */
+ public interface Listener {
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
+ void onVoicemailStatusFetched(Cursor statusCursor);
+
+ /**
+ * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete.
+ */
+ void onCallsFetched(Cursor combinedCursor);
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallLogReceiver.java b/src/com/android/dialer/calllog/CallLogReceiver.java
new file mode 100644
index 000000000..97d2951c1
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogReceiver.java
@@ -0,0 +1,50 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.util.Log;
+
+/**
+ * Receiver for call log events.
+ * <p>
+ * It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and
+ * {@link Intent#ACTION_BOOT_COMPLETED}.
+ */
+public class CallLogReceiver extends BroadcastReceiver {
+ private static final String TAG = "CallLogReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ serviceIntent.putExtra(
+ CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, intent.getData());
+ context.startService(serviceIntent);
+ } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ context.startService(serviceIntent);
+ } else {
+ Log.w(TAG, "onReceive: could not handle: " + intent);
+ }
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallTypeHelper.java b/src/com/android/dialer/calllog/CallTypeHelper.java
new file mode 100644
index 000000000..255258e01
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallTypeHelper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+
+import com.android.contacts.R;
+
+/**
+ * Helper class to perform operations related to call types.
+ */
+public class CallTypeHelper {
+ /** Name used to identify incoming calls. */
+ private final CharSequence mIncomingName;
+ /** Name used to identify outgoing calls. */
+ private final CharSequence mOutgoingName;
+ /** Name used to identify missed calls. */
+ private final CharSequence mMissedName;
+ /** Name used to identify voicemail calls. */
+ private final CharSequence mVoicemailName;
+ /** Color used to identify new missed calls. */
+ private final int mNewMissedColor;
+ /** Color used to identify new voicemail calls. */
+ private final int mNewVoicemailColor;
+
+ public CallTypeHelper(Resources resources) {
+ // Cache these values so that we do not need to look them up each time.
+ mIncomingName = resources.getString(R.string.type_incoming);
+ mOutgoingName = resources.getString(R.string.type_outgoing);
+ mMissedName = resources.getString(R.string.type_missed);
+ mVoicemailName = resources.getString(R.string.type_voicemail);
+ mNewMissedColor = resources.getColor(R.color.call_log_missed_call_highlight_color);
+ mNewVoicemailColor = resources.getColor(R.color.call_log_voicemail_highlight_color);
+ }
+
+ /** Returns the text used to represent the given call type. */
+ public CharSequence getCallTypeText(int callType) {
+ switch (callType) {
+ case Calls.INCOMING_TYPE:
+ return mIncomingName;
+
+ case Calls.OUTGOING_TYPE:
+ return mOutgoingName;
+
+ case Calls.MISSED_TYPE:
+ return mMissedName;
+
+ case Calls.VOICEMAIL_TYPE:
+ return mVoicemailName;
+
+ default:
+ throw new IllegalArgumentException("invalid call type: " + callType);
+ }
+ }
+
+ /** Returns the color used to highlight the given call type, null if not highlight is needed. */
+ public Integer getHighlightedColor(int callType) {
+ switch (callType) {
+ case Calls.INCOMING_TYPE:
+ // New incoming calls are not highlighted.
+ return null;
+
+ case Calls.OUTGOING_TYPE:
+ // New outgoing calls are not highlighted.
+ return null;
+
+ case Calls.MISSED_TYPE:
+ return mNewMissedColor;
+
+ case Calls.VOICEMAIL_TYPE:
+ return mNewVoicemailColor;
+
+ default:
+ throw new IllegalArgumentException("invalid call type: " + callType);
+ }
+ }
+}
diff --git a/src/com/android/dialer/calllog/CallTypeIconsView.java b/src/com/android/dialer/calllog/CallTypeIconsView.java
new file mode 100644
index 000000000..e26d5a1bb
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallTypeIconsView.java
@@ -0,0 +1,126 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.provider.CallLog.Calls;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.contacts.R;
+import com.android.contacts.test.NeededForTesting;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * View that draws one or more symbols for different types of calls (missed calls, outgoing etc).
+ * The symbols are set up horizontally. As this view doesn't create subviews, it is better suited
+ * for ListView-recycling that a regular LinearLayout using ImageViews.
+ */
+public class CallTypeIconsView extends View {
+ private List<Integer> mCallTypes = Lists.newArrayListWithCapacity(3);
+ private Resources mResources;
+ private int mWidth;
+ private int mHeight;
+
+ public CallTypeIconsView(Context context) {
+ this(context, null);
+ }
+
+ public CallTypeIconsView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mResources = new Resources(context);
+ }
+
+ public void clear() {
+ mCallTypes.clear();
+ mWidth = 0;
+ mHeight = 0;
+ invalidate();
+ }
+
+ public void add(int callType) {
+ mCallTypes.add(callType);
+
+ final Drawable drawable = getCallTypeDrawable(callType);
+ mWidth += drawable.getIntrinsicWidth() + mResources.iconMargin;
+ mHeight = Math.max(mHeight, drawable.getIntrinsicHeight());
+ invalidate();
+ }
+
+ @NeededForTesting
+ public int getCount() {
+ return mCallTypes.size();
+ }
+
+ @NeededForTesting
+ public int getCallType(int index) {
+ return mCallTypes.get(index);
+ }
+
+ private Drawable getCallTypeDrawable(int callType) {
+ switch (callType) {
+ case Calls.INCOMING_TYPE:
+ return mResources.incoming;
+ case Calls.OUTGOING_TYPE:
+ return mResources.outgoing;
+ case Calls.MISSED_TYPE:
+ return mResources.missed;
+ case Calls.VOICEMAIL_TYPE:
+ return mResources.voicemail;
+ default:
+ throw new IllegalArgumentException("invalid call type: " + callType);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(mWidth, mHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int left = 0;
+ for (Integer callType : mCallTypes) {
+ final Drawable drawable = getCallTypeDrawable(callType);
+ final int right = left + drawable.getIntrinsicWidth();
+ drawable.setBounds(left, 0, right, drawable.getIntrinsicHeight());
+ drawable.draw(canvas);
+ left = right + mResources.iconMargin;
+ }
+ }
+
+ private static class Resources {
+ public final Drawable incoming;
+ public final Drawable outgoing;
+ public final Drawable missed;
+ public final Drawable voicemail;
+ public final int iconMargin;
+
+ public Resources(Context context) {
+ final android.content.res.Resources r = context.getResources();
+ incoming = r.getDrawable(R.drawable.ic_call_incoming_holo_dark);
+ outgoing = r.getDrawable(R.drawable.ic_call_outgoing_holo_dark);
+ missed = r.getDrawable(R.drawable.ic_call_missed_holo_dark);
+ voicemail = r.getDrawable(R.drawable.ic_call_voicemail_holo_dark);
+ iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
+ }
+ }
+}
diff --git a/src/com/android/dialer/calllog/ClearCallLogDialog.java b/src/com/android/dialer/calllog/ClearCallLogDialog.java
new file mode 100644
index 000000000..e91c08fa8
--- /dev/null
+++ b/src/com/android/dialer/calllog/ClearCallLogDialog.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+
+import com.android.contacts.R;
+
+/**
+ * Dialog that clears the call log after confirming with the user
+ */
+public class ClearCallLogDialog extends DialogFragment {
+ /** Preferred way to show this dialog */
+ public static void show(FragmentManager fragmentManager) {
+ ClearCallLogDialog dialog = new ClearCallLogDialog();
+ dialog.show(fragmentManager, "deleteCallLog");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final OnClickListener okListener = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(),
+ getString(R.string.clearCallLogProgress_title),
+ "", true, false);
+ final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ resolver.delete(Calls.CONTENT_URI, null, null);
+ return null;
+ }
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ }
+ };
+ // TODO: Once we have the API, we should configure this ProgressDialog
+ // to only show up after a certain time (e.g. 150ms)
+ progressDialog.show();
+ task.execute();
+ }
+ };
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clearCallLogConfirmation_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.clearCallLogConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, okListener)
+ .setCancelable(true)
+ .create();
+ }
+}
diff --git a/src/com/android/dialer/calllog/ContactInfo.java b/src/com/android/dialer/calllog/ContactInfo.java
new file mode 100644
index 000000000..b48adef0f
--- /dev/null
+++ b/src/com/android/dialer/calllog/ContactInfo.java
@@ -0,0 +1,71 @@
+/*
+ * 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.dialer.calllog;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.contacts.util.UriUtils;
+
+/**
+ * Information for a contact as needed by the Call Log.
+ */
+public final class ContactInfo {
+ public Uri lookupUri;
+ public String name;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ public String normalizedNumber;
+ /** The photo for the contact, if available. */
+ public long photoId;
+ /** The high-res photo for the contact, if available. */
+ public Uri photoUri;
+
+ public static ContactInfo EMPTY = new ContactInfo();
+
+ @Override
+ public int hashCode() {
+ // Uses only name and contactUri to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same lookupUri.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((lookupUri == null) ? 0 : lookupUri.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ ContactInfo other = (ContactInfo) obj;
+ if (!UriUtils.areEqual(lookupUri, other.lookupUri)) return false;
+ if (!TextUtils.equals(name, other.name)) return false;
+ if (type != other.type) return false;
+ if (!TextUtils.equals(label, other.label)) return false;
+ if (!TextUtils.equals(number, other.number)) return false;
+ if (!TextUtils.equals(formattedNumber, other.formattedNumber)) return false;
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+ if (photoId != other.photoId) return false;
+ if (!UriUtils.areEqual(photoUri, other.photoUri)) return false;
+ return true;
+ }
+}
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
new file mode 100644
index 000000000..b6f0662f2
--- /dev/null
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -0,0 +1,215 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.util.UriUtils;
+
+/**
+ * Utility class to look up the contact information for a given number.
+ */
+public class ContactInfoHelper {
+ private final Context mContext;
+ private final String mCurrentCountryIso;
+
+ public ContactInfoHelper(Context context, String currentCountryIso) {
+ mContext = context;
+ mCurrentCountryIso = currentCountryIso;
+ }
+
+ /**
+ * Returns the contact information for the given number.
+ * <p>
+ * If the number does not match any contact, returns a contact info containing only the number
+ * and the formatted number.
+ * <p>
+ * If an error occurs during the lookup, it returns null.
+ *
+ * @param number the number to look up
+ * @param countryIso the country associated with this number
+ */
+ public ContactInfo lookupNumber(String number, String countryIso) {
+ final ContactInfo info;
+
+ // Determine the contact info.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // This "number" is really a SIP address.
+ ContactInfo sipInfo = queryContactInfoForSipAddress(number);
+ if (sipInfo == null || sipInfo == ContactInfo.EMPTY) {
+ // Check whether the "username" part of the SIP address is
+ // actually the phone number of a contact.
+ String username = PhoneNumberUtils.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ sipInfo = queryContactInfoForPhoneNumber(username, countryIso);
+ }
+ }
+ info = sipInfo;
+ } else {
+ // Look for a contact that has the given phone number.
+ ContactInfo phoneInfo = queryContactInfoForPhoneNumber(number, countryIso);
+
+ if (phoneInfo == null || phoneInfo == ContactInfo.EMPTY) {
+ // Check whether the phone number has been saved as an "Internet call" number.
+ phoneInfo = queryContactInfoForSipAddress(number);
+ }
+ info = phoneInfo;
+ }
+
+ final ContactInfo updatedInfo;
+ if (info == null) {
+ // The lookup failed.
+ updatedInfo = null;
+ } else {
+ // If we did not find a matching contact, generate an empty contact info for the number.
+ if (info == ContactInfo.EMPTY) {
+ // Did not find a matching contact.
+ updatedInfo = new ContactInfo();
+ updatedInfo.number = number;
+ updatedInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ } else {
+ updatedInfo = info;
+ }
+ }
+ return updatedInfo;
+ }
+
+ /**
+ * Looks up a contact using the given URI.
+ * <p>
+ * It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+ * found, or the {@link ContactInfo} for the given contact.
+ * <p>
+ * The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+ * value.
+ */
+ private ContactInfo lookupContactFromUri(Uri uri) {
+ final ContactInfo info;
+ Cursor phonesCursor =
+ mContext.getContentResolver().query(
+ uri, PhoneQuery._PROJECTION, null, null, null);
+
+ if (phonesCursor != null) {
+ try {
+ if (phonesCursor.moveToFirst()) {
+ info = new ContactInfo();
+ long contactId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+ String lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+ info.lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+ 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);
+ info.photoUri =
+ UriUtils.parseUriOrNull(phonesCursor.getString(PhoneQuery.PHOTO_URI));
+ info.formattedNumber = null;
+ } else {
+ info = ContactInfo.EMPTY;
+ }
+ } finally {
+ phonesCursor.close();
+ }
+ } else {
+ // Failed to fetch the data, ignore this request.
+ info = null;
+ }
+ return info;
+ }
+
+ /**
+ * Determines the contact information for the given SIP address.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+ final ContactInfo info;
+
+ // "contactNumber" is a SIP address, so use the PhoneLookup table with the SIP parameter.
+ Uri.Builder uriBuilder = PhoneLookup.CONTENT_FILTER_URI.buildUpon();
+ uriBuilder.appendPath(Uri.encode(sipAddress));
+ uriBuilder.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, "1");
+ return lookupContactFromUri(uriBuilder.build());
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso) {
+ String contactNumber = number;
+ if (!TextUtils.isEmpty(countryIso)) {
+ // Normalize the number: this is needed because the PhoneLookup query below does not
+ // accept a country code as an input.
+ String numberE164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (!TextUtils.isEmpty(numberE164)) {
+ // Only use it if the number could be formatted to E164.
+ contactNumber = numberE164;
+ }
+ }
+
+ // The "contactNumber" is a regular phone number, so use the PhoneLookup table.
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(contactNumber));
+ ContactInfo info = lookupContactFromUri(uri);
+ if (info != null && info != ContactInfo.EMPTY) {
+ info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ }
+ return info;
+ }
+
+ /**
+ * 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);
+ }
+}
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
new file mode 100644
index 000000000..0f6fe3b08
--- /dev/null
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -0,0 +1,340 @@
+/*
+ * 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.dialer.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.CallDetailActivity;
+import com.android.contacts.R;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * Implementation of {@link VoicemailNotifier} that shows a notification in the
+ * status bar.
+ */
+public class DefaultVoicemailNotifier implements VoicemailNotifier {
+ public static final String TAG = "DefaultVoicemailNotifier";
+
+ /** The tag used to identify notifications from this class. */
+ private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
+ /** The identifier of the notification of new voicemails. */
+ private static final int NOTIFICATION_ID = 1;
+
+ /** The singleton instance of {@link DefaultVoicemailNotifier}. */
+ private static DefaultVoicemailNotifier sInstance;
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ private final NewCallsQuery mNewCallsQuery;
+ private final NameLookupQuery mNameLookupQuery;
+ private final PhoneNumberHelper mPhoneNumberHelper;
+
+ /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
+ public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
+ if (sInstance == null) {
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ ContentResolver contentResolver = context.getContentResolver();
+ sInstance = new DefaultVoicemailNotifier(context, notificationManager,
+ createNewCallsQuery(contentResolver),
+ createNameLookupQuery(contentResolver),
+ createPhoneNumberHelper(context));
+ }
+ return sInstance;
+ }
+
+ private DefaultVoicemailNotifier(Context context,
+ NotificationManager notificationManager, NewCallsQuery newCallsQuery,
+ NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper) {
+ mContext = context;
+ mNotificationManager = notificationManager;
+ mNewCallsQuery = newCallsQuery;
+ mNameLookupQuery = nameLookupQuery;
+ mPhoneNumberHelper = phoneNumberHelper;
+ }
+
+ /** Updates the notification and notifies of the call with the given URI. */
+ @Override
+ public void updateNotification(Uri newCallUri) {
+ // Lookup the list of new voicemails to include in the notification.
+ // TODO: Move this into a service, to avoid holding the receiver up.
+ final NewCall[] newCalls = mNewCallsQuery.query();
+
+ if (newCalls == null) {
+ // Query failed, just return.
+ return;
+ }
+
+ if (newCalls.length == 0) {
+ // No voicemails to notify about: clear the notification.
+ clearNotification();
+ return;
+ }
+
+ Resources resources = mContext.getResources();
+
+ // This represents a list of names to include in the notification.
+ String callers = null;
+
+ // Maps each number into a name: if a number is in the map, it has already left a more
+ // recent voicemail.
+ final Map<String, String> names = Maps.newHashMap();
+
+ // Determine the call corresponding to the new voicemail we have to notify about.
+ NewCall callToNotify = null;
+
+ // Iterate over the new voicemails to determine all the information above.
+ for (NewCall newCall : newCalls) {
+ // Check if we already know the name associated with this number.
+ String name = names.get(newCall.number);
+ if (name == null) {
+ // Look it up in the database.
+ name = mNameLookupQuery.query(newCall.number);
+ // If we cannot lookup the contact, use the number instead.
+ if (name == null) {
+ name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString();
+ if (TextUtils.isEmpty(name)) {
+ name = newCall.number;
+ }
+ }
+ names.put(newCall.number, name);
+ // This is a new caller. Add it to the back of the list of callers.
+ if (TextUtils.isEmpty(callers)) {
+ callers = name;
+ } else {
+ callers = resources.getString(
+ R.string.notification_voicemail_callers_list, callers, name);
+ }
+ }
+ // Check if this is the new call we need to notify about.
+ if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) {
+ callToNotify = newCall;
+ }
+ }
+
+ if (newCallUri != null && callToNotify == null) {
+ Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
+ }
+
+ // Determine the title of the notification and the icon for it.
+ final String title = resources.getQuantityString(
+ R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
+ // TODO: Use the photo of contact if all calls are from the same person.
+ final int icon = android.R.drawable.stat_notify_voicemail;
+
+ Notification.Builder notificationBuilder = new Notification.Builder(mContext)
+ .setSmallIcon(icon)
+ .setContentTitle(title)
+ .setContentText(callers)
+ .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
+ .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
+ .setAutoCancel(true);
+
+ // Determine the intent to fire when the notification is clicked on.
+ final Intent contentIntent;
+ if (newCalls.length == 1) {
+ // Open the voicemail directly.
+ contentIntent = new Intent(mContext, CallDetailActivity.class);
+ contentIntent.setData(newCalls[0].callsUri);
+ contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ newCalls[0].voicemailUri);
+ Intent playIntent = new Intent(mContext, CallDetailActivity.class);
+ playIntent.setData(newCalls[0].callsUri);
+ playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ newCalls[0].voicemailUri);
+ playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+ playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true);
+ notificationBuilder.addAction(R.drawable.ic_play_holo_dark,
+ resources.getString(R.string.notification_action_voicemail_play),
+ PendingIntent.getActivity(mContext, 0, playIntent, 0));
+ } else {
+ // Open the call log.
+ contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
+ }
+ notificationBuilder.setContentIntent(
+ PendingIntent.getActivity(mContext, 0, contentIntent, 0));
+
+ // The text to show in the ticker, describing the new event.
+ if (callToNotify != null) {
+ notificationBuilder.setTicker(resources.getString(
+ R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
+ }
+
+ mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ /** Creates a pending intent that marks all new voicemails as old. */
+ private PendingIntent createMarkNewVoicemailsAsOldIntent() {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ @Override
+ public void clearNotification() {
+ mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ }
+
+ /** Information about a new voicemail. */
+ private static final class NewCall {
+ public final Uri callsUri;
+ public final Uri voicemailUri;
+ public final String number;
+
+ public NewCall(Uri callsUri, Uri voicemailUri, String number) {
+ this.callsUri = callsUri;
+ this.voicemailUri = voicemailUri;
+ this.number = number;
+ }
+ }
+
+ /** Allows determining the new calls for which a notification should be generated. */
+ public interface NewCallsQuery {
+ /**
+ * Returns the new calls for which a notification should be generated.
+ */
+ public NewCall[] query();
+ }
+
+ /** Create a new instance of {@link NewCallsQuery}. */
+ public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) {
+ return new DefaultNewCallsQuery(contentResolver);
+ }
+
+ /**
+ * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
+ * notify about in the call log.
+ */
+ private static final class DefaultNewCallsQuery implements NewCallsQuery {
+ private static final String[] PROJECTION = {
+ Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI
+ };
+ private static final int ID_COLUMN_INDEX = 0;
+ private static final int NUMBER_COLUMN_INDEX = 1;
+ private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
+
+ private final ContentResolver mContentResolver;
+
+ private DefaultNewCallsQuery(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ public NewCall[] query() {
+ final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+ final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
+ Cursor cursor = null;
+ try {
+ cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
+ selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
+ if (cursor == null) {
+ return null;
+ }
+ NewCall[] newCalls = new NewCall[cursor.getCount()];
+ while (cursor.moveToNext()) {
+ newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
+ }
+ return newCalls;
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ }
+
+ /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
+ private NewCall createNewCallsFromCursor(Cursor cursor) {
+ String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
+ Uri callsUri = ContentUris.withAppendedId(
+ Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
+ Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
+ return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX));
+ }
+ }
+
+ /** Allows determining the name associated with a given phone number. */
+ public interface NameLookupQuery {
+ /**
+ * Returns the name associated with the given number in the contacts database, or null if
+ * the number does not correspond to any of the contacts.
+ * <p>
+ * If there are multiple contacts with the same phone number, it will return the name of one
+ * of the matching contacts.
+ */
+ public String query(String number);
+ }
+
+ /** Create a new instance of {@link NameLookupQuery}. */
+ public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) {
+ return new DefaultNameLookupQuery(contentResolver);
+ }
+
+ /**
+ * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
+ * contacts database.
+ */
+ private static final class DefaultNameLookupQuery implements NameLookupQuery {
+ private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
+ private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
+
+ private final ContentResolver mContentResolver;
+
+ private DefaultNameLookupQuery(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ public String query(String number) {
+ Cursor cursor = null;
+ try {
+ cursor = mContentResolver.query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+ PROJECTION, null, null, null);
+ if (cursor == null || !cursor.moveToFirst()) return null;
+ return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new PhoneNumberHelper.
+ * <p>
+ * This will cause some Disk I/O, at least the first time it is created, so it should not be
+ * called from the main thread.
+ */
+ public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
+ return new PhoneNumberHelper(context.getResources());
+ }
+}
diff --git a/src/com/android/dialer/calllog/ExtendedCursor.java b/src/com/android/dialer/calllog/ExtendedCursor.java
new file mode 100644
index 000000000..3e55aabe8
--- /dev/null
+++ b/src/com/android/dialer/calllog/ExtendedCursor.java
@@ -0,0 +1,154 @@
+/*
+ * 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.dialer.calllog;
+
+import android.database.AbstractCursor;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+
+import com.android.common.io.MoreCloseables;
+
+/**
+ * Wraps a cursor to add an additional column with the same value for all rows.
+ * <p>
+ * The number of rows in the cursor and the set of columns is determined by the cursor being
+ * wrapped.
+ */
+public class ExtendedCursor extends AbstractCursor {
+ /** The cursor to wrap. */
+ private final Cursor mCursor;
+ /** The name of the additional column. */
+ private final String mColumnName;
+ /** The value to be assigned to the additional column. */
+ private final Object mValue;
+
+ /**
+ * Creates a new cursor which extends the given cursor by adding a column with a constant value.
+ *
+ * @param cursor the cursor to extend
+ * @param columnName the name of the additional column
+ * @param value the value to be assigned to the additional column
+ */
+ public ExtendedCursor(Cursor cursor, String columnName, Object value) {
+ mCursor = cursor;
+ mColumnName = columnName;
+ mValue = value;
+ }
+
+ @Override
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ String[] columnNames = mCursor.getColumnNames();
+ int length = columnNames.length;
+ String[] extendedColumnNames = new String[length + 1];
+ System.arraycopy(columnNames, 0, extendedColumnNames, 0, length);
+ extendedColumnNames[length] = mColumnName;
+ return extendedColumnNames;
+ }
+
+ @Override
+ public String getString(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (String) mValue;
+ }
+ return mCursor.getString(column);
+ }
+
+ @Override
+ public short getShort(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (Short) mValue;
+ }
+ return mCursor.getShort(column);
+ }
+
+ @Override
+ public int getInt(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (Integer) mValue;
+ }
+ return mCursor.getInt(column);
+ }
+
+ @Override
+ public long getLong(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (Long) mValue;
+ }
+ return mCursor.getLong(column);
+ }
+
+ @Override
+ public float getFloat(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (Float) mValue;
+ }
+ return mCursor.getFloat(column);
+ }
+
+ @Override
+ public double getDouble(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return (Double) mValue;
+ }
+ return mCursor.getDouble(column);
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ if (column == mCursor.getColumnCount()) {
+ return mValue == null;
+ }
+ return mCursor.isNull(column);
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mCursor.moveToPosition(newPosition);
+ }
+
+ @Override
+ public void close() {
+ MoreCloseables.closeQuietly(mCursor);
+ super.close();
+ }
+
+ @Override
+ public void registerContentObserver(ContentObserver observer) {
+ mCursor.registerContentObserver(observer);
+ }
+
+ @Override
+ public void unregisterContentObserver(ContentObserver observer) {
+ mCursor.unregisterContentObserver(observer);
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mCursor.registerDataSetObserver(observer);
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mCursor.unregisterDataSetObserver(observer);
+ }
+}
diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java
new file mode 100644
index 000000000..f43dc5104
--- /dev/null
+++ b/src/com/android/dialer/calllog/IntentProvider.java
@@ -0,0 +1,102 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+
+import com.android.dialer.CallDetailActivity;
+import com.android.contacts.ContactsUtils;
+
+/**
+ * Used to create an intent to attach to an action in the call log.
+ * <p>
+ * The intent is constructed lazily with the given information.
+ */
+public abstract class IntentProvider {
+ public abstract Intent getIntent(Context context);
+
+ public static IntentProvider getReturnCallIntentProvider(final String number) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return ContactsUtils.getCallIntent(number);
+ }
+ };
+ }
+
+ public static IntentProvider getPlayVoicemailIntentProvider(final long rowId,
+ final String voicemailUri) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ Intent intent = new Intent(context, CallDetailActivity.class);
+ intent.setData(ContentUris.withAppendedId(
+ Calls.CONTENT_URI_WITH_VOICEMAIL, rowId));
+ if (voicemailUri != null) {
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ Uri.parse(voicemailUri));
+ }
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+ return intent;
+ }
+ };
+ }
+
+ public static IntentProvider getCallDetailIntentProvider(
+ final CallLogAdapter adapter, final int position, final long id, final int groupSize) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ Cursor cursor = adapter.getCursor();
+ cursor.moveToPosition(position);
+ if (CallLogQuery.isSectionHeader(cursor)) {
+ // Do nothing when a header is clicked.
+ return null;
+ }
+ Intent intent = new Intent(context, CallDetailActivity.class);
+ // Check if the first item is a voicemail.
+ String voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
+ if (voicemailUri != null) {
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+ Uri.parse(voicemailUri));
+ }
+ intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false);
+
+ if (groupSize > 1) {
+ // We want to restore the position in the cursor at the end.
+ long[] ids = new long[groupSize];
+ // Copy the ids of the rows in the group.
+ for (int index = 0; index < groupSize; ++index) {
+ ids[index] = cursor.getLong(CallLogQuery.ID);
+ cursor.moveToNext();
+ }
+ intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, ids);
+ } else {
+ // If there is a single item, use the direct URI for it.
+ intent.setData(ContentUris.withAppendedId(
+ Calls.CONTENT_URI_WITH_VOICEMAIL, id));
+ }
+ return intent;
+ }
+ };
+ }
+}
diff --git a/src/com/android/dialer/calllog/PhoneNumberHelper.java b/src/com/android/dialer/calllog/PhoneNumberHelper.java
new file mode 100644
index 000000000..70505eebc
--- /dev/null
+++ b/src/com/android/dialer/calllog/PhoneNumberHelper.java
@@ -0,0 +1,93 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.R;
+import com.android.internal.telephony.CallerInfo;
+
+/**
+ * Helper for formatting and managing phone numbers.
+ */
+public class PhoneNumberHelper {
+ private final Resources mResources;
+
+ public PhoneNumberHelper(Resources resources) {
+ mResources = resources;
+ }
+
+ /** Returns true if it is possible to place a call to the given number. */
+ public boolean canPlaceCallsTo(CharSequence number) {
+ return !(TextUtils.isEmpty(number)
+ || number.equals(CallerInfo.UNKNOWN_NUMBER)
+ || number.equals(CallerInfo.PRIVATE_NUMBER)
+ || number.equals(CallerInfo.PAYPHONE_NUMBER));
+ }
+
+ /** Returns true if it is possible to send an SMS to the given number. */
+ public boolean canSendSmsTo(CharSequence number) {
+ return canPlaceCallsTo(number) && !isVoicemailNumber(number) && !isSipNumber(number);
+ }
+
+ /**
+ * Returns the string to display for the given phone number.
+ *
+ * @param number the number to display
+ * @param formattedNumber the formatted number if available, may be null
+ */
+ public CharSequence getDisplayNumber(CharSequence number, CharSequence formattedNumber) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
+ return mResources.getString(R.string.unknown);
+ }
+ if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
+ return mResources.getString(R.string.private_num);
+ }
+ if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+ return mResources.getString(R.string.payphone);
+ }
+ if (isVoicemailNumber(number)) {
+ return mResources.getString(R.string.voicemail);
+ }
+ if (TextUtils.isEmpty(formattedNumber)) {
+ return number;
+ } else {
+ return formattedNumber;
+ }
+ }
+
+ /**
+ * Returns true if the given number is the number of the configured voicemail.
+ * To be able to mock-out this, it is not a static method.
+ */
+ public boolean isVoicemailNumber(CharSequence number) {
+ return PhoneNumberUtils.isVoiceMailNumber(number.toString());
+ }
+
+ /**
+ * Returns true if the given number is a SIP address.
+ * To be able to mock-out this, it is not a static method.
+ */
+ public boolean isSipNumber(CharSequence number) {
+ return PhoneNumberUtils.isUriNumber(number.toString());
+ }
+}
diff --git a/src/com/android/dialer/calllog/PhoneQuery.java b/src/com/android/dialer/calllog/PhoneQuery.java
new file mode 100644
index 000000000..719052204
--- /dev/null
+++ b/src/com/android/dialer/calllog/PhoneQuery.java
@@ -0,0 +1,45 @@
+/*
+ * 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.dialer.calllog;
+
+import android.provider.ContactsContract.PhoneLookup;
+
+/**
+ * The query to look up the {@link ContactInfo} for a given number in the Call Log.
+ */
+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,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI};
+
+ 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;
+ public static final int LOOKUP_KEY = 7;
+ public static final int PHOTO_URI = 8;
+}
diff --git a/src/com/android/dialer/calllog/VoicemailNotifier.java b/src/com/android/dialer/calllog/VoicemailNotifier.java
new file mode 100644
index 000000000..d433cf7f6
--- /dev/null
+++ b/src/com/android/dialer/calllog/VoicemailNotifier.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dialer.calllog;
+
+import android.net.Uri;
+
+/**
+ * Handles notifications for voicemails.
+ */
+public interface VoicemailNotifier {
+ /**
+ * Updates the notification and clears it if there are no new voicemails.
+ * <p>
+ * If the given URI corresponds to a new voicemail, also notifies about it.
+ * <p>
+ * It is not safe to call this method from the main thread.
+ *
+ * @param newCallUri URI of the new call, may be null
+ */
+ public void updateNotification(Uri newCallUri);
+
+ /** Clears the new voicemail notification. */
+ public void clearNotification();
+}