summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/calllog
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dialer/calllog')
-rw-r--r--src/com/android/dialer/calllog/CallDetailHistoryAdapter.java28
-rw-r--r--src/com/android/dialer/calllog/CallLogActivity.java54
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapter.java411
-rw-r--r--src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java245
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java187
-rw-r--r--src/com/android/dialer/calllog/CallLogGroupBuilder.java190
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemHelper.java51
-rw-r--r--src/com/android/dialer/calllog/CallLogListItemViewHolder.java354
-rw-r--r--src/com/android/dialer/calllog/CallLogNotificationsHelper.java314
-rw-r--r--src/com/android/dialer/calllog/CallLogNotificationsService.java129
-rw-r--r--src/com/android/dialer/calllog/CallLogQuery.java42
-rw-r--r--src/com/android/dialer/calllog/CallLogQueryHandler.java117
-rw-r--r--src/com/android/dialer/calllog/CallTypeHelper.java35
-rw-r--r--src/com/android/dialer/calllog/CallTypeIconsView.java62
-rw-r--r--src/com/android/dialer/calllog/ContactInfo.java25
-rw-r--r--src/com/android/dialer/calllog/ContactInfoHelper.java211
-rw-r--r--src/com/android/dialer/calllog/DefaultVoicemailNotifier.java328
-rw-r--r--src/com/android/dialer/calllog/GroupingListAdapter.java374
-rw-r--r--src/com/android/dialer/calllog/IntentProvider.java21
-rw-r--r--src/com/android/dialer/calllog/MissedCallNotificationReceiver.java53
-rw-r--r--src/com/android/dialer/calllog/MissedCallNotifier.java286
-rw-r--r--src/com/android/dialer/calllog/PhoneAccountUtils.java45
-rw-r--r--src/com/android/dialer/calllog/PhoneCallDetailsHelper.java174
-rw-r--r--src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java7
-rw-r--r--src/com/android/dialer/calllog/PhoneQuery.java59
-rw-r--r--src/com/android/dialer/calllog/PromoCardViewHolder.java42
-rw-r--r--src/com/android/dialer/calllog/ShowCallHistoryViewHolder.java46
-rw-r--r--src/com/android/dialer/calllog/VisualVoicemailCallLogFragment.java87
-rw-r--r--src/com/android/dialer/calllog/VoicemailQueryHandler.java3
-rw-r--r--src/com/android/dialer/calllog/calllogcache/CallLogCache.java96
-rw-r--r--src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipop.java73
-rw-r--r--src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipopMr1.java (renamed from src/com/android/dialer/calllog/TelecomCallLogCache.java)64
32 files changed, 2794 insertions, 1419 deletions
diff --git a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
index 3b488a8ae..ac56332ce 100644
--- a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
+++ b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
@@ -38,8 +38,6 @@ import java.util.ArrayList;
* 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;
@@ -69,53 +67,37 @@ public class CallDetailHistoryAdapter extends BaseAdapter {
@Override
public int getCount() {
- return mPhoneCallDetails.length + 1;
+ return mPhoneCallDetails.length;
}
@Override
public Object getItem(int position) {
- if (position == 0) {
- return null;
- }
- return mPhoneCallDetails[position - 1];
+ return mPhoneCallDetails[position];
}
@Override
public long getItemId(int position) {
- if (position == 0) {
- return -1;
- }
- return position - 1;
+ return position;
}
@Override
public int getViewTypeCount() {
- return 2;
+ return 1;
}
@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;
- 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];
+ PhoneCallDetails details = mPhoneCallDetails[position];
CallTypeIconsView callTypeIconView =
(CallTypeIconsView) result.findViewById(R.id.call_type_icon);
TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java
index 97e601630..1823a5bd3 100644
--- a/src/com/android/dialer/calllog/CallLogActivity.java
+++ b/src/com/android/dialer/calllog/CallLogActivity.java
@@ -15,7 +15,6 @@
*/
package com.android.dialer.calllog;
-import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -27,6 +26,7 @@ import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -39,11 +39,12 @@ import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.commonbind.analytics.AnalyticsUtil;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
+import com.android.dialer.TransactionSafeActivity;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.ScreenEvent;
import com.android.dialer.util.DialerUtils;
-import com.android.dialer.voicemail.VoicemailStatusHelper;
-import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
-public class CallLogActivity extends Activity implements ViewPager.OnPageChangeListener {
+public class CallLogActivity extends TransactionSafeActivity implements ViewPager.OnPageChangeListener {
private ViewPager mViewPager;
private ViewPagerTabs mViewPagerTabs;
private ViewPagerAdapter mViewPagerAdapter;
@@ -73,9 +74,10 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL
public Fragment getItem(int position) {
switch (getRtlPosition(position)) {
case TAB_INDEX_ALL:
- return new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL);
+ return new CallLogFragment(
+ CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */);
case TAB_INDEX_MISSED:
- return new CallLogFragment(Calls.MISSED_TYPE);
+ return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */);
}
throw new IllegalStateException("No fragment at position " + position);
}
@@ -121,7 +123,7 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL
setContentView(R.layout.call_log_activity);
getWindow().setBackgroundDrawable(null);
- final ActionBar actionBar = getActionBar();
+ final ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
@@ -186,15 +188,18 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- final Intent intent = new Intent(this, DialtactsActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(intent);
- return true;
- case R.id.delete_all:
- ClearCallLogDialog.show(getFragmentManager());
- return true;
+ if (!isSafeToCommitTransactions()) {
+ return true;
+ }
+
+ if (item.getItemId() == android.R.id.home) {
+ final Intent intent = new Intent(this, DialtactsActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ } else if (item.getItemId() == R.id.delete_all) {
+ ClearCallLogDialog.show(getFragmentManager());
+ return true;
}
return super.onOptionsItemSelected(item);
}
@@ -218,22 +223,7 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL
}
private void sendScreenViewForChildFragment(int position) {
- AnalyticsUtil.sendScreenView(CallLogFragment.class.getSimpleName(), this,
- getFragmentTagForPosition(position));
- }
-
- /**
- * Returns the fragment located at the given position in the {@link ViewPagerAdapter}. May
- * be null if the position is invalid.
- */
- private String getFragmentTagForPosition(int position) {
- switch (position) {
- case TAB_INDEX_ALL:
- return "All";
- case TAB_INDEX_MISSED:
- return "Missed";
- }
- return null;
+ Logger.logScreenView(ScreenEvent.CALL_LOG_FILTER, this);
}
private int getRtlPosition(int position) {
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 5a87bc8ce..dfb5190e1 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -16,75 +16,81 @@
package com.android.dialer.calllog;
+import com.google.common.annotations.VisibleForTesting;
+
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.support.v7.widget.RecyclerView;
import android.os.Bundle;
import android.os.Trace;
-import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.provider.CallLog;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
+import android.util.ArrayMap;
import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.ContextMenu.ContextMenuInfo;
import android.view.accessibility.AccessibilityEvent;
-import com.android.contacts.common.CallUtil;
-import com.android.contacts.common.ClipboardUtils;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.R;
+import com.android.dialer.calllog.calllogcache.CallLogCache;
import com.android.dialer.contactinfo.ContactInfoCache;
import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
-import com.android.dialer.util.DialerUtils;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.database.VoicemailArchiveContract;
+import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
+import com.android.dialer.logging.InteractionEvent;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.service.ExtendedBlockingButtonRenderer;
import com.android.dialer.util.PhoneNumberUtil;
import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
-import com.google.common.annotations.VisibleForTesting;
-
import java.util.HashMap;
+import java.util.Map;
/**
* Adapter class to fill in data for the Call Log.
*/
public class CallLogAdapter extends GroupingListAdapter
implements CallLogGroupBuilder.GroupCreator,
- VoicemailPlaybackPresenter.OnVoicemailDeletedListener {
+ VoicemailPlaybackPresenter.OnVoicemailDeletedListener,
+ ExtendedBlockingButtonRenderer.Listener {
+
+ // Types of activities the call log adapter is used for
+ public static final int ACTIVITY_TYPE_CALL_LOG = 1;
+ public static final int ACTIVITY_TYPE_ARCHIVE = 2;
+ public static final int ACTIVITY_TYPE_DIALTACTS = 3;
/** Interface used to initiate a refresh of the content. */
public interface CallFetcher {
public void fetchCalls();
}
- private static final int VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM = 10;
private static final int NO_EXPANDED_LIST_ITEM = -1;
+ // ConcurrentHashMap doesn't store null values. Use this value for numbers which aren't blocked.
+ private static final int NOT_BLOCKED = -1;
private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
- /**
- * View type for voicemail promo card. Note: Numbering starts at 20 to avoid collision
- * with {@link com.android.common.widget.GroupingListAdapter#ITEM_TYPE_IN_GROUP}, and
- * {@link CallLogAdapter#VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM}.
- */
- private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 20;
+
+ protected static final int VIEW_TYPE_NORMAL = 0;
+ private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1;
/**
* The key for the show voicemail promo card preference which will determine whether the promo
@@ -95,12 +101,14 @@ public class CallLogAdapter extends GroupingListAdapter
protected final Context mContext;
private final ContactInfoHelper mContactInfoHelper;
- private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
private final CallFetcher mCallFetcher;
+ private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+ private final Map<String, Boolean> mBlockedNumberCache = new ArrayMap<>();
protected ContactInfoCache mContactInfoCache;
- private boolean mIsShowingRecentsTab;
+ private final int mActivityType;
private static final String KEY_EXPANDED_POSITION = "expanded_position";
private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
@@ -110,6 +118,9 @@ public class CallLogAdapter extends GroupingListAdapter
// Tracks the rowId of the currently expanded list item, so the position can be updated if there
// are any changes to the call log entries, such as additions or removals.
private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+ private int mHiddenPosition = RecyclerView.NO_POSITION;
+ private Uri mHiddenItemUri = null;
+ private boolean mPendingHide = false;
/**
* Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are
@@ -123,19 +134,21 @@ public class CallLogAdapter extends GroupingListAdapter
* its day group. This hashmap provides a means of determining the previous day group without
* having to reverse the cursor to the start of the previous day call log entry.
*/
- private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>();
+ private HashMap<Long, Integer> mDayGroups = new HashMap<>();
private boolean mLoading = true;
private SharedPreferences mPrefs;
- private boolean mShowPromoCard = false;
+ private ContactsPreferences mContactsPreferences;
+
+ protected boolean mShowVoicemailPromoCard = false;
/** Instance of helper class for managing views. */
private final CallLogListItemHelper mCallLogListItemHelper;
- /** Cache for repeated requests to TelecomManager. */
- protected final TelecomCallLogCache mTelecomCallLogCache;
+ /** Cache for repeated requests to Telecom/Telephony. */
+ protected final CallLogCache mCallLogCache;
/** Helper to group call log entries. */
private final CallLogGroupBuilder mCallLogGroupBuilder;
@@ -163,6 +176,12 @@ public class CallLogAdapter extends GroupingListAdapter
mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
} else {
+ if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
+ CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
+ if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
+ ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
+ }
+ }
expandViewHolderActions(viewHolder);
}
@@ -193,68 +212,6 @@ public class CallLogAdapter extends GroupingListAdapter
}
};
- /**
- * Listener that is triggered to populate the context menu with actions to perform on the call's
- * number, when the call log entry is long pressed.
- */
- private final View.OnCreateContextMenuListener mOnCreateContextMenuListener =
- new View.OnCreateContextMenuListener() {
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v,
- ContextMenuInfo menuInfo) {
- final CallLogListItemViewHolder vh =
- (CallLogListItemViewHolder) v.getTag();
- if (TextUtils.isEmpty(vh.number)) {
- return;
- }
-
- menu.setHeaderTitle(vh.number);
-
- final MenuItem copyItem = menu.add(
- ContextMenu.NONE,
- R.id.context_menu_copy_to_clipboard,
- ContextMenu.NONE,
- R.string.copy_text);
-
- copyItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- ClipboardUtils.copyText(CallLogAdapter.this.mContext, null,
- vh.number, true);
- return true;
- }
- });
-
- // The edit number before call does not show up if any of the conditions apply:
- // 1) Number cannot be called
- // 2) Number is the voicemail number
- // 3) Number is a SIP address
-
- if (!PhoneNumberUtil.canPlaceCallsTo(vh.number, vh.numberPresentation)
- || mTelecomCallLogCache.isVoicemailNumber(vh.accountHandle, vh.number)
- || PhoneNumberUtil.isSipNumber(vh.number)) {
- return;
- }
-
- final MenuItem editItem = menu.add(
- ContextMenu.NONE,
- R.id.context_menu_edit_before_call,
- ContextMenu.NONE,
- R.string.recentCalls_editNumberBeforeCall);
-
- editItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- final Intent intent = new Intent(Intent.ACTION_DIAL,
- CallUtil.getCallUri(vh.number));
- intent.setClass(mContext, DialtactsActivity.class);
- DialerUtils.startActivityWithErrorToast(mContext, intent);
- return true;
- }
- });
- }
- };
-
private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
// If another item is expanded, notify it that it has changed. Its actions will be
// hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
@@ -279,6 +236,11 @@ public class CallLogAdapter extends GroupingListAdapter
// function on clicks causes the action views to lose the focus indicator.
CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag();
if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) {
+ if (mVoicemailPlaybackPresenter != null) {
+ // Always reset the voicemail playback state on expand.
+ mVoicemailPlaybackPresenter.resetAll();
+ }
+
expandViewHolderActions((CallLogListItemViewHolder) host.getTag());
}
}
@@ -299,7 +261,7 @@ public class CallLogAdapter extends GroupingListAdapter
CallFetcher callFetcher,
ContactInfoHelper contactInfoHelper,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
- boolean isShowingRecentsTab) {
+ int activityType) {
super(context);
mContext = context;
@@ -309,7 +271,8 @@ public class CallLogAdapter extends GroupingListAdapter
if (mVoicemailPlaybackPresenter != null) {
mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
}
- mIsShowingRecentsTab = isShowingRecentsTab;
+
+ mActivityType = activityType;
mContactInfoCache = new ContactInfoCache(
mContactInfoHelper, mOnContactInfoChangedListener);
@@ -320,13 +283,18 @@ public class CallLogAdapter extends GroupingListAdapter
Resources resources = mContext.getResources();
CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
- mTelecomCallLogCache = new TelecomCallLogCache(mContext);
+ mCallLogCache = CallLogCache.getCallLogCache(mContext);
+
PhoneCallDetailsHelper phoneCallDetailsHelper =
- new PhoneCallDetailsHelper(mContext, resources, mTelecomCallLogCache);
+ new PhoneCallDetailsHelper(mContext, resources, mCallLogCache);
mCallLogListItemHelper =
- new CallLogListItemHelper(phoneCallDetailsHelper, resources, mTelecomCallLogCache);
+ new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+ mFilteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
+
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ mContactsPreferences = new ContactsPreferences(mContext);
maybeShowVoicemailPromoCard();
}
@@ -344,6 +312,24 @@ public class CallLogAdapter extends GroupingListAdapter
}
}
+ @Override
+ public void onBlockedNumber(String number,String countryIso) {
+ String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (!TextUtils.isEmpty(cacheKey)) {
+ mBlockedNumberCache.put(cacheKey, true);
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onUnblockedNumber( String number, String countryIso) {
+ String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (!TextUtils.isEmpty(cacheKey)) {
+ mBlockedNumberCache.put(cacheKey, false);
+ notifyDataSetChanged();
+ }
+ }
+
/**
* Requery on background thread when {@link Cursor} changes.
*/
@@ -369,15 +355,25 @@ public class CallLogAdapter extends GroupingListAdapter
mContactInfoCache.invalidate();
}
- public void startCache() {
+ public void onResume() {
if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
mContactInfoCache.start();
}
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ }
+
+ public void onPause() {
+ pauseCache();
+
+ if (mHiddenItemUri != null) {
+ CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
+ }
}
- public void pauseCache() {
+ @VisibleForTesting
+ /* package */ void pauseCache() {
mContactInfoCache.stop();
- mTelecomCallLogCache.reset();
+ mCallLogCache.reset();
}
@Override
@@ -386,10 +382,13 @@ public class CallLogAdapter extends GroupingListAdapter
}
@Override
+ public void addVoicemailGroups(Cursor cursor) {
+ mCallLogGroupBuilder.addVoicemailGroups(cursor);
+ }
+
+ @Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- if (viewType == VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM) {
- return ShowCallHistoryViewHolder.create(mContext, parent);
- } else if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
+ if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
return createVoicemailPromoCardViewHolder(parent);
}
return createCallLogEntryViewHolder(parent);
@@ -407,15 +406,32 @@ public class CallLogAdapter extends GroupingListAdapter
CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create(
view,
mContext,
+ this,
mExpandCollapseListener,
- mTelecomCallLogCache,
+ mCallLogCache,
mCallLogListItemHelper,
- mVoicemailPlaybackPresenter);
+ mVoicemailPlaybackPresenter,
+ mFilteredNumberAsyncQueryHandler,
+ new Callback() {
+ @Override
+ public void onFilterNumberSuccess() {
+ Logger.logInteraction(
+ InteractionEvent.BLOCK_NUMBER_CALL_LOG);
+ }
+
+ @Override
+ public void onUnfilterNumberSuccess() {
+ Logger.logInteraction(
+ InteractionEvent.UNBLOCK_NUMBER_CALL_LOG);
+ }
+
+ @Override
+ public void onChangeFilteredNumberUndo() {}
+ }, mActivityType == ACTIVITY_TYPE_ARCHIVE);
viewHolder.callLogEntryView.setTag(viewHolder);
viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
- viewHolder.primaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
viewHolder.primaryActionView.setTag(viewHolder);
return viewHolder;
@@ -426,15 +442,14 @@ public class CallLogAdapter extends GroupingListAdapter
* TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and
* should not. It invokes cross-process methods and the repeat execution can get costly.
*
- * @param ViewHolder The view corresponding to this entry.
+ * @param viewHolder The view corresponding to this entry.
* @param position The position of the entry.
*/
+ @Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Trace.beginSection("onBindViewHolder: " + position);
switch (getItemViewType(position)) {
- case VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM:
- break;
case VIEW_TYPE_VOICEMAIL_PROMO_CARD:
bindVoicemailPromoCardViewHolder(viewHolder);
break;
@@ -454,9 +469,9 @@ public class CallLogAdapter extends GroupingListAdapter
protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
- promoCardViewHolder.getSettingsTextView().setOnClickListener(
- mVoicemailSettingsActionListener);
- promoCardViewHolder.getOkTextView().setOnClickListener(mOkActionListener);
+ promoCardViewHolder.getSecondaryActionView()
+ .setOnClickListener(mVoicemailSettingsActionListener);
+ promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener);
}
/**
@@ -475,14 +490,18 @@ public class CallLogAdapter extends GroupingListAdapter
int count = getGroupSize(position);
final String number = c.getString(CallLogQuery.NUMBER);
+ final String postDialDigits = CompatUtils.isNCompatible()
+ && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
+ c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+
final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
c.getString(CallLogQuery.ACCOUNT_ID));
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
- final ContactInfo cachedContactInfo = mContactInfoHelper.getContactInfo(c);
+ final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(c);
final boolean isVoicemailNumber =
- mTelecomCallLogCache.isVoicemailNumber(accountHandle, number);
+ mCallLogCache.isVoicemailNumber(accountHandle, number);
// Note: Binding of the action buttons is done as required in configureActionViews when the
// user expands the actions ViewStub.
@@ -490,49 +509,51 @@ public class CallLogAdapter extends GroupingListAdapter
ContactInfo info = ContactInfo.EMPTY;
if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) {
// Lookup contacts with this number
- info = mContactInfoCache.getValue(number, countryIso, cachedContactInfo);
+ info = mContactInfoCache.getValue(number + postDialDigits,
+ countryIso, cachedContactInfo);
}
CharSequence formattedNumber = info.formattedNumber == null
- ? null : PhoneNumberUtils.createTtsSpannable(info.formattedNumber);
+ ? null : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
final PhoneCallDetails details = new PhoneCallDetails(
- mContext, number, numberPresentation, formattedNumber, isVoicemailNumber);
+ mContext, number, numberPresentation, formattedNumber,
+ postDialDigits, isVoicemailNumber);
details.accountHandle = accountHandle;
- details.callTypes = getCallTypes(c, count);
details.countryIso = countryIso;
details.date = c.getLong(CallLogQuery.DATE);
details.duration = c.getLong(CallLogQuery.DURATION);
details.features = getCallFeatures(c, count);
details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
- if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) {
- details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
- }
+ details.callTypes = getCallTypes(c, count);
if (!c.isNull(CallLogQuery.DATA_USAGE)) {
details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
}
- if (!TextUtils.isEmpty(info.name)) {
+ if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
details.contactUri = info.lookupUri;
- details.name = info.name;
+ details.namePrimary = info.name;
+ details.nameAlternative = info.nameAlternative;
+ details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
details.numberType = info.type;
details.numberLabel = info.label;
details.photoUri = info.photoUri;
details.sourceType = info.sourceType;
details.objectId = info.objectId;
+ details.contactUserType = info.userType;
}
- CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
+ final CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
views.info = info;
views.rowId = c.getLong(CallLogQuery.ID);
// Store values used when the actions ViewStub is inflated on expansion.
views.number = number;
+ views.postDialDigits = details.postDialDigits;
views.displayNumber = details.displayNumber;
views.numberPresentation = numberPresentation;
- views.callType = c.getInt(CallLogQuery.CALL_TYPE);
+
views.accountHandle = accountHandle;
- views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
// Stash away the Ids of the calls so that we can support deleting a row in the call log.
views.callIds = getCallIds(c, count);
views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
@@ -540,6 +561,8 @@ public class CallLogAdapter extends GroupingListAdapter
details.numberLabel);
// Default case: an item in the call log.
views.primaryActionView.setVisibility(View.VISIBLE);
+ views.workIconView.setVisibility(
+ details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
// Check if the day group has changed and display a header if necessary.
int currentGroup = getDayGroupForCall(views.rowId);
@@ -551,6 +574,21 @@ public class CallLogAdapter extends GroupingListAdapter
views.dayGroupHeader.setVisibility(View.GONE);
}
+ if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
+ views.callType = CallLog.Calls.VOICEMAIL_TYPE;
+ views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt(
+ c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID)))
+ .toString();
+
+ } else {
+ if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
+ details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
+ details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
+ }
+ views.callType = c.getInt(CallLogQuery.CALL_TYPE);
+ views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+ }
+
mCallLogListItemHelper.setPhoneCallDetails(views, details);
if (mCurrentlyExpandedRowId == views.rowId) {
@@ -560,29 +598,28 @@ public class CallLogAdapter extends GroupingListAdapter
}
views.showActions(mCurrentlyExpandedPosition == position);
-
- String nameForDefaultImage = null;
- if (TextUtils.isEmpty(info.name)) {
- nameForDefaultImage = details.displayNumber;
- } else {
- nameForDefaultImage = info.name;
- }
- views.setPhoto(info.photoId, info.photoUri, info.lookupUri, nameForDefaultImage,
- isVoicemailNumber, views.isBusiness);
+ views.updatePhoto();
mCallLogListItemHelper.setPhoneCallDetails(views, details);
}
+ private String getPreferredDisplayName(ContactInfo contactInfo) {
+ if (mContactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY ||
+ TextUtils.isEmpty(contactInfo.nameAlternative)) {
+ return contactInfo.name;
+ }
+ return contactInfo.nameAlternative;
+ }
+
@Override
public int getItemCount() {
- return super.getItemCount() + ((isShowingRecentsTab() || mShowPromoCard) ? 1 : 0);
+ return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
+ - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
}
@Override
public int getItemViewType(int position) {
- if (position == getItemCount() - 1 && isShowingRecentsTab()) {
- return VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM;
- } else if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowPromoCard) {
+ if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowVoicemailPromoCard) {
return VIEW_TYPE_VOICEMAIL_PROMO_CARD;
}
return super.getItemViewType(position);
@@ -597,20 +634,87 @@ public class CallLogAdapter extends GroupingListAdapter
*/
@Override
public Object getItem(int position) {
- return super.getItem(position - (mShowPromoCard ? 1 : 0));
+ return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
+ + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
+ ? 1 : 0));
+ }
+
+ @Override
+ public int getGroupSize(int position) {
+ return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
}
- protected boolean isShowingRecentsTab() {
- return mIsShowingRecentsTab;
+ protected boolean isCallLogActivity() {
+ return mActivityType == ACTIVITY_TYPE_CALL_LOG;
}
+ /**
+ * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
+ * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
+ * clicks delete on a second item before the first item's undo option has expired, the first
+ * item is immediately deleted so that only one item can be "undoed" at a time.
+ */
@Override
public void onVoicemailDeleted(Uri uri) {
+ if (mHiddenItemUri == null) {
+ // Immediately hide the currently expanded card.
+ mHiddenPosition = mCurrentlyExpandedPosition;
+ notifyDataSetChanged();
+ } else {
+ // This means that there was a previous item that was hidden in the UI but not
+ // yet deleted from the database (call it a "pending delete"). Delete this previous item
+ // now since it is only possible to do one "undo" at a time.
+ CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
+
+ // Set pending hide action so that the current item is hidden only after the previous
+ // item is permanently deleted.
+ mPendingHide = true;
+ }
+
+ collapseExpandedCard();
+
+ // Save the new hidden item uri in case it needs to be deleted from the database when
+ // a user attempts to delete another item.
+ mHiddenItemUri = uri;
+ }
+
+ private void collapseExpandedCard() {
mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
}
/**
+ * When the user clicks "undo", the hidden item is unhidden.
+ */
+ @Override
+ public void onVoicemailDeleteUndo() {
+ mHiddenPosition = RecyclerView.NO_POSITION;
+ mHiddenItemUri = null;
+
+ mPendingHide = false;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * This callback signifies that a database deletion has completed. This means that if there is
+ * an item pending deletion, it will be hidden because the previous item that was in "undo" mode
+ * has been removed from the database. Otherwise it simply resets the hidden state because there
+ * are no pending deletes and thus no hidden items.
+ */
+ @Override
+ public void onVoicemailDeletedInDatabase() {
+ if (mPendingHide) {
+ mHiddenPosition = mCurrentlyExpandedPosition;
+ mPendingHide = false;
+ } else {
+ // There should no longer be any hidden item because it has been deleted from the
+ // database.
+ mHiddenPosition = RecyclerView.NO_POSITION;
+ mHiddenItemUri = null;
+ }
+ }
+
+ /**
* Retrieves the day group of the previous call in the call log. Used to determine if the day
* group has changed and to trigger display of the day group text.
*
@@ -622,8 +726,16 @@ public class CallLogAdapter extends GroupingListAdapter
int startingPosition = cursor.getPosition();
int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
if (cursor.moveToPrevious()) {
- long previousRowId = cursor.getLong(CallLogQuery.ID);
- dayGroup = getDayGroupForCall(previousRowId);
+ // If the previous entry is hidden (deleted in the UI but not in the database), skip it
+ // and check the card above it. A list with the voicemail promo card at the top will be
+ // 1-indexed because the 0th index is the promo card iteself.
+ int previousViewPosition = mShowVoicemailPromoCard ? startingPosition :
+ startingPosition - 1;
+ if (previousViewPosition != mHiddenPosition ||
+ (previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) {
+ long previousRowId = cursor.getLong(CallLogQuery.ID);
+ dayGroup = getDayGroupForCall(previousRowId);
+ }
}
cursor.moveToPosition(startingPosition);
return dayGroup;
@@ -651,6 +763,9 @@ public class CallLogAdapter extends GroupingListAdapter
* It position in the cursor is unchanged by this function.
*/
private int[] getCallTypes(Cursor cursor, int count) {
+ if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
+ return new int[] {CallLog.Calls.VOICEMAIL_TYPE};
+ }
int position = cursor.getPosition();
int[] callTypes = new int[count];
for (int index = 0; index < count; ++index) {
@@ -698,11 +813,6 @@ public class CallLogAdapter extends GroupingListAdapter
mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
}
- @Override
- public void addGroup(int cursorPosition, int size, boolean expanded) {
- super.addGroup(cursorPosition, size, expanded);
- }
-
/**
* Stores the day group associated with a call in the call log.
*
@@ -767,7 +877,8 @@ public class CallLogAdapter extends GroupingListAdapter
private void maybeShowVoicemailPromoCard() {
boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
- mShowPromoCard = (mVoicemailPlaybackPresenter != null) && showPromoCard;
+ mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE &&
+ (mVoicemailPlaybackPresenter != null) && showPromoCard;
}
/**
@@ -775,7 +886,7 @@ public class CallLogAdapter extends GroupingListAdapter
*/
private void dismissVoicemailPromoCard() {
mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply();
- mShowPromoCard = false;
+ mShowVoicemailPromoCard = false;
notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION);
}
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index 22dece57c..7cb35f514 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -16,6 +16,9 @@
package com.android.dialer.calllog;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -29,13 +32,18 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.util.PermissionsUtil;
import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.compat.CallsSdkCompat;
+import com.android.dialer.database.VoicemailArchiveContract;
import com.android.dialer.util.AsyncTaskExecutor;
import com.android.dialer.util.AsyncTaskExecutors;
import com.android.dialer.util.PhoneNumberUtil;
import com.android.dialer.util.TelecomUtil;
-import com.google.common.annotations.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.Arrays;
public class CallLogAsyncTaskUtil {
private static String TAG = CallLogAsyncTaskUtil.class.getSimpleName();
@@ -44,12 +52,16 @@ public class CallLogAsyncTaskUtil {
public enum Tasks {
DELETE_VOICEMAIL,
DELETE_CALL,
+ DELETE_BLOCKED_CALL,
MARK_VOICEMAIL_READ,
+ MARK_CALL_READ,
GET_CALL_DETAILS,
+ UPDATE_DURATION
}
- private static class CallDetailQuery {
- static final String[] CALL_LOG_PROJECTION = new String[] {
+ private static final class CallDetailQuery {
+
+ private static final String[] CALL_LOG_PROJECTION_INTERNAL = new String[] {
CallLog.Calls.DATE,
CallLog.Calls.DURATION,
CallLog.Calls.NUMBER,
@@ -63,6 +75,7 @@ public class CallLogAsyncTaskUtil {
CallLog.Calls.DATA_USAGE,
CallLog.Calls.TRANSCRIPTION
};
+ public static final String[] CALL_LOG_PROJECTION;
static final int DATE_COLUMN_INDEX = 0;
static final int DURATION_COLUMN_INDEX = 1;
@@ -76,14 +89,44 @@ public class CallLogAsyncTaskUtil {
static final int FEATURES = 9;
static final int DATA_USAGE = 10;
static final int TRANSCRIPTION_COLUMN_INDEX = 11;
+ static final int POST_DIAL_DIGITS = 12;
+
+ static {
+ ArrayList<String> projectionList = new ArrayList<>();
+ projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL));
+ if (CompatUtils.isNCompatible()) {
+ projectionList.add(CallsSdkCompat.POST_DIAL_DIGITS);
+ }
+ projectionList.trimToSize();
+ CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+
+ private static class CallLogDeleteBlockedCallQuery {
+ static final String[] PROJECTION = new String[] {
+ CallLog.Calls._ID,
+ CallLog.Calls.DATE
+ };
+
+ static final int ID_COLUMN_INDEX = 0;
+ static final int DATE_COLUMN_INDEX = 1;
}
public interface CallLogAsyncTaskListener {
- public void onDeleteCall();
- public void onDeleteVoicemail();
- public void onGetCallDetails(PhoneCallDetails[] details);
+ void onDeleteCall();
+ void onDeleteVoicemail();
+ void onGetCallDetails(PhoneCallDetails[] details);
+ }
+
+ public interface OnCallLogQueryFinishedListener {
+ void onQueryFinished(boolean hasEntry);
}
+ // Try to identify if a call log entry corresponds to a number which was blocked. We match by
+ // by comparing its creation time to the time it was added in the InCallUi and seeing if they
+ // fall within a certain threshold.
+ private static final int MATCH_BLOCKED_CALL_THRESHOLD_MS = 3000;
+
private static AsyncTaskExecutor sAsyncTaskExecutor;
private static void initTaskExecutor() {
@@ -142,6 +185,8 @@ public class CallLogAsyncTaskUtil {
// Read call log.
final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
+ final String postDialDigits = CompatUtils.isNCompatible()
+ ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) : "";
final int numberPresentation =
cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
@@ -155,19 +200,21 @@ public class CallLogAsyncTaskUtil {
boolean isVoicemail = PhoneNumberUtil.isVoicemailNumber(context, accountHandle, number);
boolean shouldLookupNumber =
PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
-
ContactInfo info = ContactInfo.EMPTY;
+
if (shouldLookupNumber) {
ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
}
PhoneCallDetails details = new PhoneCallDetails(
- context, number, numberPresentation, info.formattedNumber, isVoicemail);
+ context, number, numberPresentation, info.formattedNumber,
+ postDialDigits, isVoicemail);
details.accountHandle = accountHandle;
details.contactUri = info.lookupUri;
- details.name = info.name;
+ details.namePrimary = info.name;
+ details.nameAlternative = info.nameAlternative;
details.numberType = info.type;
details.numberLabel = info.label;
details.photoUri = info.photoUri;
@@ -204,7 +251,7 @@ public class CallLogAsyncTaskUtil {
*
* @param context The context.
* @param callIds String of the callIds to delete from the call log, delimited by commas (",").
- * @param callLogAsyncTaskListenerg The listener to invoke after the entries have been deleted.
+ * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted.
*/
public static void deleteCalls(
final Context context,
@@ -214,26 +261,82 @@ public class CallLogAsyncTaskUtil {
initTaskExecutor();
}
- sAsyncTaskExecutor.submit(Tasks.DELETE_CALL,
- new AsyncTask<Void, Void, Void>() {
- @Override
- public Void doInBackground(Void... params) {
+ sAsyncTaskExecutor.submit(Tasks.DELETE_CALL, new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ context.getContentResolver().delete(
+ TelecomUtil.getCallLogUri(context),
+ CallLog.Calls._ID + " IN (" + callIds + ")", null);
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void result) {
+ if (callLogAsyncTaskListener != null) {
+ callLogAsyncTaskListener.onDeleteCall();
+ }
+ }
+ });
+ }
+
+ /**
+ * Deletes the last call made by the number within a threshold of the call time added in the
+ * call log, assuming it is a blocked call for which no entry should be shown.
+ *
+ * @param context The context.
+ * @param number Number of blocked call, for which to delete the call log entry.
+ * @param timeAddedMs The time the number was added to InCall, in milliseconds.
+ * @param listener The listener to invoke after looking up for a call log entry matching the
+ * number and time added.
+ */
+ public static void deleteBlockedCall(
+ final Context context,
+ final String number,
+ final long timeAddedMs,
+ final OnCallLogQueryFinishedListener listener) {
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(Tasks.DELETE_BLOCKED_CALL, new AsyncTask<Void, Void, Long>() {
+ @Override
+ public Long doInBackground(Void... params) {
+ // First, lookup the call log entry of the most recent call with this number.
+ Cursor cursor = context.getContentResolver().query(
+ TelecomUtil.getCallLogUri(context),
+ CallLogDeleteBlockedCallQuery.PROJECTION,
+ CallLog.Calls.NUMBER + "= ?",
+ new String[] { number },
+ CallLog.Calls.DATE + " DESC LIMIT 1");
+
+ // If match is found, delete this call log entry and return the call log entry id.
+ if (cursor.moveToFirst()) {
+ long creationTime =
+ cursor.getLong(CallLogDeleteBlockedCallQuery.DATE_COLUMN_INDEX);
+ if (timeAddedMs > creationTime
+ && timeAddedMs - creationTime < MATCH_BLOCKED_CALL_THRESHOLD_MS) {
+ long callLogEntryId =
+ cursor.getLong(CallLogDeleteBlockedCallQuery.ID_COLUMN_INDEX);
context.getContentResolver().delete(
TelecomUtil.getCallLogUri(context),
- CallLog.Calls._ID + " IN (" + callIds + ")", null);
- return null;
- }
-
- @Override
- public void onPostExecute(Void result) {
- if (callLogAsyncTaskListener != null) {
- callLogAsyncTaskListener.onDeleteCall();
- }
+ CallLog.Calls._ID + " IN (" + callLogEntryId + ")",
+ null);
+ return callLogEntryId;
}
- });
+ }
+ return (long) -1;
+ }
+ @Override
+ public void onPostExecute(Long callLogEntryId) {
+ if (listener != null) {
+ listener.onQueryFinished(callLogEntryId >= 0);
+ }
+ }
+ });
}
+
public static void markVoicemailAsRead(final Context context, final Uri voicemailUri) {
if (sAsyncTaskExecutor == null) {
initTaskExecutor();
@@ -263,21 +366,87 @@ public class CallLogAsyncTaskUtil {
initTaskExecutor();
}
- sAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL,
- new AsyncTask<Void, Void, Void>() {
- @Override
- public Void doInBackground(Void... params) {
- context.getContentResolver().delete(voicemailUri, null, null);
- return null;
- }
+ sAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL, new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ context.getContentResolver().delete(voicemailUri, null, null);
+ return null;
+ }
- @Override
- public void onPostExecute(Void result) {
- if (callLogAsyncTaskListener != null) {
- callLogAsyncTaskListener.onDeleteVoicemail();
- }
- }
- });
+ @Override
+ public void onPostExecute(Void result) {
+ if (callLogAsyncTaskListener != null) {
+ callLogAsyncTaskListener.onDeleteVoicemail();
+ }
+ }
+ });
+ }
+
+ public static void markCallAsRead(final Context context, final long[] callIds) {
+ if (!PermissionsUtil.hasPhonePermissions(context)) {
+ return;
+ }
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(Tasks.MARK_CALL_READ, new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+
+ StringBuilder where = new StringBuilder();
+ where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE);
+ where.append(" AND ");
+
+ Long[] callIdLongs = new Long[callIds.length];
+ for (int i = 0; i < callIds.length; i++) {
+ callIdLongs[i] = callIds[i];
+ }
+ where.append(CallLog.Calls._ID).append(
+ " IN (" + TextUtils.join(",", callIdLongs) + ")");
+
+ ContentValues values = new ContentValues(1);
+ values.put(CallLog.Calls.IS_READ, "1");
+ context.getContentResolver().update(
+ CallLog.Calls.CONTENT_URI, values, where.toString(), null);
+ return null;
+ }
+ });
+ }
+
+ /**
+ * Updates the duration of a voicemail call log entry if the duration given is greater than 0,
+ * and if if the duration currently in the database is less than or equal to 0 (non-existent).
+ */
+ public static void updateVoicemailDuration(
+ final Context context,
+ final Uri voicemailUri,
+ final long duration) {
+ if (duration <= 0 || !PermissionsUtil.hasPhonePermissions(context)) {
+ return;
+ }
+ if (sAsyncTaskExecutor == null) {
+ initTaskExecutor();
+ }
+
+ sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ ContentResolver contentResolver = context.getContentResolver();
+ Cursor cursor = contentResolver.query(
+ voicemailUri,
+ new String[] { VoicemailArchiveContract.VoicemailArchive.DURATION },
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst() && cursor.getInt(
+ cursor.getColumnIndex(
+ VoicemailArchiveContract.VoicemailArchive.DURATION)) <= 0) {
+ ContentValues values = new ContentValues(1);
+ values.put(CallLog.Calls.DURATION, duration);
+ context.getContentResolver().update(voicemailUri, values, null, null);
+ }
+ return null;
+ }
+ });
}
@VisibleForTesting
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index e7b77646d..07299a2fb 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -16,61 +16,47 @@
package com.android.dialer.calllog;
-import static android.Manifest.permission.READ_CALL_LOG;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.app.Activity;
-import android.app.DialogFragment;
import android.app.Fragment;
import android.app.KeyguardManager;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
-import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Message;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
-import android.provider.VoicemailContract.Status;
-import android.support.v7.widget.RecyclerView;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.ListView;
-import android.widget.TextView;
import com.android.contacts.common.GeoUtil;
import com.android.contacts.common.util.PermissionsUtil;
-import com.android.contacts.common.util.ViewUtil;
import com.android.dialer.R;
-import com.android.dialer.list.ListsFragment.HostInterface;
-import com.android.dialer.util.DialerUtils;
+import com.android.dialer.list.ListsFragment;
import com.android.dialer.util.EmptyLoader;
import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
-import com.android.dialer.voicemail.VoicemailStatusHelper;
-import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
-import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
import com.android.dialer.widget.EmptyContentView;
import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
import com.android.dialerbind.ObjectFactory;
-import java.util.List;
+import static android.Manifest.permission.READ_CALL_LOG;
/**
* Displays a list of call log entries. To filter for a particular kind of call
* (all, missed or voicemails), specify it in the constructor.
*/
public class CallLogFragment extends Fragment implements CallLogQueryHandler.Listener,
- CallLogAdapter.CallFetcher, OnEmptyViewActionButtonClickedListener {
+ CallLogAdapter.CallFetcher, OnEmptyViewActionButtonClickedListener,
+ FragmentCompat.OnRequestPermissionsResultCallback {
private static final String TAG = "CallLogFragment";
/**
@@ -81,6 +67,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
private static final String KEY_FILTER_TYPE = "filter_type";
private static final String KEY_LOG_LIMIT = "log_limit";
private static final String KEY_DATE_LIMIT = "date_limit";
+ private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
// No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
private static final int NO_LOG_LIMIT = -1;
@@ -89,15 +76,16 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
+ private static final int EVENT_UPDATE_DISPLAY = 1;
+
+ private static final long MILLIS_IN_MINUTE = 60 * 1000;
+
private RecyclerView mRecyclerView;
private LinearLayoutManager mLayoutManager;
private CallLogAdapter mAdapter;
private CallLogQueryHandler mCallLogQueryHandler;
- private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
private boolean mScrollToTop;
- /** Whether there is at least one voicemail source installed. */
- private boolean mVoicemailSourcesAvailable = false;
private EmptyContentView mEmptyListView;
private KeyguardManager mKeyguardManager;
@@ -106,9 +94,21 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
private boolean mCallLogFetched;
private boolean mVoicemailStatusFetched;
+ private final Handler mDisplayUpdateHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_UPDATE_DISPLAY:
+ refreshData();
+ rescheduleDisplayUpdate();
+ break;
+ }
+ }
+ };
+
private final Handler mHandler = new Handler();
- private class CustomContentObserver extends ContentObserver {
+ protected class CustomContentObserver extends ContentObserver {
public CustomContentObserver() {
super(mHandler);
}
@@ -121,7 +121,6 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
// See issue 6363009
private final ContentObserver mCallLogObserver = new CustomContentObserver();
private final ContentObserver mContactsObserver = new CustomContentObserver();
- private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
private boolean mRefreshDataRequired = true;
private boolean mHasReadCallLogPermission = false;
@@ -141,10 +140,9 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
private long mDateLimit = NO_DATE_LIMIT;
/*
- * True if this instance of the CallLogFragment is the Recents screen shown in
- * DialtactsActivity.
+ * True if this instance of the CallLogFragment shown in the CallLogActivity.
*/
- private boolean mIsRecentsFragment;
+ private boolean mIsCallLogActivity = false;
public interface HostInterface {
public void showDialpad();
@@ -158,6 +156,11 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
this(filterType, NO_LOG_LIMIT);
}
+ public CallLogFragment(int filterType, boolean isCallLogActivity) {
+ this(filterType, NO_LOG_LIMIT);
+ mIsCallLogActivity = isCallLogActivity;
+ }
+
public CallLogFragment(int filterType, int logLimit) {
this(filterType, logLimit, NO_DATE_LIMIT);
}
@@ -192,10 +195,9 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
+ mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
}
- mIsRecentsFragment = mLogLimit != NO_LOG_LIMIT;
-
final Activity activity = getActivity();
final ContentResolver resolver = activity.getContentResolver();
String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
@@ -205,13 +207,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true,
mContactsObserver);
- resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
setHasOptionsMenu(true);
-
- if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
- mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter
- .getInstance(activity, state);
- }
}
/** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@@ -282,9 +278,20 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
}
@Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {}
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
+
+ @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+ setupView(view, null);
+ return view;
+ }
+ protected void setupView(
+ View view, @Nullable VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
@@ -293,18 +300,17 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
mEmptyListView.setImage(R.drawable.empty_call_log);
mEmptyListView.setActionClickedListener(this);
+ int activityType = mIsCallLogActivity ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG :
+ CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
- boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT;
mAdapter = ObjectFactory.newCallLogAdapter(
- getActivity(),
- this,
- new ContactInfoHelper(getActivity(), currentCountryIso),
- mVoicemailPlaybackPresenter,
- isShowingRecentsTab);
+ getActivity(),
+ this,
+ new ContactInfoHelper(getActivity(), currentCountryIso),
+ voicemailPlaybackPresenter,
+ activityType);
mRecyclerView.setAdapter(mAdapter);
-
fetchCalls();
- return view;
}
@Override
@@ -336,39 +342,34 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
mRefreshDataRequired = true;
updateEmptyMessage(mCallTypeFilter);
}
+
mHasReadCallLogPermission = hasReadCallLogPermission;
refreshData();
- mAdapter.startCache();
+ mAdapter.onResume();
+
+ rescheduleDisplayUpdate();
}
@Override
public void onPause() {
- if (mVoicemailPlaybackPresenter != null) {
- mVoicemailPlaybackPresenter.onPause();
- }
- mAdapter.pauseCache();
+ cancelDisplayUpdate();
+ mAdapter.onPause();
super.onPause();
}
@Override
public void onStop() {
- updateOnTransition(false /* onEntry */);
+ updateOnTransition();
super.onStop();
}
@Override
public void onDestroy() {
- mAdapter.pauseCache();
mAdapter.changeCursor(null);
- if (mVoicemailPlaybackPresenter != null) {
- mVoicemailPlaybackPresenter.onDestroy();
- }
-
getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
- getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
super.onDestroy();
}
@@ -378,17 +379,17 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
outState.putInt(KEY_LOG_LIMIT, mLogLimit);
outState.putLong(KEY_DATE_LIMIT, mDateLimit);
+ outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
mAdapter.onSaveInstanceState(outState);
-
- if (mVoicemailPlaybackPresenter != null) {
- mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
- }
}
@Override
public void fetchCalls() {
mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
+ if (!mIsCallLogActivity) {
+ ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ }
}
private void updateEmptyMessage(int filterType) {
@@ -406,23 +407,23 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
final int messageId;
switch (filterType) {
case Calls.MISSED_TYPE:
- messageId = R.string.recentMissed_empty;
+ messageId = R.string.call_log_missed_empty;
break;
case Calls.VOICEMAIL_TYPE:
- messageId = R.string.recentVoicemails_empty;
+ messageId = R.string.call_log_voicemail_empty;
break;
case CallLogQueryHandler.CALL_TYPE_ALL:
- messageId = R.string.recentCalls_empty;
+ messageId = R.string.call_log_all_empty;
break;
default:
throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
+ filterType);
}
mEmptyListView.setDescription(messageId);
- if (mIsRecentsFragment) {
- mEmptyListView.setActionLabel(R.string.recentCalls_empty_action);
- } else {
+ if (mIsCallLogActivity) {
mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+ } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
+ mEmptyListView.setActionLabel(R.string.call_log_all_empty_action);
}
}
@@ -436,7 +437,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
if (mMenuVisible != menuVisible) {
mMenuVisible = menuVisible;
if (!menuVisible) {
- updateOnTransition(false /* onEntry */);
+ updateOnTransition();
} else if (isResumed()) {
refreshData();
}
@@ -454,8 +455,8 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
fetchCalls();
mCallLogQueryHandler.fetchVoicemailStatus();
-
- updateOnTransition(true /* onEntry */);
+ mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+ updateOnTransition();
mRefreshDataRequired = false;
} else {
// Refresh the display of the existing data to update the timestamp text descriptions.
@@ -464,24 +465,16 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
}
/**
- * Updates the call data and notification state on entering or leaving the call log tab.
- *
- * If we are leaving the call log tab, mark all the missed calls as read.
+ * Updates the voicemail notification state.
*
* TODO: Move to CallLogActivity
*/
- private void updateOnTransition(boolean onEntry) {
+ private void updateOnTransition() {
// 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 update the missed call and voicemail notifications.
- // While exiting we additionally consume all missed calls (by marking them as read).
- mCallLogQueryHandler.markNewCallsAsOld();
- if (!onEntry) {
- mCallLogQueryHandler.markMissedCallsAsRead();
- }
- CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
+ if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()
+ && mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
}
}
@@ -494,9 +487,10 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
}
if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
- requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
- } else if (mIsRecentsFragment) {
- // Show dialpad if we are the recents fragment.
+ FragmentCompat.requestPermissions(this, new String[] {READ_CALL_LOG},
+ READ_CALL_LOG_PERMISSION_REQUEST_CODE);
+ } else if (!mIsCallLogActivity) {
+ // Show dialpad if we are not in the call log activity.
((HostInterface) activity).showDialpad();
}
}
@@ -511,4 +505,25 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis
}
}
}
+
+ /**
+ * Schedules an update to the relative call times (X mins ago).
+ */
+ private void rescheduleDisplayUpdate() {
+ if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) {
+ long time = System.currentTimeMillis();
+ // This value allows us to change the display relatively close to when the time changes
+ // from one minute to the next.
+ long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE);
+ mDisplayUpdateHandler.sendEmptyMessageDelayed(
+ EVENT_UPDATE_DISPLAY, millisUtilNextMinute);
+ }
+ }
+
+ /**
+ * Cancels any pending update requests to update the relative call times (X mins ago).
+ */
+ private void cancelDisplayUpdate() {
+ mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 0826aeb4a..0931e0644 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -16,17 +16,17 @@
package com.android.dialer.calllog;
+import com.google.common.annotations.VisibleForTesting;
+
import android.database.Cursor;
-import android.provider.CallLog.Calls;
import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
import android.text.format.Time;
+import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.util.DateUtils;
import com.android.contacts.common.util.PhoneNumberHelper;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import java.util.Objects;
+import com.android.dialer.util.AppCompatConstants;
/**
* Groups together calls in the call log. The primary grouping attempts to group together calls
@@ -46,9 +46,8 @@ public class CallLogGroupBuilder {
* dialed.
* @param cursorPosition The starting position of the group in the cursor.
* @param size The size of the group.
- * @param expanded Whether the group is expanded; always false for the call log.
*/
- public void addGroup(int cursorPosition, int size, boolean expanded);
+ public void addGroup(int cursorPosition, int size);
/**
* Defines the interface for tracking the day group each call belongs to. Calls in a call
@@ -94,7 +93,7 @@ public class CallLogGroupBuilder {
/**
* 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
+ * calls {@link GroupCreator#addGroup(int, int)} 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.
@@ -114,98 +113,106 @@ public class CallLogGroupBuilder {
// Get current system time, used for calculating which day group calls belong to.
long currentTime = System.currentTimeMillis();
-
- 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);
-
- // The account information of the first entry in the group.
- String firstAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
- String firstAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
// Determine the day group for the first call in the cursor.
final long firstDate = cursor.getLong(CallLogQuery.DATE);
final long firstRowId = cursor.getLong(CallLogQuery.ID);
- int currentGroupDayGroup = getDayGroup(firstDate, currentTime);
- mGroupCreator.setDayGroup(firstRowId, currentGroupDayGroup);
+ int groupDayGroup = getDayGroup(firstDate, currentTime);
+ mGroupCreator.setDayGroup(firstRowId, groupDayGroup);
+
+ // Instantiate the group values to those of the first call in the cursor.
+ String groupNumber = cursor.getString(CallLogQuery.NUMBER);
+ String groupPostDialDigits = CompatUtils.isNCompatible()
+ ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+ String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+ int groupSize = 1;
+
+ String number;
+ String numberPostDialDigits;
+ int callType;
+ String accountComponentName;
+ String accountId;
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 String currentAccountComponentName = cursor.getString(
- CallLogQuery.ACCOUNT_COMPONENT_NAME);
- final String currentAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
-
- final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
- final boolean sameAccountComponentName = Objects.equals(
- firstAccountComponentName,
- currentAccountComponentName);
- final boolean sameAccountId = Objects.equals(
- firstAccountId,
- currentAccountId);
- final boolean sameAccount = sameAccountComponentName && sameAccountId;
-
- final boolean shouldGroup;
- final long currentCallId = cursor.getLong(CallLogQuery.ID);
- final long date = cursor.getLong(CallLogQuery.DATE);
-
- if (!sameNumber || !sameAccount) {
- // 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.VOICEMAIL_TYPE;
- }
-
- if (shouldGroup) {
+ // Obtain the values for the current call to group.
+ number = cursor.getString(CallLogQuery.NUMBER);
+ numberPostDialDigits = CompatUtils.isNCompatible()
+ ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+ accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+
+ final boolean isSameNumber = equalNumbers(groupNumber, number);
+ final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits);
+ final boolean isSameAccount = isSameAccount(
+ groupAccountComponentName, accountComponentName, groupAccountId, accountId);
+
+ // Group with the same number and account. Never group voicemails. Only group blocked
+ // calls with other blocked calls.
+ if (isSameNumber && isSameAccount && isSamePostDialDigits
+ && areBothNotVoicemail(callType, groupCallType)
+ && (areBothNotBlocked(callType, groupCallType)
+ || areBothBlocked(callType, groupCallType))) {
// 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++;
+ // the group until finding a call that does not match.
+ groupSize++;
} else {
- // The call group has changed, so determine the day group for the new call group.
- // This ensures all calls grouped together in the call log are assigned the same
- // day group.
- currentGroupDayGroup = getDayGroup(date, currentTime);
-
- // 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);
- }
+ // The call group has changed. Determine the day group for the new call group.
+ final long date = cursor.getLong(CallLogQuery.DATE);
+ groupDayGroup = getDayGroup(date, currentTime);
+
+ // Create a group for the previous group of calls, which does not include the
+ // current call.
+ mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize);
+
// 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;
- firstAccountComponentName = currentAccountComponentName;
- firstAccountId = currentAccountId;
+ groupSize = 1;
+
+ // Update the group values to those of the current call.
+ groupNumber = number;
+ groupPostDialDigits = numberPostDialDigits;
+ groupCallType = callType;
+ groupAccountComponentName = accountComponentName;
+ groupAccountId = accountId;
}
// Save the day group associated with the current call.
- mGroupCreator.setDayGroup(currentCallId, currentGroupDayGroup);
- }
- // 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);
+ final long currentCallId = cursor.getLong(CallLogQuery.ID);
+ mGroupCreator.setDayGroup(currentCallId, groupDayGroup);
}
+
+ // Create a group for the last set of calls.
+ mGroupCreator.addGroup(count - groupSize, groupSize);
}
/**
- * Creates a group of items in the cursor.
- * <p>
- * The group is always unexpanded.
- *
- * @see CallLogAdapter#addGroup(int, int, boolean)
+ * Group cursor entries by date, with only one entry per group. This is used for listing
+ * voicemails in the archive tab.
*/
- private void addGroup(int cursorPosition, int size) {
- mGroupCreator.addGroup(cursorPosition, size, false);
+ public void addVoicemailGroups(Cursor cursor) {
+ if (cursor.getCount() == 0) {
+ return;
+ }
+
+ // Clear any previous day grouping information.
+ mGroupCreator.clearDayGroups();
+
+ // Get current system time, used for calculating which day group calls belong to.
+ long currentTime = System.currentTimeMillis();
+
+ // Reset cursor to start before the first row
+ cursor.moveToPosition(-1);
+
+ // Create an individual group for each voicemail
+ while (cursor.moveToNext()) {
+ mGroupCreator.addGroup(cursor.getPosition(), 1);
+ mGroupCreator.setDayGroup(cursor.getLong(CallLogQuery.ID),
+ getDayGroup(cursor.getLong(CallLogQuery.DATE), currentTime));
+
+ }
}
@VisibleForTesting
@@ -217,6 +224,10 @@ public class CallLogGroupBuilder {
}
}
+ private boolean isSameAccount(String name1, String name2, String id1, String id2) {
+ return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2);
+ }
+
@VisibleForTesting
boolean compareSipAddresses(String number1, String number2) {
if (number1 == null || number2 == null) return number1 == number2;
@@ -264,4 +275,19 @@ public class CallLogGroupBuilder {
return DAY_GROUP_OTHER;
}
}
+
+ private boolean areBothNotVoicemail(int callType, int groupCallType) {
+ return callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE
+ && groupCallType != AppCompatConstants.CALLS_VOICEMAIL_TYPE;
+ }
+
+ private boolean areBothNotBlocked(int callType, int groupCallType) {
+ return callType != AppCompatConstants.CALLS_BLOCKED_TYPE
+ && groupCallType != AppCompatConstants.CALLS_BLOCKED_TYPE;
+ }
+
+ private boolean areBothBlocked(int callType, int groupCallType) {
+ return callType == AppCompatConstants.CALLS_BLOCKED_TYPE
+ && groupCallType == AppCompatConstants.CALLS_BLOCKED_TYPE;
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
index 1c8e397e4..5d2bc8591 100644
--- a/src/com/android/dialer/calllog/CallLogListItemHelper.java
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -16,7 +16,6 @@
package com.android.dialer.calllog;
-import android.content.Context;
import android.content.res.Resources;
import android.provider.CallLog.Calls;
import android.text.SpannableStringBuilder;
@@ -24,7 +23,9 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.util.AppCompatConstants;
import com.android.dialer.R;
+import com.android.dialer.calllog.calllogcache.CallLogCache;
/**
* Helper class to fill in the views of a call log entry.
@@ -36,27 +37,27 @@ import com.android.dialer.R;
private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
/** Resources to look up strings. */
private final Resources mResources;
- private final TelecomCallLogCache mTelecomCallLogCache;
+ private final CallLogCache mCallLogCache;
/**
* Creates a new helper instance.
*
* @param phoneCallDetailsHelper used to set the details of a phone call
- * @param phoneNumberHelper used to process phone number
+ * @param resources The object from which resources can be retrieved
+ * @param callLogCache A cache for values retrieved from telecom/telephony
*/
public CallLogListItemHelper(
PhoneCallDetailsHelper phoneCallDetailsHelper,
Resources resources,
- TelecomCallLogCache telecomCallLogCache) {
+ CallLogCache callLogCache) {
mPhoneCallDetailsHelper = phoneCallDetailsHelper;
mResources = resources;
- mTelecomCallLogCache = telecomCallLogCache;
+ mCallLogCache = callLogCache;
}
/**
* Sets the name, label, and number for a contact.
*
- * @param context The application context.
* @param views the views to populate
* @param details the details of a phone call needed to fill in the data
*/
@@ -74,6 +75,13 @@ import com.android.dialer.R;
// Cache name or number of caller. Used when setting the content descriptions of buttons
// when the actions ViewStub is inflated.
views.nameOrNumber = getNameOrNumber(details);
+
+ // The call type or Location associated with the call. Use when setting text for a
+ // voicemail log's call button
+ views.callTypeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details);
+
+ // Cache country iso. Used for number filtering.
+ views.countryIso = details.countryIso;
}
/**
@@ -157,7 +165,6 @@ import com.android.dialer.R;
*/
public CharSequence getCallDescription(PhoneCallDetails details) {
int lastCallType = getLastCallType(details.callTypes);
- boolean isVoiceMail = lastCallType == Calls.VOICEMAIL_TYPE;
// Get the name or number of the caller.
final CharSequence nameOrNumber = getNameOrNumber(details);
@@ -170,11 +177,6 @@ import com.android.dialer.R;
SpannableStringBuilder callDescription = new SpannableStringBuilder();
- // Prepend the voicemail indication.
- if (isVoiceMail) {
- callDescription.append(mResources.getString(R.string.description_new_voicemail));
- }
-
// Add number of calls if more than one.
if (details.callTypes.length > 1) {
callDescription.append(mResources.getString(R.string.description_num_calls,
@@ -186,8 +188,8 @@ import com.android.dialer.R;
callDescription.append(mResources.getString(R.string.description_video_call));
}
- int stringID = getCallDescriptionStringID(details.callTypes);
- String accountLabel = mTelecomCallLogCache.getAccountLabel(details.accountHandle);
+ int stringID = getCallDescriptionStringID(details.callTypes, details.isRead);
+ String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
// Use chosen string resource to build up the message.
CharSequence onAccountLabel = accountLabel == null
@@ -210,21 +212,28 @@ import com.android.dialer.R;
/**
* Determine the appropriate string ID to describe a call for accessibility purposes.
*
- * @param details Call details.
+ * @param callTypes The type of call corresponding to this entry or multiple if this entry
+ * represents multiple calls grouped together.
+ * @param isRead If the entry is a voicemail, {@code true} if the voicemail is read.
* @return String resource ID to use.
*/
- public int getCallDescriptionStringID(int[] callTypes) {
+ public int getCallDescriptionStringID(int[] callTypes, boolean isRead) {
int lastCallType = getLastCallType(callTypes);
int stringID;
- if (lastCallType == Calls.VOICEMAIL_TYPE || lastCallType == Calls.MISSED_TYPE) {
+ if (lastCallType == AppCompatConstants.CALLS_MISSED_TYPE) {
//Message: Missed call from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
//<PhoneAccount>.
stringID = R.string.description_incoming_missed_call;
- } else if (lastCallType == Calls.INCOMING_TYPE) {
+ } else if (lastCallType == AppCompatConstants.CALLS_INCOMING_TYPE) {
//Message: Answered call from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
//<PhoneAccount>.
stringID = R.string.description_incoming_answered_call;
+ } else if (lastCallType == AppCompatConstants.CALLS_VOICEMAIL_TYPE) {
+ //Message: (Unread) [V/v]oicemail from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID = isRead ? R.string.description_read_voicemail
+ : R.string.description_unread_voicemail;
} else {
//Message: Call to <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>, <PhoneAccount>.
stringID = R.string.description_outgoing_call;
@@ -252,10 +261,10 @@ import com.android.dialer.R;
*/
private CharSequence getNameOrNumber(PhoneCallDetails details) {
final CharSequence recipient;
- if (!TextUtils.isEmpty(details.name)) {
- recipient = details.name;
+ if (!TextUtils.isEmpty(details.getPreferredName())) {
+ recipient = details.getPreferredName();
} else {
- recipient = details.displayNumber;
+ recipient = details.displayNumber + details.postDialDigits;
}
return recipient;
}
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index 0fa5e6d33..750914bdf 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -18,32 +18,55 @@ package com.android.dialer.calllog;
import android.app.Activity;
import android.content.Context;
-import android.content.res.Resources;
import android.content.Intent;
+import android.content.res.Resources;
import android.net.Uri;
+import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.telecom.PhoneAccountHandle;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.MenuItem;
import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewStub;
-import android.widget.QuickContactBadge;
+import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.QuickContactBadge;
import android.widget.TextView;
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ClipboardUtils;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.dialog.CallSubjectDialog;
import com.android.contacts.common.testing.NeededForTesting;
import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
+import com.android.dialer.calllog.calllogcache.CallLogCache;
+import com.android.dialer.compat.FilteredNumberCompat;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.filterednumber.BlockNumberDialogFragment;
+import com.android.dialer.filterednumber.FilteredNumbersUtil;
+import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.ScreenEvent;
+import com.android.dialer.service.ExtendedBlockingButtonRenderer;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.PhoneNumberUtil;
-import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
import com.android.dialer.voicemail.VoicemailPlaybackLayout;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialerbind.ObjectFactory;
+import com.google.common.collect.Lists;
+
+import java.util.List;
/**
* This is an object containing references to views contained by the call log list item. This
@@ -52,7 +75,8 @@ import com.android.dialer.voicemail.VoicemailPlaybackLayout;
* This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter.
*/
public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
- implements View.OnClickListener {
+ implements View.OnClickListener, MenuItem.OnMenuItemClickListener,
+ View.OnCreateContextMenuListener {
/** The root view of the call log list item */
public final View rootView;
@@ -80,6 +104,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
public View sendMessageView;
public View detailsButtonView;
public View callWithNoteButtonView;
+ public ImageView workIconView;
/**
* The row Id for the first call associated with the call log entry. Used as a key for the
@@ -100,6 +125,11 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
public String number;
/**
+ * The post-dial numbers that are dialed following the phone number.
+ */
+ public String postDialDigits;
+
+ /**
* The formatted phone number to display.
*/
public String displayNumber;
@@ -116,12 +146,24 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
public String numberType;
/**
+ * The country iso for the call. Cached here as the call back
+ * intent is set only when the actions ViewStub is inflated.
+ */
+ public String countryIso;
+
+ /**
* The type of call for the current call log entry. Cached here as the call back
* intent is set only when the actions ViewStub is inflated.
*/
public int callType;
/**
+ * ID for blocked numbers database.
+ * Set when context menu is created, if the number is blocked.
+ */
+ public Integer blockId;
+
+ /**
* The account for the current call log entry. Cached here as the call back
* intent is set only when the actions ViewStub is inflated.
*/
@@ -141,6 +183,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
public CharSequence nameOrNumber;
/**
+ * The call type or Location associated with the call. Cached here for use when setting text
+ * for a voicemail log's call button
+ */
+ public CharSequence callTypeOrLocation;
+
+ /**
* Whether this row is for a business or not.
*/
public boolean isBusiness;
@@ -150,38 +198,57 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
*/
public ContactInfo info;
- private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10;
+ /**
+ * Whether the current log entry is a blocked number or not. Used in updatePhoto()
+ */
+ public boolean isBlocked;
+
+ /**
+ * Whether this is the archive tab or not.
+ */
+ public final boolean isArchiveTab;
private final Context mContext;
- private final TelecomCallLogCache mTelecomCallLogCache;
+ private final CallLogCache mCallLogCache;
private final CallLogListItemHelper mCallLogListItemHelper;
private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+
+ private final BlockNumberDialogFragment.Callback mFilteredNumberDialogCallback;
private final int mPhotoSize;
+ private ViewStub mExtendedBlockingViewStub;
+ private final ExtendedBlockingButtonRenderer mExtendedBlockingButtonRenderer;
private View.OnClickListener mExpandCollapseListener;
private boolean mVoicemailPrimaryActionButtonClicked;
private CallLogListItemViewHolder(
Context context,
+ ExtendedBlockingButtonRenderer.Listener eventListener,
View.OnClickListener expandCollapseListener,
- TelecomCallLogCache telecomCallLogCache,
+ CallLogCache callLogCache,
CallLogListItemHelper callLogListItemHelper,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
+ BlockNumberDialogFragment.Callback filteredNumberDialogCallback,
View rootView,
QuickContactBadge quickContactView,
View primaryActionView,
PhoneCallDetailsViews phoneCallDetailsViews,
CardView callLogEntryView,
TextView dayGroupHeader,
- ImageView primaryActionButtonView) {
+ ImageView primaryActionButtonView,
+ boolean isArchiveTab) {
super(rootView);
mContext = context;
mExpandCollapseListener = expandCollapseListener;
- mTelecomCallLogCache = telecomCallLogCache;
+ mCallLogCache = callLogCache;
mCallLogListItemHelper = callLogListItemHelper;
mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
+ mFilteredNumberDialogCallback = filteredNumberDialogCallback;
this.rootView = rootView;
this.quickContactView = quickContactView;
@@ -190,7 +257,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
this.callLogEntryView = callLogEntryView;
this.dayGroupHeader = dayGroupHeader;
this.primaryActionButtonView = primaryActionButtonView;
-
+ this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon);
+ this.isArchiveTab = isArchiveTab;
Resources resources = mContext.getResources();
mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
@@ -198,49 +266,151 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
phoneCallDetailsViews.nameView.setElegantTextHeight(false);
phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);
- quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
-
+ quickContactView.setOverlay(null);
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
primaryActionButtonView.setOnClickListener(this);
primaryActionView.setOnClickListener(mExpandCollapseListener);
+ primaryActionView.setOnCreateContextMenuListener(this);
+ mExtendedBlockingButtonRenderer =
+ ObjectFactory.newExtendedBlockingButtonRenderer(mContext, eventListener);
}
public static CallLogListItemViewHolder create(
View view,
Context context,
+ ExtendedBlockingButtonRenderer.Listener eventListener,
View.OnClickListener expandCollapseListener,
- TelecomCallLogCache telecomCallLogCache,
+ CallLogCache callLogCache,
CallLogListItemHelper callLogListItemHelper,
- VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+ VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
+ BlockNumberDialogFragment.Callback filteredNumberDialogCallback,
+ boolean isArchiveTab) {
return new CallLogListItemViewHolder(
context,
+ eventListener,
expandCollapseListener,
- telecomCallLogCache,
+ callLogCache,
callLogListItemHelper,
voicemailPlaybackPresenter,
+ filteredNumberAsyncQueryHandler,
+ filteredNumberDialogCallback,
view,
(QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
view.findViewById(R.id.primary_action_view),
PhoneCallDetailsViews.fromView(view),
(CardView) view.findViewById(R.id.call_log_row),
(TextView) view.findViewById(R.id.call_log_day_group_label),
- (ImageView) view.findViewById(R.id.primary_action_button));
+ (ImageView) view.findViewById(R.id.primary_action_button),
+ isArchiveTab);
+ }
+
+ @Override
+ public void onCreateContextMenu(
+ final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+
+ if (callType == CallLog.Calls.VOICEMAIL_TYPE) {
+ menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
+ } else {
+ menu.setHeaderTitle(PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR)));
+ }
+
+ menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard, ContextMenu.NONE,
+ R.string.action_copy_number_text)
+ .setOnMenuItemClickListener(this);
+
+ // The edit number before call does not show up if any of the conditions apply:
+ // 1) Number cannot be called
+ // 2) Number is the voicemail number
+ // 3) Number is a SIP address
+
+ if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation)
+ && !mCallLogCache.isVoicemailNumber(accountHandle, number)
+ && !PhoneNumberUtil.isSipNumber(number)) {
+ menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call, ContextMenu.NONE,
+ R.string.action_edit_number_before_call)
+ .setOnMenuItemClickListener(this);
+ }
+
+ if (callType == CallLog.Calls.VOICEMAIL_TYPE
+ && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) {
+ menu.add(ContextMenu.NONE, R.id.context_menu_copy_transcript_to_clipboard,
+ ContextMenu.NONE, R.string.copy_transcript_text)
+ .setOnMenuItemClickListener(this);
+ }
+
+ if (FilteredNumbersUtil.canBlockNumber(mContext, number, countryIso)) {
+ mFilteredNumberAsyncQueryHandler.isBlockedNumber(
+ new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ blockId = id;
+ int blockTitleId = blockId == null ? R.string.action_block_number
+ : R.string.action_unblock_number;
+ final MenuItem blockItem = menu.add(
+ ContextMenu.NONE,
+ R.id.context_menu_block_number,
+ ContextMenu.NONE,
+ blockTitleId);
+ blockItem.setOnMenuItemClickListener(
+ CallLogListItemViewHolder.this);
+ }
+ }, number, countryIso);
+ }
+
+ Logger.logScreenView(ScreenEvent.CALL_LOG_CONTEXT_MENU, (Activity) mContext);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int resId = item.getItemId();
+ if (resId == R.id.context_menu_block_number) {
+ FilteredNumberCompat
+ .showBlockNumberDialogFlow(mContext.getContentResolver(), blockId, number,
+ countryIso, displayNumber, R.id.floating_action_button_container,
+ ((Activity) mContext).getFragmentManager(),
+ mFilteredNumberDialogCallback);
+ return true;
+ } else if (resId == R.id.context_menu_copy_to_clipboard) {
+ ClipboardUtils.copyText(mContext, null, number, true);
+ return true;
+ } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) {
+ ClipboardUtils.copyText(mContext, null,
+ phoneCallDetailsViews.voicemailTranscriptionView.getText(), true);
+ return true;
+ } else if (resId == R.id.context_menu_edit_before_call) {
+ final Intent intent = new Intent(
+ Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+ intent.setClass(mContext, DialtactsActivity.class);
+ DialerUtils.startActivityWithErrorToast(mContext, intent);
+ return true;
+ }
+ return false;
}
/**
* Configures the action buttons in the expandable actions ViewStub. The ViewStub is not
* inflated during initial binding, so click handlers, tags and accessibility text must be set
* here, if necessary.
- *
- * @param callLogItem The call log list item view.
*/
public void inflateActionViewStub() {
ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub);
if (stub != null) {
- actionsView = (ViewGroup) stub.inflate();
+ actionsView = stub.inflate();
voicemailPlaybackView = (VoicemailPlaybackLayout) actionsView
.findViewById(R.id.voicemail_playback_layout);
+ if (isArchiveTab) {
+ voicemailPlaybackView.hideArchiveButton();
+ }
+
callButtonView = actionsView.findViewById(R.id.call_action);
callButtonView.setOnClickListener(this);
@@ -263,6 +433,9 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action);
callWithNoteButtonView.setOnClickListener(this);
+
+ mExtendedBlockingViewStub =
+ (ViewStub) actionsView.findViewById(R.id.extended_blocking_actions_container);
}
bindActionButtons();
@@ -273,25 +446,25 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
// Treat as voicemail list item; show play button if not expanded.
if (!isExpanded) {
primaryActionButtonView.setImageResource(R.drawable.ic_play_arrow_24dp);
+ primaryActionButtonView.setContentDescription(TextUtils.expandTemplate(
+ mContext.getString(R.string.description_voicemail_action),
+ nameOrNumber));
primaryActionButtonView.setVisibility(View.VISIBLE);
} else {
primaryActionButtonView.setVisibility(View.GONE);
}
} else {
// Treat as normal list item; show call button, if possible.
- boolean canPlaceCallToNumber =
- PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation);
-
- if (canPlaceCallToNumber) {
+ if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation)) {
boolean isVoicemailNumber =
- mTelecomCallLogCache.isVoicemailNumber(accountHandle, number);
+ mCallLogCache.isVoicemailNumber(accountHandle, number);
if (isVoicemailNumber) {
// Call to generic voicemail number, in case there are multiple accounts.
primaryActionButtonView.setTag(
IntentProvider.getReturnVoicemailCallIntentProvider());
} else {
primaryActionButtonView.setTag(
- IntentProvider.getReturnCallIntentProvider(number));
+ IntentProvider.getReturnCallIntentProvider(number + postDialDigits));
}
primaryActionButtonView.setContentDescription(TextUtils.expandTemplate(
@@ -319,13 +492,21 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
.setText(TextUtils.expandTemplate(
mContext.getString(R.string.call_log_action_call),
nameOrNumber));
+ TextView callTypeOrLocationView = ((TextView) callButtonView.findViewById(
+ R.id.call_type_or_location_text));
+ if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) {
+ callTypeOrLocationView.setText(callTypeOrLocation);
+ callTypeOrLocationView.setVisibility(View.VISIBLE);
+ } else {
+ callTypeOrLocationView.setVisibility(View.GONE);
+ }
callButtonView.setVisibility(View.VISIBLE);
} else {
callButtonView.setVisibility(View.GONE);
}
// If one of the calls had video capabilities, show the video call button.
- if (mTelecomCallLogCache.isVideoEnabled() && canPlaceCallToNumber &&
+ if (mCallLogCache.isVideoEnabled() && canPlaceCallToNumber &&
phoneCallDetailsViews.callTypeIcons.isVideoShown()) {
videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number));
videoCallButtonView.setVisibility(View.VISIBLE);
@@ -334,22 +515,29 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
}
// For voicemail calls, show the voicemail playback layout; hide otherwise.
- if (callType == Calls.VOICEMAIL_TYPE && mVoicemailPlaybackPresenter != null) {
+ if (callType == Calls.VOICEMAIL_TYPE && mVoicemailPlaybackPresenter != null
+ && !TextUtils.isEmpty(voicemailUri)) {
voicemailPlaybackView.setVisibility(View.VISIBLE);
Uri uri = Uri.parse(voicemailUri);
mVoicemailPlaybackPresenter.setPlaybackView(
voicemailPlaybackView, uri, mVoicemailPrimaryActionButtonClicked);
mVoicemailPrimaryActionButtonClicked = false;
-
- CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+ // Only mark voicemail as read when not in archive tab
+ if (!isArchiveTab) {
+ CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
+ }
} else {
voicemailPlaybackView.setVisibility(View.GONE);
}
- detailsButtonView.setVisibility(View.VISIBLE);
- detailsButtonView.setTag(
- IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ detailsButtonView.setVisibility(View.GONE);
+ } else {
+ detailsButtonView.setVisibility(View.VISIBLE);
+ detailsButtonView.setTag(
+ IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+ }
if (info != null && UriUtils.isEncodedContactUri(info.lookupUri)) {
createNewContactButtonView.setTag(IntentProvider.getAddContactIntentProvider(
@@ -364,16 +552,48 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
addToExistingContactButtonView.setVisibility(View.GONE);
}
- sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number));
+ if (canPlaceCallToNumber) {
+ sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number));
+ sendMessageView.setVisibility(View.VISIBLE);
+ } else {
+ sendMessageView.setVisibility(View.GONE);
+ }
mCallLogListItemHelper.setActionContentDescriptions(this);
boolean supportsCallSubject =
- mTelecomCallLogCache.doesAccountSupportCallSubject(accountHandle);
+ mCallLogCache.doesAccountSupportCallSubject(accountHandle);
boolean isVoicemailNumber =
- mTelecomCallLogCache.isVoicemailNumber(accountHandle, number);
+ mCallLogCache.isVoicemailNumber(accountHandle, number);
callWithNoteButtonView.setVisibility(
supportsCallSubject && !isVoicemailNumber ? View.VISIBLE : View.GONE);
+
+ if(mExtendedBlockingButtonRenderer != null){
+ List<View> completeLogListItems = Lists.newArrayList(
+ createNewContactButtonView,
+ addToExistingContactButtonView,
+ sendMessageView,
+ callButtonView,
+ callWithNoteButtonView,
+ detailsButtonView,
+ voicemailPlaybackView);
+
+ List<View> blockedNumberVisibleViews = Lists.newArrayList(detailsButtonView);
+ List<View> extendedBlockingVisibleViews = Lists.newArrayList(detailsButtonView);
+
+ ExtendedBlockingButtonRenderer.ViewHolderInfo viewHolderInfo =
+ new ExtendedBlockingButtonRenderer.ViewHolderInfo(
+ completeLogListItems,
+ extendedBlockingVisibleViews,
+ blockedNumberVisibleViews,
+ number,
+ countryIso,
+ nameOrNumber.toString(),
+ displayNumber);
+ mExtendedBlockingButtonRenderer.setViewHolderInfo(viewHolderInfo);
+
+ mExtendedBlockingButtonRenderer.render(mExtendedBlockingViewStub);
+ }
}
/**
@@ -382,7 +602,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
* If the action views have never been shown yet for this view, inflate the view stub.
*/
public void showActions(boolean show) {
- expandVoicemailTranscriptionView(show);
+ showOrHideVoicemailTranscriptionView(show);
if (show) {
// Inflate the view stub if necessary, and wire up the event handlers.
@@ -401,24 +621,23 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
updatePrimaryActionButton(show);
}
- public void expandVoicemailTranscriptionView(boolean isExpanded) {
+ public void showOrHideVoicemailTranscriptionView(boolean isExpanded) {
if (callType != Calls.VOICEMAIL_TYPE) {
return;
}
final TextView view = phoneCallDetailsViews.voicemailTranscriptionView;
- if (TextUtils.isEmpty(view.getText())) {
+ if (!isExpanded || TextUtils.isEmpty(view.getText())) {
+ view.setVisibility(View.GONE);
return;
}
- view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1);
- view.setSingleLine(!isExpanded);
+ view.setVisibility(View.VISIBLE);
}
- public void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName,
- boolean isVoicemail, boolean isBusiness) {
- quickContactView.assignContactUri(contactUri);
- quickContactView.setOverlay(null);
+ public void updatePhoto() {
+ quickContactView.assignContactUri(info.lookupUri);
+ final boolean isVoicemail = mCallLogCache.isVoicemailNumber(accountHandle, number);
int contactType = ContactPhotoManager.TYPE_DEFAULT;
if (isVoicemail) {
contactType = ContactPhotoManager.TYPE_VOICEMAIL;
@@ -426,21 +645,27 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
contactType = ContactPhotoManager.TYPE_BUSINESS;
}
- String lookupKey = null;
- if (contactUri != null) {
- lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
- }
-
- DefaultImageRequest request = new DefaultImageRequest(
+ final String lookupKey = info.lookupUri != null
+ ? UriUtils.getLookupKeyFromUri(info.lookupUri) : null;
+ final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name;
+ final DefaultImageRequest request = new DefaultImageRequest(
displayName, lookupKey, contactType, true /* isCircular */);
- if (photoId == 0 && photoUri != null) {
- ContactPhotoManager.getInstance(mContext).loadPhoto(quickContactView, photoUri,
+ if (info.photoId == 0 && info.photoUri != null) {
+ ContactPhotoManager.getInstance(mContext).loadPhoto(quickContactView, info.photoUri,
mPhotoSize, false /* darkTheme */, true /* isCircular */, request);
} else {
- ContactPhotoManager.getInstance(mContext).loadThumbnail(quickContactView, photoId,
+ ContactPhotoManager.getInstance(mContext).loadThumbnail(quickContactView, info.photoId,
false /* darkTheme */, true /* isCircular */, request);
}
+
+ if (mExtendedBlockingButtonRenderer != null) {
+ mExtendedBlockingButtonRenderer.updatePhotoAndLabelIfNecessary(
+ number,
+ countryIso,
+ quickContactView,
+ phoneCallDetailsViews.callLocationAndDate);
+ }
}
@Override
@@ -456,7 +681,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
info.lookupUri,
(String) nameOrNumber /* top line of contact view in call subject dialog */,
isBusiness,
- number, /* callable number used for ACTION_CALL intent */
+ number,
TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact
view in dialog. */
numberType, /* phone number type (e.g. mobile) in second line of contact view */
@@ -476,27 +701,32 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
@NeededForTesting
public static CallLogListItemViewHolder createForTest(Context context) {
Resources resources = context.getResources();
- TelecomCallLogCache telecomCallLogCache = new TelecomCallLogCache(context);
+ CallLogCache callLogCache =
+ CallLogCache.getCallLogCache(context);
PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
- context, resources, telecomCallLogCache);
+ context, resources, callLogCache);
CallLogListItemViewHolder viewHolder = new CallLogListItemViewHolder(
context,
+ null,
null /* expandCollapseListener */,
- telecomCallLogCache,
- new CallLogListItemHelper(phoneCallDetailsHelper, resources, telecomCallLogCache),
+ callLogCache,
+ new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache),
null /* voicemailPlaybackPresenter */,
+ null /* filteredNumberAsyncQueryHandler */,
+ null /* filteredNumberDialogCallback */,
new View(context),
new QuickContactBadge(context),
new View(context),
PhoneCallDetailsViews.createForTest(context),
new CardView(context),
new TextView(context),
- new ImageView(context));
+ new ImageView(context),
+ false);
viewHolder.detailsButtonView = new TextView(context);
viewHolder.actionsView = new View(context);
viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context);
-
+ viewHolder.workIconView = new ImageButton(context);
return viewHolder;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
index 367cb78c3..189263199 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
@@ -16,14 +16,144 @@
package com.android.dialer.calllog;
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.R;
import com.android.dialer.util.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Helper class operating on call log notifications.
*/
public class CallLogNotificationsHelper {
+ private static final String TAG = "CallLogNotifHelper";
+ private static CallLogNotificationsHelper sInstance;
+
+ /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
+ public static CallLogNotificationsHelper getInstance(Context context) {
+ if (sInstance == null) {
+ ContentResolver contentResolver = context.getContentResolver();
+ String countryIso = GeoUtil.getCurrentCountryIso(context);
+ sInstance = new CallLogNotificationsHelper(context,
+ createNewCallsQuery(context, contentResolver),
+ createNameLookupQuery(context, contentResolver),
+ new ContactInfoHelper(context, countryIso),
+ countryIso);
+ }
+ return sInstance;
+ }
+
+ private final Context mContext;
+ private final NewCallsQuery mNewCallsQuery;
+ private final NameLookupQuery mNameLookupQuery;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final String mCurrentCountryIso;
+
+ CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery,
+ NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper,
+ String countryIso) {
+ mContext = context;
+ mNewCallsQuery = newCallsQuery;
+ mNameLookupQuery = nameLookupQuery;
+ mContactInfoHelper = contactInfoHelper;
+ mCurrentCountryIso = countryIso;
+ }
+
+ /**
+ * Get all voicemails with the "new" flag set to 1.
+ *
+ * @return A list of NewCall objects where each object represents a new voicemail.
+ */
+ @Nullable
+ public List<NewCall> getNewVoicemails() {
+ return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
+ }
+
+ /**
+ * Get all missed calls with the "new" flag set to 1.
+ *
+ * @return A list of NewCall objects where each object represents a new missed call.
+ */
+ @Nullable
+ public List<NewCall> getNewMissedCalls() {
+ return mNewCallsQuery.query(Calls.MISSED_TYPE);
+ }
+
+ /**
+ * Given a number and number information (presentation and country ISO), get the best name
+ * for display. If the name is empty but we have a special presentation, display that.
+ * Otherwise attempt to look it up in the database or the cache.
+ * If that fails, fall back to displaying the number.
+ */
+ public String getName(@Nullable String number, int numberPresentation,
+ @Nullable String countryIso) {
+ return getContactInfo(number, numberPresentation, countryIso).name;
+ }
+
+ /**
+ * Given a number and number information (presentation and country ISO), get
+ * {@link ContactInfo}. If the name is empty but we have a special presentation, display that.
+ * Otherwise attempt to look it up in the cache.
+ * If that fails, fall back to displaying the number.
+ */
+ public @NonNull ContactInfo getContactInfo(@Nullable String number, int numberPresentation,
+ @Nullable String countryIso) {
+ if (countryIso == null) {
+ countryIso = mCurrentCountryIso;
+ }
+
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.number = number;
+ contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso);
+ // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo.
+ contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+
+ // 1. Special number representation.
+ contactInfo.name = PhoneNumberDisplayUtil.getDisplayName(
+ mContext,
+ number,
+ numberPresentation,
+ false).toString();
+ if (!TextUtils.isEmpty(contactInfo.name)) {
+ return contactInfo;
+ }
+
+ // 2. Look it up in the cache.
+ ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
+
+ if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
+ return cachedContactInfo;
+ }
+
+ if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
+ // 3. If we cannot lookup the contact, use the formatted number instead.
+ contactInfo.name = contactInfo.formattedNumber;
+ } else if (!TextUtils.isEmpty(number)) {
+ // 4. If number can't be formatted, use number.
+ contactInfo.name = number;
+ } else {
+ // 5. Otherwise, it's unknown number.
+ contactInfo.name = mContext.getResources().getString(R.string.unknown);
+ }
+ return contactInfo;
+ }
+
/** Removes the missed call notifications. */
public static void removeMissedCallNotifications(Context context) {
TelecomUtil.cancelMissedCallsNotification(context);
@@ -33,4 +163,188 @@ public class CallLogNotificationsHelper {
public static void updateVoicemailNotifications(Context context) {
CallLogNotificationsService.updateVoicemailNotifications(context, null);
}
+
+ /** Information about a new voicemail. */
+ public static final class NewCall {
+ public final Uri callsUri;
+ public final Uri voicemailUri;
+ public final String number;
+ public final int numberPresentation;
+ public final String accountComponentName;
+ public final String accountId;
+ public final String transcription;
+ public final String countryIso;
+ public final long dateMs;
+
+ public NewCall(
+ Uri callsUri,
+ Uri voicemailUri,
+ String number,
+ int numberPresentation,
+ String accountComponentName,
+ String accountId,
+ String transcription,
+ String countryIso,
+ long dateMs) {
+ this.callsUri = callsUri;
+ this.voicemailUri = voicemailUri;
+ this.number = number;
+ this.numberPresentation = numberPresentation;
+ this.accountComponentName = accountComponentName;
+ this.accountId = accountId;
+ this.transcription = transcription;
+ this.countryIso = countryIso;
+ this.dateMs = dateMs;
+ }
+ }
+
+ /** Allows determining the new calls for which a notification should be generated. */
+ public interface NewCallsQuery {
+ /**
+ * Returns the new calls of a certain type for which a notification should be generated.
+ */
+ @Nullable
+ public List<NewCall> query(int type);
+ }
+
+ /** Create a new instance of {@link NewCallsQuery}. */
+ public static NewCallsQuery createNewCallsQuery(Context context,
+ ContentResolver contentResolver) {
+
+ return new DefaultNewCallsQuery(context.getApplicationContext(), 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,
+ Calls.NUMBER_PRESENTATION,
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME,
+ Calls.PHONE_ACCOUNT_ID,
+ Calls.TRANSCRIPTION,
+ Calls.COUNTRY_ISO,
+ Calls.DATE
+ };
+ 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 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
+ private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
+ private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
+ private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
+ private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
+ private static final int DATE_COLUMN_INDEX = 8;
+
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+
+ private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
+ mContext = context;
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ @Nullable
+ public List<NewCall> query(int type) {
+ if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
+ Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+ return null;
+ }
+ final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+ final String[] selectionArgs = new String[]{ Integer.toString(type) };
+ try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL,
+ PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) {
+ if (cursor == null) {
+ return null;
+ }
+ List<NewCall> newCalls = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ newCalls.add(createNewCallsFromCursor(cursor));
+ }
+ return newCalls;
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+ return null;
+ }
+ }
+
+ /** 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),
+ cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
+ cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
+ cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
+ cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
+ cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
+ cursor.getLong(DATE_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.
+ */
+ @Nullable
+ public String query(@Nullable String number);
+ }
+
+ /** Create a new instance of {@link NameLookupQuery}. */
+ public static NameLookupQuery createNameLookupQuery(Context context,
+ ContentResolver contentResolver) {
+ return new DefaultNameLookupQuery(context.getApplicationContext(), 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 final Context mContext;
+
+ private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
+ mContext = context;
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ @Nullable
+ public String query(@Nullable String number) {
+ if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CONTACTS)) {
+ Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
+ return null;
+ }
+ try (Cursor 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);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
+ return null;
+ }
+ }
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsService.java b/src/com/android/dialer/calllog/CallLogNotificationsService.java
index 9a67b61b6..4ff9576ca 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsService.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsService.java
@@ -26,15 +26,16 @@ import com.android.contacts.common.util.PermissionsUtil;
import com.android.dialer.util.TelecomUtil;
/**
- * Provides operations for managing notifications.
+ * Provides operations for managing call-related 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>
+ * <li>Updating voicemail notifications</li>
+ * <li>Marking new voicemails as old</li>
+ * <li>Updating missed call notifications</li>
+ * <li>Marking new missed calls as old</li>
+ * <li>Calling back from a missed call</li>
+ * <li>Sending an SMS from a missed call</li>
* </ul>
*/
public class CallLogNotificationsService extends IntentService {
@@ -45,21 +46,62 @@ public class CallLogNotificationsService extends IntentService {
"com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
/**
- * Action to update the notifications.
+ * Action to update voicemail 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";
+ public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
/**
- * Extra to included with {@link #ACTION_UPDATE_NOTIFICATIONS} to identify the new voicemail
- * that triggered an update.
+ * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_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";
+ /**
+ * Action to update the missed call notifications.
+ * <p>
+ * Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and
+ * {@link #EXTRA_MISSED_CALL_COUNT}.
+ */
+ public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+
+ /** Action to mark all the new missed calls as old. */
+ public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
+ "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+
+ /** Action to call back a missed call. */
+ public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
+
+ public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION";
+
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS},
+ * {@link #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and
+ * {@link #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display,
+ * call or text back.
+ * <p>
+ * It must be a {@link String}.
+ */
+ public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER";
+
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the
+ * number of missed calls.
+ * <p>
+ * It must be a {@link Integer}
+ */
+ public static final String EXTRA_MISSED_CALL_COUNT =
+ "MISSED_CALL_COUNT";
+
+ public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
+
private VoicemailQueryHandler mVoicemailQueryHandler;
public CallLogNotificationsService() {
@@ -67,12 +109,6 @@ public class CallLogNotificationsService extends IntentService {
}
@Override
- public void onCreate() {
- super.onCreate();
- mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
- }
-
- @Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
Log.d(TAG, "onHandleIntent: could not handle null intent");
@@ -83,13 +119,38 @@ public class CallLogNotificationsService extends IntentService {
return;
}
- if (ACTION_MARK_NEW_VOICEMAILS_AS_OLD.equals(intent.getAction())) {
- mVoicemailQueryHandler.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);
+ String action = intent.getAction();
+ switch (action) {
+ case ACTION_MARK_NEW_VOICEMAILS_AS_OLD:
+ if (mVoicemailQueryHandler == null) {
+ mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
+ }
+ mVoicemailQueryHandler.markNewVoicemailsAsOld();
+ break;
+ case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
+ Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+ DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+ break;
+ case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
+ int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT,
+ UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER);
+ MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
+ break;
+ case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
+ CallLogNotificationsHelper.removeMissedCallNotifications(this);
+ break;
+ case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this).callBackFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this).sendSmsFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ default:
+ Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+ break;
}
}
@@ -103,7 +164,8 @@ public class CallLogNotificationsService extends IntentService {
public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
- serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
// If voicemailUri is null, then notifications for all voicemails will be updated.
if (voicemailUri != null) {
serviceIntent.putExtra(
@@ -112,4 +174,21 @@ public class CallLogNotificationsService extends IntentService {
context.startService(serviceIntent);
}
}
+
+ /**
+ * Updates notifications for any new missed calls.
+ *
+ * @param context A valid context.
+ * @param count The number of new missed calls.
+ * @param number The phone number of the newest missed call.
+ */
+ public static void updateMissedCallNotifications(Context context, int count,
+ String number) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number);
+ context.startService(serviceIntent);
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogQuery.java b/src/com/android/dialer/calllog/CallLogQuery.java
index 2b43c2857..4900354bf 100644
--- a/src/com/android/dialer/calllog/CallLogQuery.java
+++ b/src/com/android/dialer/calllog/CallLogQuery.java
@@ -16,13 +16,22 @@
package com.android.dialer.calllog;
+import com.google.common.collect.Lists;
+
import android.provider.CallLog.Calls;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.dialer.compat.CallsSdkCompat;
+import com.android.dialer.compat.DialerCompatUtils;
+
+import java.util.List;
+
/**
* The query for the call log table.
*/
public final class CallLogQuery {
- public static final String[] _PROJECTION = new String[] {
+
+ private static final String[] _PROJECTION_INTERNAL = new String[] {
Calls._ID, // 0
Calls.NUMBER, // 1
Calls.DATE, // 2
@@ -46,7 +55,6 @@ public final class CallLogQuery {
Calls.FEATURES, // 20
Calls.DATA_USAGE, // 21
Calls.TRANSCRIPTION, // 22
- Calls.CACHED_PHOTO_URI // 23
};
public static final int ID = 0;
@@ -72,5 +80,33 @@ public final class CallLogQuery {
public static final int FEATURES = 20;
public static final int DATA_USAGE = 21;
public static final int TRANSCRIPTION = 22;
- public static final int CACHED_PHOTO_URI = 23;
+
+ // Indices for columns that may not be available, depending on the Sdk Version
+ /**
+ * Only available in versions >= M
+ * Call {@link DialerCompatUtils#isCallsCachedPhotoUriCompatible()} prior to use
+ */
+ public static int CACHED_PHOTO_URI = -1;
+
+ /**
+ * Only available in versions > M
+ * Call {@link CompatUtils#isNCompatible()} prior to use
+ */
+ public static int POST_DIAL_DIGITS = -1;
+
+ public static final String[] _PROJECTION;
+
+ static {
+ List<String> projectionList = Lists.newArrayList(_PROJECTION_INTERNAL);
+ if (DialerCompatUtils.isCallsCachedPhotoUriCompatible()) {
+ projectionList.add(Calls.CACHED_PHOTO_URI);
+ CACHED_PHOTO_URI = projectionList.size() - 1;
+ }
+ if (CompatUtils.isNCompatible()) {
+ projectionList.add(CallsSdkCompat.POST_DIAL_DIGITS);
+ POST_DIAL_DIGITS = projectionList.size() - 1;
+ }
+ _PROJECTION = projectionList.toArray(new String[projectionList.size()]);
+ }
+
}
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
index 60bdcff46..cf86bad7f 100644
--- a/src/com/android/dialer/calllog/CallLogQueryHandler.java
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -26,6 +26,7 @@ import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -34,8 +35,11 @@ import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
import android.util.Log;
+import com.android.contacts.common.compat.SdkVersionOverride;
import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.database.VoicemailArchiveContract;
+import com.android.dialer.util.AppCompatConstants;
import com.android.dialer.util.TelecomUtil;
import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
@@ -46,8 +50,6 @@ import java.util.List;
/** Handles asynchronous queries to the call log. */
public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
- 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;
@@ -59,6 +61,12 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 56;
/** The token for the query to fetch voicemail status messages. */
private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
+ /** The token for the query to fetch the number of unread voicemails. */
+ private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
+ /** The token for the query to fetch the number of missed calls. */
+ private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
+ /** The oken for the query to fetch the archived voicemails. */
+ private static final int QUERY_VOICEMAIL_ARCHIVE = 60;
private final int mLogLimit;
@@ -122,6 +130,17 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
}
/**
+ * Fetch all the voicemails in the voicemail archive.
+ */
+ public void fetchVoicemailArchive() {
+ startQuery(QUERY_VOICEMAIL_ARCHIVE, null,
+ VoicemailArchiveContract.VoicemailArchive.CONTENT_URI,
+ null, VoicemailArchiveContract.VoicemailArchive.ARCHIVED + " = 1", null,
+ VoicemailArchiveContract.VoicemailArchive.DATE + " DESC");
+ }
+
+
+ /**
* Fetches the list of calls from the call log for a given type.
* This call ignores the new or old state.
* <p>
@@ -147,36 +166,44 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
}
}
+ public void fetchVoicemailUnreadCount() {
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+ // Only count voicemails that have not been read and have not been deleted.
+ startQuery(QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN, null, Voicemails.CONTENT_URI,
+ new String[] { Voicemails._ID },
+ Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0", null, null);
+ }
+ }
+
/** Fetches the list of calls in the call log. */
private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
- // 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".
StringBuilder where = new StringBuilder();
List<String> selectionArgs = Lists.newArrayList();
+ // Always hide blocked calls.
+ where.append("(").append(Calls.TYPE).append(" != ?)");
+ selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
+
// Ignore voicemails marked as deleted
- where.append(Voicemails.DELETED);
- where.append(" = 0");
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
+ >= Build.VERSION_CODES.M) {
+ where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
+ }
if (newOnly) {
- where.append(" AND ");
- where.append(Calls.NEW);
- where.append(" = 1");
+ where.append(" AND (").append(Calls.NEW).append(" = 1)");
}
if (callType > CALL_TYPE_ALL) {
- where.append(" AND ");
- where.append(String.format("(%s = ?)", Calls.TYPE));
+ where.append(" AND (").append(Calls.TYPE).append(" = ?)");
selectionArgs.add(Integer.toString(callType));
} else {
where.append(" AND NOT ");
- where.append("(" + Calls.TYPE + " = " + Calls.VOICEMAIL_TYPE + ")");
+ where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
}
if (newerThan > 0) {
- where.append(" AND ");
- where.append(String.format("(%s > ?)", Calls.DATE));
+ where.append(" AND (").append(Calls.DATE).append(" > ?)");
selectionArgs.add(Long.toString(newerThan));
}
@@ -185,9 +212,8 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
Uri uri = TelecomUtil.getCallLogUri(mContext).buildUpon()
.appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
.build();
- startQuery(token, null, uri,
- CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
- Calls.DEFAULT_SORT_ORDER);
+ startQuery(token, null, uri, CallLogQuery._PROJECTION, selection, selectionArgs.toArray(
+ new String[selectionArgs.size()]), Calls.DEFAULT_SORT_ORDER);
}
/** Cancel any pending fetch request. */
@@ -217,19 +243,25 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
if (!PermissionsUtil.hasPhonePermissions(mContext)) {
return;
}
- // 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);
+ getUnreadMissedCallsQuery(), null);
+ }
+
+ /** Fetch all missed calls received since last time the tab was opened. */
+ public void fetchMissedCallsUnreadCount() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+
+ startQuery(QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN, null, Calls.CONTENT_URI,
+ new String[]{Calls._ID}, getUnreadMissedCallsQuery(), null, null);
}
+
@Override
protected synchronized void onNotNullableQueryComplete(int token, Object cookie,
Cursor cursor) {
@@ -237,12 +269,16 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
return;
}
try {
- if (token == QUERY_CALLLOG_TOKEN) {
+ if (token == QUERY_CALLLOG_TOKEN || token == QUERY_VOICEMAIL_ARCHIVE) {
if (updateAdapterData(cursor)) {
cursor = null;
}
} else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
updateVoicemailStatus(cursor);
+ } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
+ updateVoicemailUnreadCount(cursor);
+ } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
+ updateMissedCallsUnreadCount(cursor);
} else {
Log.w(TAG, "Unknown query completed: ignoring: " + token);
}
@@ -266,6 +302,17 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
}
+ /**
+ * @return Query string to get all unread missed calls.
+ */
+ private String getUnreadMissedCallsQuery() {
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.IS_READ).append(" = 0 OR ").append(Calls.IS_READ).append(" IS NULL");
+ where.append(" AND ");
+ where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+ return where.toString();
+ }
+
private void updateVoicemailStatus(Cursor statusCursor) {
final Listener listener = mListener.get();
if (listener != null) {
@@ -273,11 +320,31 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
}
}
+ private void updateVoicemailUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailUnreadCountFetched(statusCursor);
+ }
+ }
+
+ private void updateMissedCallsUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onMissedCallsUnreadCountFetched(statusCursor);
+ }
+ }
+
/** Listener to completion of various queries. */
public interface Listener {
/** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
void onVoicemailStatusFetched(Cursor statusCursor);
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
+ void onVoicemailUnreadCountFetched(Cursor cursor);
+
+ /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
+ void onMissedCallsUnreadCountFetched(Cursor cursor);
+
/**
* Called when {@link CallLogQueryHandler#fetchCalls(int)} complete.
* Returns true if takes ownership of cursor.
diff --git a/src/com/android/dialer/calllog/CallTypeHelper.java b/src/com/android/dialer/calllog/CallTypeHelper.java
index 36c0975bd..acc114c5c 100644
--- a/src/com/android/dialer/calllog/CallTypeHelper.java
+++ b/src/com/android/dialer/calllog/CallTypeHelper.java
@@ -17,9 +17,9 @@
package com.android.dialer.calllog;
import android.content.res.Resources;
-import android.provider.CallLog.Calls;
import com.android.dialer.R;
+import com.android.dialer.util.AppCompatConstants;
/**
* Helper class to perform operations related to call types.
@@ -39,6 +39,10 @@ public class CallTypeHelper {
private final CharSequence mMissedVideoName;
/** Name used to identify voicemail calls. */
private final CharSequence mVoicemailName;
+ /** Name used to identify rejected calls. */
+ private final CharSequence mRejectedName;
+ /** Name used to identify blocked calls. */
+ private final CharSequence mBlockedName;
/** Color used to identify new missed calls. */
private final int mNewMissedColor;
/** Color used to identify new voicemail calls. */
@@ -53,6 +57,8 @@ public class CallTypeHelper {
mOutgoingVideoName = resources.getString(R.string.type_outgoing_video);
mMissedVideoName = resources.getString(R.string.type_missed_video);
mVoicemailName = resources.getString(R.string.type_voicemail);
+ mRejectedName = resources.getString(R.string.type_rejected);
+ mBlockedName = resources.getString(R.string.type_blocked);
mNewMissedColor = resources.getColor(R.color.call_log_missed_call_highlight_color);
mNewVoicemailColor = resources.getColor(R.color.call_log_voicemail_highlight_color);
}
@@ -60,30 +66,36 @@ public class CallTypeHelper {
/** Returns the text used to represent the given call type. */
public CharSequence getCallTypeText(int callType, boolean isVideoCall) {
switch (callType) {
- case Calls.INCOMING_TYPE:
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
if (isVideoCall) {
return mIncomingVideoName;
} else {
return mIncomingName;
}
- case Calls.OUTGOING_TYPE:
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
if (isVideoCall) {
return mOutgoingVideoName;
} else {
return mOutgoingName;
}
- case Calls.MISSED_TYPE:
+ case AppCompatConstants.CALLS_MISSED_TYPE:
if (isVideoCall) {
return mMissedVideoName;
} else {
return mMissedName;
}
- case Calls.VOICEMAIL_TYPE:
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
return mVoicemailName;
+ case AppCompatConstants.CALLS_REJECTED_TYPE:
+ return mRejectedName;
+
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ return mBlockedName;
+
default:
return mMissedName;
}
@@ -92,18 +104,18 @@ public class CallTypeHelper {
/** 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:
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
// New incoming calls are not highlighted.
return null;
- case Calls.OUTGOING_TYPE:
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
// New outgoing calls are not highlighted.
return null;
- case Calls.MISSED_TYPE:
+ case AppCompatConstants.CALLS_MISSED_TYPE:
return mNewMissedColor;
- case Calls.VOICEMAIL_TYPE:
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
return mNewVoicemailColor;
default:
@@ -115,7 +127,8 @@ public class CallTypeHelper {
}
public static boolean isMissedCallType(int callType) {
- return (callType != Calls.INCOMING_TYPE && callType != Calls.OUTGOING_TYPE &&
- callType != Calls.VOICEMAIL_TYPE);
+ return (callType != AppCompatConstants.CALLS_INCOMING_TYPE
+ && callType != AppCompatConstants.CALLS_OUTGOING_TYPE
+ && callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE);
}
}
diff --git a/src/com/android/dialer/calllog/CallTypeIconsView.java b/src/com/android/dialer/calllog/CallTypeIconsView.java
index 31d4f4b0e..cfd8f9748 100644
--- a/src/com/android/dialer/calllog/CallTypeIconsView.java
+++ b/src/com/android/dialer/calllog/CallTypeIconsView.java
@@ -23,13 +23,13 @@ import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import android.provider.CallLog.Calls;
import android.util.AttributeSet;
import android.view.View;
import com.android.contacts.common.testing.NeededForTesting;
import com.android.contacts.common.util.BitmapUtil;
import com.android.dialer.R;
+import com.android.dialer.util.AppCompatConstants;
import com.google.common.collect.Lists;
import java.util.List;
@@ -106,14 +106,16 @@ public class CallTypeIconsView extends View {
private Drawable getCallTypeDrawable(int callType) {
switch (callType) {
- case Calls.INCOMING_TYPE:
+ case AppCompatConstants.CALLS_INCOMING_TYPE:
return mResources.incoming;
- case Calls.OUTGOING_TYPE:
+ case AppCompatConstants.CALLS_OUTGOING_TYPE:
return mResources.outgoing;
- case Calls.MISSED_TYPE:
+ case AppCompatConstants.CALLS_MISSED_TYPE:
return mResources.missed;
- case Calls.VOICEMAIL_TYPE:
+ case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
return mResources.voicemail;
+ case AppCompatConstants.CALLS_BLOCKED_TYPE:
+ return mResources.blocked;
default:
// It is possible for users to end up with calls with unknown call types in their
// call history, possibly due to 3rd party call log implementations (e.g. to
@@ -150,29 +152,22 @@ public class CallTypeIconsView extends View {
private static class Resources {
- /**
- * Drawable representing an incoming answered call.
- */
+ // Drawable representing an incoming answered call.
public final Drawable incoming;
- /**
- * Drawable respresenting an outgoing call.
- */
+ // Drawable respresenting an outgoing call.
public final Drawable outgoing;
- /**
- * Drawable representing an incoming missed call.
- */
+ // Drawable representing an incoming missed call.
public final Drawable missed;
- /**
- * Drawable representing a voicemail.
- */
+ // Drawable representing a voicemail.
public final Drawable voicemail;
- /**
- * Drawable repesenting a video call.
- */
+ // Drawable representing a blocked call.
+ public final Drawable blocked;
+
+ // Drawable repesenting a video call.
public final Drawable videoCall;
/**
@@ -204,21 +199,26 @@ public class CallTypeIconsView extends View {
voicemail = r.getDrawable(R.drawable.ic_call_voicemail_holo_dark);
- // Get the video call icon, scaled to match the height of the call arrows.
- // We want the video call icon to be the same height as the call arrows, while keeping
- // the same width aspect ratio.
- Bitmap videoIcon = BitmapFactory.decodeResource(context.getResources(),
- R.drawable.ic_videocam_24dp);
- int scaledHeight = missed.getIntrinsicHeight();
- int scaledWidth = (int) ((float) videoIcon.getWidth() *
- ((float) missed.getIntrinsicHeight() /
- (float) videoIcon.getHeight()));
- Bitmap scaled = Bitmap.createScaledBitmap(videoIcon, scaledWidth, scaledHeight, false);
- videoCall = new BitmapDrawable(context.getResources(), scaled);
+ blocked = getScaledBitmap(context, R.drawable.ic_block_24dp);
+ blocked.setColorFilter(r.getColor(R.color.blocked_call), PorterDuff.Mode.MULTIPLY);
+
+ videoCall = getScaledBitmap(context, R.drawable.ic_videocam_24dp);
videoCall.setColorFilter(r.getColor(R.color.dialtacts_secondary_text_color),
PorterDuff.Mode.MULTIPLY);
iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
}
+
+ // Gets the icon, scaled to the height of the call type icons. This helps display all the
+ // icons to be the same height, while preserving their width aspect ratio.
+ private Drawable getScaledBitmap(Context context, int resourceId) {
+ Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resourceId);
+ int scaledHeight =
+ context.getResources().getDimensionPixelSize(R.dimen.call_type_icon_size);
+ int scaledWidth = (int) ((float) icon.getWidth()
+ * ((float) scaledHeight / (float) icon.getHeight()));
+ Bitmap scaledIcon = Bitmap.createScaledBitmap(icon, scaledWidth, scaledHeight, false);
+ return new BitmapDrawable(context.getResources(), scaledIcon);
+ }
}
}
diff --git a/src/com/android/dialer/calllog/ContactInfo.java b/src/com/android/dialer/calllog/ContactInfo.java
index 357c832cf..8fe4964bc 100644
--- a/src/com/android/dialer/calllog/ContactInfo.java
+++ b/src/com/android/dialer/calllog/ContactInfo.java
@@ -19,6 +19,7 @@ package com.android.dialer.calllog;
import android.net.Uri;
import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
import com.android.contacts.common.util.UriUtils;
import com.google.common.base.Objects;
@@ -34,10 +35,20 @@ public class ContactInfo {
*/
public String lookupKey;
public String name;
+ public String nameAlternative;
public int type;
public String label;
public String number;
public String formattedNumber;
+ /*
+ * ContactInfo.normalizedNumber is a column value returned by PhoneLookup query. By definition,
+ * it's E164 representation.
+ * http://developer.android.com/reference/android/provider/ContactsContract.PhoneLookupColumns.
+ * html#NORMALIZED_NUMBER.
+ *
+ * The fallback value, when PhoneLookup fails or else, should be either null or
+ * PhoneNumberUtils.formatNumberToE164.
+ */
public String normalizedNumber;
/** The photo for the contact, if available. */
public long photoId;
@@ -45,6 +56,7 @@ public class ContactInfo {
public Uri photoUri;
public boolean isBadData;
public String objectId;
+ public @UserType long userType;
public static ContactInfo EMPTY = new ContactInfo();
@@ -70,6 +82,7 @@ public class ContactInfo {
ContactInfo other = (ContactInfo) obj;
if (!UriUtils.areEqual(lookupUri, other.lookupUri)) return false;
if (!TextUtils.equals(name, other.name)) return false;
+ if (!TextUtils.equals(nameAlternative, other.nameAlternative)) return false;
if (type != other.type) return false;
if (!TextUtils.equals(label, other.label)) return false;
if (!TextUtils.equals(number, other.number)) return false;
@@ -78,14 +91,18 @@ public class ContactInfo {
if (photoId != other.photoId) return false;
if (!UriUtils.areEqual(photoUri, other.photoUri)) return false;
if (!TextUtils.equals(objectId, other.objectId)) return false;
+ if (userType != other.userType) return false;
return true;
}
@Override
public String toString() {
- return Objects.toStringHelper(this).add("lookupUri", lookupUri).add("name", name).add(
- "type", type).add("label", label).add("number", number).add("formattedNumber",
- formattedNumber).add("normalizedNumber", normalizedNumber).add("photoId", photoId)
- .add("photoUri", photoUri).add("objectId", objectId).toString();
+ return Objects.toStringHelper(this).add("lookupUri", lookupUri).add("name", name)
+ .add("nameAlternative", nameAlternative)
+ .add("type", type).add("label", label)
+ .add("number", number).add("formattedNumber",formattedNumber)
+ .add("normalizedNumber", normalizedNumber).add("photoId", photoId)
+ .add("photoUri", photoUri).add("objectId", objectId)
+ .add("userType",userType).toString();
}
}
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index 2e07a03b1..6e84a92f9 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -25,14 +25,19 @@ import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.util.Constants;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.common.util.PhoneNumberHelper;
import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.compat.DialerCompatUtils;
import com.android.dialer.service.CachedNumberLookupService;
import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
import com.android.dialer.util.TelecomUtil;
@@ -41,8 +46,6 @@ import com.android.dialerbind.ObjectFactory;
import org.json.JSONException;
import org.json.JSONObject;
-import java.util.List;
-
/**
* Utility class to look up the contact information for a given number.
*/
@@ -71,34 +74,27 @@ public class ContactInfoHelper {
* @param number the number to look up
* @param countryIso the country associated with this number
*/
+ @Nullable
public ContactInfo lookupNumber(String number, String countryIso) {
if (TextUtils.isEmpty(number)) {
return null;
}
- final ContactInfo info;
- // Determine the contact info.
+ ContactInfo info;
+
if (PhoneNumberHelper.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.
+ // The number is a SIP address..
+ info = lookupContactFromUri(getContactInfoLookupUri(number), true);
+ if (info == null || info == ContactInfo.EMPTY) {
+ // If lookup failed, check if the "username" of the SIP address is a phone number.
String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
- sipInfo = queryContactInfoForPhoneNumber(username, countryIso);
+ info = queryContactInfoForPhoneNumber(username, countryIso, true);
}
}
- 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;
+ info = queryContactInfoForPhoneNumber(number, countryIso, false);
}
final ContactInfo updatedInfo;
@@ -159,67 +155,82 @@ public class ContactInfoHelper {
* The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
* value.
*/
- private ContactInfo lookupContactFromUri(Uri uri) {
+ ContactInfo lookupContactFromUri(Uri uri, boolean isSip) {
if (uri == null) {
return null;
}
if (!PermissionsUtil.hasContactsPermissions(mContext)) {
return ContactInfo.EMPTY;
}
- 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.lookupKey = lookupKey;
- 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();
+
+ Cursor phoneLookupCursor = null;
+ try {
+ String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
+ phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null,
+ null);
+ } catch (NullPointerException e) {
+ // Trap NPE from pre-N CP2
+ return null;
+ }
+ if (phoneLookupCursor == null) {
+ return null;
+ }
+
+ try {
+ if (!phoneLookupCursor.moveToFirst()) {
+ return ContactInfo.EMPTY;
}
- } else {
- // Failed to fetch the data, ignore this request.
- info = null;
+ String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+ ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
+ contactInfo.nameAlternative = lookUpDisplayNameAlternative(mContext, lookupKey,
+ contactInfo.userType);
+ return contactInfo;
+ } finally {
+ phoneLookupCursor.close();
}
+ }
+
+ private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
+ ContactInfo info = new ContactInfo();
+ info.lookupKey = lookupKey;
+ info.lookupUri = Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID),
+ lookupKey);
+ info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
+ info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
+ info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+ info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
+ info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
+ info.formattedNumber = null;
+ info.userType = ContactsUtils.determineUserType(null,
+ phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
+
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) {
- if (TextUtils.isEmpty(sipAddress)) {
+ public static String lookUpDisplayNameAlternative(Context context, String lookupKey,
+ @UserType long userType) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
return null;
}
- final ContactInfo info;
+ final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(uri,
+ PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
- // "contactNumber" is a SIP address, so use the PhoneLookup table with the SIP parameter.
- Uri.Builder uriBuilder = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon();
- uriBuilder.appendPath(Uri.encode(sipAddress));
- uriBuilder.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, "1");
- return lookupContactFromUri(uriBuilder.build());
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return null;
}
/**
@@ -231,25 +242,13 @@ public class ContactInfoHelper {
* <p>
* If the lookup fails for some other reason, it returns null.
*/
- private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso) {
+ private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso,
+ boolean isSip) {
if (TextUtils.isEmpty(number)) {
return null;
}
- 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.ENTERPRISE_CONTENT_FILTER_URI,
- Uri.encode(contactNumber));
- ContactInfo info = lookupContactFromUri(uri);
+ ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number), isSip);
if (info != null && info != ContactInfo.EMPTY) {
info.formattedNumber = formatPhoneNumber(number, null, countryIso);
} else if (mCachedNumberLookupService != null) {
@@ -345,7 +344,8 @@ public class ContactInfoHelper {
final Uri updatedPhotoUriContactsOnly =
UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
- if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
+ if (DialerCompatUtils.isCallsCachedPhotoUriCompatible() &&
+ !UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
values.put(Calls.CACHED_PHOTO_URI,
UriUtils.uriToString(updatedPhotoUriContactsOnly));
needsUpdate = true;
@@ -364,8 +364,10 @@ public class ContactInfoHelper {
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_PHOTO_URI, UriUtils.uriToString(
- UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+ if (DialerCompatUtils.isCallsCachedPhotoUriCompatible()) {
+ values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
+ UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+ }
values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
needsUpdate = true;
}
@@ -393,6 +395,34 @@ public class ContactInfoHelper {
}
}
+ public static Uri getContactInfoLookupUri(String number) {
+ return getContactInfoLookupUri(number, -1);
+ }
+
+ public static Uri getContactInfoLookupUri(String number, long directoryId) {
+ // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
+ // the number is a SIP number.
+ Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
+ if (!ContactsUtils.FLAG_N_FEATURE) {
+ if (directoryId != -1) {
+ // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
+ uri = PhoneLookup.CONTENT_FILTER_URI;
+ } else {
+ // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+ number = Uri.encode(number);
+ }
+ }
+ Uri.Builder builder = uri.buildUpon()
+ .appendPath(number)
+ .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(PhoneNumberHelper.isUriNumber(number)));
+ if (directoryId != -1) {
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(directoryId));
+ }
+ return builder.build();
+ }
+
/**
* Returns the contact information stored in an entry of the call log.
*
@@ -400,17 +430,22 @@ public class ContactInfoHelper {
*/
public static ContactInfo getContactInfo(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;
+ String postDialDigits = CompatUtils.isNCompatible()
+ ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ info.number = (matchedNumber == null) ?
+ c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
+
info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
- info.photoUri = UriUtils.nullForNonContactsUri(
- UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
+ info.photoUri = DialerCompatUtils.isCallsCachedPhotoUriCompatible() ?
+ UriUtils.nullForNonContactsUri(
+ UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)))
+ : null;
info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
return info;
@@ -439,6 +474,4 @@ public class ContactInfoHelper {
return mCachedNumberLookupService != null
&& mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
}
-
-
}
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index a6d165e3a..de6fc6a3d 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -16,39 +16,45 @@
package com.android.dialer.calllog;
-import static android.Manifest.permission.READ_CALL_LOG;
-import static android.Manifest.permission.READ_CONTACTS;
+import com.google.common.collect.Maps;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.ComponentName;
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.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
-import com.android.common.io.MoreCloseables;
-import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
-import com.android.dialer.calllog.PhoneAccountUtils;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.filterednumber.FilteredNumbersUtil;
import com.android.dialer.list.ListsFragment;
-import com.google.common.collect.Maps;
+import com.android.dialer.util.TelecomUtil;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
/**
- * VoicemailNotifier that shows a notification in the status bar.
+ * Shows a voicemail notification in the status bar.
*/
public class DefaultVoicemailNotifier {
- public static final String TAG = "DefaultVoicemailNotifier";
+ public static final String TAG = "VoicemailNotifier";
/** The tag used to identify notifications from this class. */
private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
@@ -59,30 +65,18 @@ public class DefaultVoicemailNotifier {
private static DefaultVoicemailNotifier sInstance;
private final Context mContext;
- private final NotificationManager mNotificationManager;
- private final NewCallsQuery mNewCallsQuery;
- private final NameLookupQuery mNameLookupQuery;
/** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
- public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
+ public static 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(context, contentResolver),
- createNameLookupQuery(context, contentResolver));
+ sInstance = new DefaultVoicemailNotifier(context);
}
return sInstance;
}
- private DefaultVoicemailNotifier(Context context,
- NotificationManager notificationManager, NewCallsQuery newCallsQuery,
- NameLookupQuery nameLookupQuery) {
+ private DefaultVoicemailNotifier(Context context) {
mContext = context;
- mNotificationManager = notificationManager;
- mNewCallsQuery = newCallsQuery;
- mNameLookupQuery = nameLookupQuery;
}
/**
@@ -96,16 +90,17 @@ public class DefaultVoicemailNotifier {
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();
+ final List<NewCall> newCalls =
+ CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
if (newCalls == null) {
// Query failed, just return.
return;
}
- if (newCalls.length == 0) {
+ if (newCalls.isEmpty()) {
// No voicemails to notify about: clear the notification.
- clearNotification();
+ getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
return;
}
@@ -122,23 +117,25 @@ public class DefaultVoicemailNotifier {
NewCall callToNotify = null;
// Iterate over the new voicemails to determine all the information above.
- for (NewCall newCall : newCalls) {
+ Iterator<NewCall> itr = newCalls.iterator();
+ while (itr.hasNext()) {
+ NewCall newCall = itr.next();
+
+ // Skip notifying for numbers which are blocked.
+ if (FilteredNumbersUtil.shouldBlockVoicemail(
+ mContext, newCall.number, newCall.countryIso, newCall.dateMs)) {
+ itr.remove();
+
+ // Delete the voicemail.
+ mContext.getContentResolver().delete(newCall.voicemailUri, null, null);
+ continue;
+ }
+
// Check if we already know the name associated with this number.
String name = names.get(newCall.number);
if (name == null) {
- name = PhoneNumberDisplayUtil.getDisplayName(
- mContext,
- newCall.number,
- newCall.numberPresentation,
- /* isVoicemail */ false).toString();
- // If we cannot lookup the contact, use the number instead.
- if (TextUtils.isEmpty(name)) {
- // Look it up in the database.
- name = mNameLookupQuery.query(newCall.number);
- if (TextUtils.isEmpty(name)) {
- name = newCall.number;
- }
- }
+ name = CallLogNotificationsHelper.getInstance(mContext).getName(newCall.number,
+ newCall.numberPresentation, newCall.countryIso);
names.put(newCall.number, name);
// This is a new caller. Add it to the back of the list of callers.
if (TextUtils.isEmpty(callers)) {
@@ -155,10 +152,15 @@ public class DefaultVoicemailNotifier {
}
}
+ // All the potential new voicemails have been removed, e.g. if they were spam.
+ if (newCalls.isEmpty()) {
+ return;
+ }
+
// If there is only one voicemail, set its transcription as the "long text".
String transcription = null;
- if (newCalls.length == 1) {
- transcription = newCalls[0].transcription;
+ if (newCalls.size() == 1) {
+ transcription = newCalls.get(0).transcription;
}
if (newCallUri != null && callToNotify == null) {
@@ -167,24 +169,26 @@ public class DefaultVoicemailNotifier {
// 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);
+ R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size());
// TODO: Use the photo of contact if all calls are from the same person.
final int icon = android.R.drawable.stat_notify_voicemail;
+ Pair<Uri, Integer> info = getNotificationInfo(callToNotify);
+
Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setSmallIcon(icon)
.setContentTitle(title)
.setContentText(callers)
.setStyle(new Notification.BigTextStyle().bigText(transcription))
.setColor(resources.getColor(R.color.dialer_theme_color))
- .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
+ .setSound(info.first)
+ .setDefaults(info.second)
.setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
.setAutoCancel(true);
// Determine the intent to fire when the notification is clicked on.
final Intent contentIntent;
// Open the call log.
- // TODO: Send to recents tab in Dialer instead.
contentIntent = new Intent(mContext, DialtactsActivity.class);
contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_VOICEMAIL);
notificationBuilder.setContentIntent(PendingIntent.getActivity(
@@ -192,196 +196,74 @@ public class DefaultVoicemailNotifier {
// 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)));
+ CharSequence msg = ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ resources,
+ R.string.notification_new_voicemail_ticker,
+ names.get(callToNotify.number));
+ notificationBuilder.setTicker(msg);
}
-
- 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);
- }
-
- 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 final int numberPresentation;
- public final String accountComponentName;
- public final String accountId;
- public final String transcription;
-
- public NewCall(
- Uri callsUri,
- Uri voicemailUri,
- String number,
- int numberPresentation,
- String accountComponentName,
- String accountId,
- String transcription) {
- this.callsUri = callsUri;
- this.voicemailUri = voicemailUri;
- this.number = number;
- this.numberPresentation = numberPresentation;
- this.accountComponentName = accountComponentName;
- this.accountId = accountId;
- this.transcription = transcription;
- }
- }
-
- /** 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(Context context,
- ContentResolver contentResolver) {
- return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
+ Log.i(TAG, "Creating voicemail notification");
+ getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID,
+ notificationBuilder.build());
}
/**
- * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
- * notify about in the call log.
+ * Determines which ringtone Uri and Notification defaults to use when updating the notification
+ * for the given call.
*/
- private static final class DefaultNewCallsQuery implements NewCallsQuery {
- private static final String[] PROJECTION = {
- Calls._ID,
- Calls.NUMBER,
- Calls.VOICEMAIL_URI,
- Calls.NUMBER_PRESENTATION,
- Calls.PHONE_ACCOUNT_COMPONENT_NAME,
- Calls.PHONE_ACCOUNT_ID,
- Calls.TRANSCRIPTION
- };
- 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 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
- private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
- private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
- private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
-
- private final ContentResolver mContentResolver;
- private final Context mContext;
-
- private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
- mContext = context;
- mContentResolver = contentResolver;
+ private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) {
+ Log.v(TAG, "getNotificationInfo");
+ if (callToNotify == null) {
+ Log.i(TAG, "callToNotify == null");
+ return new Pair<>(null, 0);
}
-
- @Override
- public NewCall[] query() {
- if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
- Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
- return null;
- }
- 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;
- } catch (RuntimeException e) {
- Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
- return null;
- } finally {
- MoreCloseables.closeQuietly(cursor);
+ PhoneAccountHandle accountHandle = null;
+ if (callToNotify.accountComponentName == null || callToNotify.accountId == null) {
+ Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null");
+ accountHandle = TelecomUtil
+ .getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL);
+ if (accountHandle == null) {
+ Log.i(TAG, "No default phone account found, using default notification ringtone");
+ return new Pair<>(null, Notification.DEFAULT_ALL);
}
- }
- /** 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),
- cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
- cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
- cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
- cursor.getString(TRANSCRIPTION_COLUMN_INDEX));
+ } else {
+ accountHandle = new PhoneAccountHandle(
+ ComponentName.unflattenFromString(callToNotify.accountComponentName),
+ callToNotify.accountId);
}
+ if (accountHandle.getComponentName() != null) {
+ Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName());
+ } else {
+ Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null");
+ }
+ return new Pair<>(
+ TelephonyManagerCompat.getVoicemailRingtoneUri(
+ getTelephonyManager(), accountHandle),
+ getNotificationDefaults(accountHandle));
}
- /** 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);
+ private int getNotificationDefaults(PhoneAccountHandle accountHandle) {
+ if (ContactsUtils.FLAG_N_FEATURE) {
+ return TelephonyManagerCompat.isVoicemailVibrationEnabled(getTelephonyManager(),
+ accountHandle) ? Notification.DEFAULT_VIBRATE : 0;
+ }
+ return Notification.DEFAULT_ALL;
}
- /** Create a new instance of {@link NameLookupQuery}. */
- public static NameLookupQuery createNameLookupQuery(Context context,
- ContentResolver contentResolver) {
- return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
+ /** 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);
}
- /**
- * 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 final Context mContext;
-
- private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
- mContext = context;
- mContentResolver = contentResolver;
- }
+ private NotificationManager getNotificationManager() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
- @Override
- public String query(String number) {
- if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
- Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
- return null;
- }
- 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);
- } catch (RuntimeException e) {
- Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
- return null;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
+ private TelephonyManager getTelephonyManager() {
+ return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
}
+
}
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 8d3ab4545..0d06298e7 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -22,78 +22,28 @@ import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
-import android.util.Log;
import android.util.SparseIntArray;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import com.android.contacts.common.testing.NeededForTesting;
/**
- * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
+ * Maintains a list that groups items into groups of consecutive elements which are disjoint,
+ * that is, an item can only belong to one group. This is leveraged for grouping calls in the
+ * call log received from or made to the same phone number.
*
- * The list has three types of elements: stand-alone, group header and group child. Groups are
- * collapsible and collapsed by default. This is used by the call log to group related entries.
+ * There are two integers stored as metadata for every list item in the adapter.
*/
abstract class GroupingListAdapter extends RecyclerView.Adapter {
- private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
- private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
- private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL;
- private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L;
- private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
-
- public static final int ITEM_TYPE_STANDALONE = 0;
- public static final int ITEM_TYPE_GROUP_HEADER = 1;
- public static final int ITEM_TYPE_IN_GROUP = 2;
-
- /**
- * Information about a specific list item: is it a group, if so is it expanded.
- * Otherwise, is it a stand-alone item or a group member.
- */
- protected static class PositionMetadata {
- int itemType;
- boolean isExpanded;
- int cursorPosition;
- int childCount;
- private int groupPosition;
- private int listPosition = -1;
- }
-
private Context mContext;
private Cursor mCursor;
/**
- * Count of list items.
- */
- private int mCount;
-
- private int mRowIdColumnIndex;
-
- /**
- * Count of groups in the list.
- */
- private int mGroupCount;
-
- /**
- * Information about where these groups are located in the list, how large they are
- * and whether they are expanded.
+ * SparseIntArray, which maps the cursor position of the first element of a group to the size
+ * of the group. The index of a key in this map corresponds to the list position of that group.
*/
- private long[] mGroupMetadata;
-
- private SparseIntArray mPositionCache = new SparseIntArray();
- private int mLastCachedListPosition;
- private int mLastCachedCursorPosition;
- private int mLastCachedGroup;
-
- /**
- * A reusable temporary instance of PositionMetadata
- */
- private PositionMetadata mPositionMetadata = new PositionMetadata();
+ private SparseIntArray mGroupMetadata;
+ private int mItemCount;
protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
-
@Override
public boolean deliverSelfNotifications() {
return true;
@@ -106,7 +56,6 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter {
};
protected DataSetObserver mDataSetObserver = new DataSetObserver() {
-
@Override
public void onChanged() {
notifyDataSetChanged();
@@ -115,7 +64,7 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter {
public GroupingListAdapter(Context context) {
mContext = context;
- resetCache();
+ reset();
}
/**
@@ -124,21 +73,19 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter {
*/
protected abstract void addGroups(Cursor cursor);
+ protected abstract void addVoicemailGroups(Cursor cursor);
+
protected abstract void onContentChanged();
- /**
- * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
- */
- private void resetCache() {
- mCount = -1;
- mLastCachedListPosition = -1;
- mLastCachedCursorPosition = -1;
- mLastCachedGroup = -1;
- mPositionMetadata.listPosition = -1;
- mPositionCache.clear();
+ public void changeCursor(Cursor cursor) {
+ changeCursor(cursor, false);
}
- public void changeCursor(Cursor cursor) {
+ public void changeCursorVoicemail(Cursor cursor) {
+ changeCursor(cursor, true);
+ }
+
+ public void changeCursor(Cursor cursor, boolean voicemail) {
if (cursor == mCursor) {
return;
}
@@ -148,288 +95,77 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter {
mCursor.unregisterDataSetObserver(mDataSetObserver);
mCursor.close();
}
+
+ // Reset whenever the cursor is changed.
+ reset();
mCursor = cursor;
- resetCache();
- findGroups();
if (cursor != null) {
+ if (voicemail) {
+ addVoicemailGroups(mCursor);
+ } else {
+ addGroups(mCursor);
+ }
+
+ // Calculate the item count by subtracting group child counts from the cursor count.
+ mItemCount = mGroupMetadata.size();
+
cursor.registerContentObserver(mChangeObserver);
cursor.registerDataSetObserver(mDataSetObserver);
- mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
notifyDataSetChanged();
}
}
- @NeededForTesting
- public Cursor getCursor() {
- return mCursor;
- }
-
- /**
- * Scans over the entire cursor looking for duplicate phone numbers that need
- * to be collapsed.
- */
- private void findGroups() {
- mGroupCount = 0;
- mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
-
- if (mCursor == null) {
- return;
- }
-
- addGroups(mCursor);
- }
-
/**
- * Records information about grouping in the list. Should be called by the overridden
- * {@link #addGroups} method.
+ * Records information about grouping in the list.
+ * Should be called by the overridden {@link #addGroups} method.
*/
- protected void addGroup(int cursorPosition, int size, boolean expanded) {
- if (mGroupCount >= mGroupMetadata.length) {
- int newSize = idealLongArraySize(
- mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
- long[] array = new long[newSize];
- System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
- mGroupMetadata = array;
- }
-
- long metadata = ((long)size << 32) | cursorPosition;
- if (expanded) {
- metadata |= EXPANDED_GROUP_MASK;
+ public void addGroup(int cursorPosition, int groupSize) {
+ int lastIndex = mGroupMetadata.size() - 1;
+ if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) {
+ mGroupMetadata.put(cursorPosition, groupSize);
+ } else {
+ // Optimization to avoid binary search if adding groups in ascending cursor position.
+ mGroupMetadata.append(cursorPosition, groupSize);
}
- mGroupMetadata[mGroupCount++] = metadata;
- }
-
- // Copy/paste from ArrayUtils
- private int idealLongArraySize(int need) {
- return idealByteArraySize(need * 8) / 8;
- }
-
- // Copy/paste from ArrayUtils
- private int idealByteArraySize(int need) {
- for (int i = 4; i < 32; i++)
- if (need <= (1 << i) - 12)
- return (1 << i) - 12;
-
- return need;
}
@Override
public int getItemCount() {
- if (mCursor == null) {
- return 0;
- }
-
- if (mCount != -1) {
- return mCount;
- }
-
- int cursorPosition = 0;
- int count = 0;
- for (int i = 0; i < mGroupCount; i++) {
- long metadata = mGroupMetadata[i];
- int offset = (int)(metadata & GROUP_OFFSET_MASK);
- boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
- int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
-
- count += (offset - cursorPosition);
-
- if (expanded) {
- count += size + 1;
- } else {
- count++;
- }
-
- cursorPosition = offset + size;
- }
-
- mCount = count + mCursor.getCount() - cursorPosition;
- return mCount;
+ return mItemCount;
}
/**
- * Figures out whether the item at the specified position represents a
- * stand-alone element, a group or a group child. Also computes the
- * corresponding cursor position.
+ * Given the position of a list item, returns the size of the group of items corresponding to
+ * that position.
*/
- public void obtainPositionMetadata(PositionMetadata metadata, int position) {
- // If the description object already contains requested information, just return
- if (metadata.listPosition == position) {
- return;
- }
-
- int listPosition = 0;
- int cursorPosition = 0;
- int firstGroupToCheck = 0;
-
- // Check cache for the supplied position. What we are looking for is
- // the group descriptor immediately preceding the supplied position.
- // Once we have that, we will be able to tell whether the position
- // is the header of the group, a member of the group or a standalone item.
- if (mLastCachedListPosition != -1) {
- if (position <= mLastCachedListPosition) {
-
- // Have SparceIntArray do a binary search for us.
- int index = mPositionCache.indexOfKey(position);
-
- // If we get back a positive number, the position corresponds to
- // a group header.
- if (index < 0) {
-
- // We had a cache miss, but we did obtain valuable information anyway.
- // The negative number will allow us to compute the location of
- // the group header immediately preceding the supplied position.
- index = ~index - 1;
-
- if (index >= mPositionCache.size()) {
- index--;
- }
- }
-
- // A non-negative index gives us the position of the group header
- // corresponding or preceding the position, so we can
- // search for the group information at the supplied position
- // starting with the cached group we just found
- if (index >= 0) {
- listPosition = mPositionCache.keyAt(index);
- firstGroupToCheck = mPositionCache.valueAt(index);
- long descriptor = mGroupMetadata[firstGroupToCheck];
- cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
- }
- } else {
-
- // If we haven't examined groups beyond the supplied position,
- // we will start where we left off previously
- firstGroupToCheck = mLastCachedGroup;
- listPosition = mLastCachedListPosition;
- cursorPosition = mLastCachedCursorPosition;
- }
- }
-
- for (int i = firstGroupToCheck; i < mGroupCount; i++) {
- long group = mGroupMetadata[i];
- int offset = (int)(group & GROUP_OFFSET_MASK);
-
- // Move pointers to the beginning of the group
- listPosition += (offset - cursorPosition);
- cursorPosition = offset;
-
- if (i > mLastCachedGroup) {
- mPositionCache.append(listPosition, i);
- mLastCachedListPosition = listPosition;
- mLastCachedCursorPosition = cursorPosition;
- mLastCachedGroup = i;
- }
-
- // Now we have several possibilities:
- // A) The requested position precedes the group
- if (position < listPosition) {
- metadata.itemType = ITEM_TYPE_STANDALONE;
- metadata.cursorPosition = cursorPosition - (listPosition - position);
- metadata.childCount = 1;
- return;
- }
-
- boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
- int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
-
- // B) The requested position is a group header
- if (position == listPosition) {
- metadata.itemType = ITEM_TYPE_GROUP_HEADER;
- metadata.groupPosition = i;
- metadata.isExpanded = expanded;
- metadata.childCount = size;
- metadata.cursorPosition = offset;
- return;
- }
-
- if (expanded) {
- // C) The requested position is an element in the expanded group
- if (position < listPosition + size + 1) {
- metadata.itemType = ITEM_TYPE_IN_GROUP;
- metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
- return;
- }
-
- // D) The element is past the expanded group
- listPosition += size + 1;
- } else {
-
- // E) The element is past the collapsed group
- listPosition++;
- }
-
- // Move cursor past the group
- cursorPosition += size;
+ public int getGroupSize(int listPosition) {
+ if (listPosition < 0 || listPosition >= mGroupMetadata.size()) {
+ return 0;
}
- // The required item is past the last group
- metadata.itemType = ITEM_TYPE_STANDALONE;
- metadata.cursorPosition = cursorPosition + (position - listPosition);
- metadata.childCount = 1;
+ return mGroupMetadata.valueAt(listPosition);
}
/**
- * Returns true if the specified position in the list corresponds to a
- * group header.
+ * Given the position of a list item, returns the the first item in the group of items
+ * corresponding to that position.
*/
- public boolean isGroupHeader(int position) {
- obtainPositionMetadata(mPositionMetadata, position);
- return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
- }
-
- /**
- * Given a position of a groups header in the list, returns the size of
- * the corresponding group.
- */
- public int getGroupSize(int position) {
- obtainPositionMetadata(mPositionMetadata, position);
- return mPositionMetadata.childCount;
- }
-
- /**
- * Mark group as expanded if it is collapsed and vice versa.
- */
- @NeededForTesting
- public void toggleGroup(int position) {
- obtainPositionMetadata(mPositionMetadata, position);
- if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
- throw new IllegalArgumentException("Not a group at position " + position);
- }
-
- if (mPositionMetadata.isExpanded) {
- mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
- } else {
- mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
- }
- resetCache();
- notifyDataSetChanged();
- }
-
- public int getItemViewType(int position) {
- obtainPositionMetadata(mPositionMetadata, position);
- return mPositionMetadata.itemType;
- }
-
- public Object getItem(int position) {
- if (mCursor == null) {
+ public Object getItem(int listPosition) {
+ if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) {
return null;
}
- obtainPositionMetadata(mPositionMetadata, position);
- if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
+ int cursorPosition = mGroupMetadata.keyAt(listPosition);
+ if (mCursor.moveToPosition(cursorPosition)) {
return mCursor;
} else {
return null;
}
}
- public long getItemId(int position) {
- Object item = getItem(position);
- if (item != null) {
- return mCursor.getLong(mRowIdColumnIndex);
- } else {
- return -1;
- }
+ private void reset() {
+ mItemCount = 0;
+ mGroupMetadata = new SparseIntArray();
}
}
diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java
index a11d00bc2..773436be4 100644
--- a/src/com/android/dialer/calllog/IntentProvider.java
+++ b/src/com/android/dialer/calllog/IntentProvider.java
@@ -21,17 +21,17 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.telecom.PhoneAccountHandle;
+import com.android.contacts.common.CallUtil;
import com.android.contacts.common.model.Contact;
import com.android.contacts.common.model.ContactLoader;
import com.android.dialer.CallDetailActivity;
-import com.android.dialer.DialtactsActivity;
-import com.android.dialer.PhoneCallDetails;
import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
import com.android.dialer.util.TelecomUtil;
+import com.android.incallui.Call.LogState;
import java.util.ArrayList;
@@ -55,7 +55,10 @@ public abstract class IntentProvider {
return new IntentProvider() {
@Override
public Intent getIntent(Context context) {
- return IntentUtil.getCallIntent(number, accountHandle);
+ return new CallIntentBuilder(number)
+ .setPhoneAccountHandle(accountHandle)
+ .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+ .build();
}
};
}
@@ -69,7 +72,11 @@ public abstract class IntentProvider {
return new IntentProvider() {
@Override
public Intent getIntent(Context context) {
- return IntentUtil.getVideoCallIntent(number, accountHandle);
+ return new CallIntentBuilder(number)
+ .setPhoneAccountHandle(accountHandle)
+ .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+ .setIsVideoCall(true)
+ .build();
}
};
}
@@ -78,7 +85,9 @@ public abstract class IntentProvider {
return new IntentProvider() {
@Override
public Intent getIntent(Context context) {
- return IntentUtil.getVoicemailIntent();
+ return new CallIntentBuilder(CallUtil.getVoicemailUri())
+ .setCallInitiationType(LogState.INITIATION_CALL_LOG)
+ .build();
}
};
}
diff --git a/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
new file mode 100644
index 000000000..86d6cb9fb
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.dialer.calllog.CallLogNotificationsService;
+
+/**
+ * Receives broadcasts that should trigger a refresh of the missed call notification. This includes
+ * both an explicit broadcast from Telecom and a reboot.
+ */
+public class MissedCallNotificationReceiver extends BroadcastReceiver {
+ //TODO: Use compat class for these methods.
+ public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION =
+ "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION";
+
+ public static final String EXTRA_NOTIFICATION_COUNT =
+ "android.telecom.extra.NOTIFICATION_COUNT";
+
+ public static final String EXTRA_NOTIFICATION_PHONE_NUMBER =
+ "android.telecom.extra.NOTIFICATION_PHONE_NUMBER";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) {
+ return;
+ }
+
+ int count = intent.getIntExtra(EXTRA_NOTIFICATION_COUNT,
+ CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER);
+ CallLogNotificationsService.updateMissedCallNotifications(context, count, number);
+ }
+}
diff --git a/src/com/android/dialer/calllog/MissedCallNotifier.java b/src/com/android/dialer/calllog/MissedCallNotifier.java
new file mode 100644
index 000000000..98d02d095
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotifier.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016 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.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.contactinfo.ContactPhotoLoader;
+import com.android.dialer.compat.UserManagerCompat;
+import com.android.dialer.list.ListsFragment;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
+
+import java.util.List;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ *
+ */
+public class MissedCallNotifier {
+ public static final String TAG = "MissedCallNotifier";
+
+ /** The tag used to identify notifications from this class. */
+ private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+ /** The identifier of the notification of new missed calls. */
+ private static final int NOTIFICATION_ID = 1;
+ /** Preference file key for number of missed calls. */
+ private static final String MISSED_CALL_COUNT = "missed_call_count";
+
+ private static MissedCallNotifier sInstance;
+ private Context mContext;
+
+ /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+ public static MissedCallNotifier getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new MissedCallNotifier(context);
+ }
+ return sInstance;
+ }
+
+ private MissedCallNotifier(Context context) {
+ mContext = context;
+ }
+
+ public void updateMissedCallNotification(int count, String number) {
+ final int titleResId;
+ final String expandedText; // The text in the notification's line 1 and 2.
+
+ final List<NewCall> newCalls =
+ CallLogNotificationsHelper.getInstance(mContext).getNewMissedCalls();
+
+ if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+ if (newCalls == null) {
+ // If the intent did not contain a count, and we are unable to get a count from the
+ // call log, then no notification can be shown.
+ return;
+ }
+ count = newCalls.size();
+ }
+
+ if (count == 0) {
+ // No voicemails to notify about: clear the notification.
+ clearMissedCalls();
+ return;
+ }
+
+ // The call log has been updated, use that information preferentially.
+ boolean useCallLog = newCalls != null && newCalls.size() == count;
+ NewCall newestCall = useCallLog ? newCalls.get(0) : null;
+ long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
+
+ Notification.Builder builder = new Notification.Builder(mContext);
+ // Display the first line of the notification:
+ // 1 missed call: <caller name || handle>
+ // More than 1 missed call: <number of calls> + "missed calls"
+ if (count == 1) {
+ //TODO: look up caller ID that is not in contacts.
+ ContactInfo contactInfo = CallLogNotificationsHelper.getInstance(mContext)
+ .getContactInfo(useCallLog ? newestCall.number : number,
+ useCallLog ? newestCall.numberPresentation
+ : Calls.PRESENTATION_ALLOWED,
+ useCallLog ? newestCall.countryIso : null);
+
+ titleResId = contactInfo.userType == ContactsUtils.USER_TYPE_WORK
+ ? R.string.notification_missedWorkCallTitle
+ : R.string.notification_missedCallTitle;
+
+ expandedText = contactInfo.name;
+ ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
+ Bitmap photoIcon = loader.loadPhotoIcon();
+ if (photoIcon != null) {
+ builder.setLargeIcon(photoIcon);
+ }
+ } else {
+ titleResId = R.string.notification_missedCallsTitle;
+ expandedText =
+ mContext.getString(R.string.notification_missedCallsMsg, count);
+ }
+
+ // Create a public viewable version of the notification, suitable for display when sensitive
+ // notification content is hidden.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // Show "Phone" for notification title.
+ .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
+ // Notification details shows that there are missed call(s), but does not reveal
+ // the missed caller information.
+ .setContentText(mContext.getText(titleResId))
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+ // Create the notification suitable for display when sensitive information is showing.
+ builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ .setContentTitle(mContext.getText(titleResId))
+ .setContentText(expandedText)
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setDeleteIntent(createClearMissedCallsPendingIntent())
+ // Include a public version of the notification to be shown when the missed call
+ // notification is shown on the user's lock screen and they have chosen to hide
+ // sensitive notification information.
+ .setPublicVersion(publicBuilder.build());
+
+ // Add additional actions when there is only 1 missed call and the user isn't locked
+ if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) {
+ if (!TextUtils.isEmpty(number)
+ && !TextUtils.equals(
+ number, mContext.getString(R.string.handle_restricted))) {
+ builder.addAction(R.drawable.ic_phone_24dp,
+ mContext.getString(R.string.notification_missedCall_call_back),
+ createCallBackPendingIntent(number));
+
+ if (!PhoneNumberHelper.isUriNumber(number)) {
+ builder.addAction(R.drawable.ic_message_24dp,
+ mContext.getString(R.string.notification_missedCall_message),
+ createSendSmsFromNotificationPendingIntent(number));
+ }
+ }
+ }
+
+ Notification notification = builder.build();
+ configureLedOnNotification(notification);
+
+ Log.i(TAG, "Adding missed call notification.");
+ getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+ }
+
+ private void clearMissedCalls() {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Call log is only accessible when unlocked. If that's the case, clear the list of
+ // new missed calls from the call log.
+ if (UserManagerCompat.isUserUnlocked(mContext)) {
+ ContentValues values = new ContentValues();
+ values.put(Calls.NEW, 0);
+ values.put(Calls.IS_READ, 1);
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+ try {
+ mContext.getContentResolver().update(Calls.CONTENT_URI, values,
+ where.toString(), new String[]{Integer.toString(Calls.
+ MISSED_TYPE)});
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "ContactsProvider update command failed", e);
+ }
+ }
+ getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ }
+ });
+ }
+
+ /**
+ * Trigger an intent to make a call from a missed call number.
+ */
+ public void callBackFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext,
+ new CallIntentBuilder(number)
+ .build()
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /**
+ * Trigger an intent to send an sms from a missed call number.
+ */
+ public void sendSmsFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext,
+ IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /**
+ * Creates a new pending intent that sends the user to the call log.
+ *
+ * @return The pending intent.
+ */
+ private PendingIntent createCallLogPendingIntent() {
+ Intent contentIntent = new Intent(mContext, DialtactsActivity.class);
+ contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_HISTORY);
+ return PendingIntent.getActivity(
+ mContext, 0, contentIntent,PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /** Creates a pending intent that marks all new missed calls as old. */
+ private PendingIntent createClearMissedCallsPendingIntent() {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private PendingIntent createCallBackPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(
+ CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(
+ CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ /**
+ * Configures a notification to emit the blinky notification light.
+ */
+ private void configureLedOnNotification(Notification notification) {
+ notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+ notification.defaults |= Notification.DEFAULT_LIGHTS;
+ }
+
+ /**
+ * Closes open system dialogs and the notification shade.
+ */
+ private void closeSystemDialogs(Context context) {
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ private NotificationManager getNotificationMgr() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+}
diff --git a/src/com/android/dialer/calllog/PhoneAccountUtils.java b/src/com/android/dialer/calllog/PhoneAccountUtils.java
index 143d13e86..8c3985b3f 100644
--- a/src/com/android/dialer/calllog/PhoneAccountUtils.java
+++ b/src/com/android/dialer/calllog/PhoneAccountUtils.java
@@ -18,11 +18,14 @@ package com.android.dialer.calllog;
import android.content.ComponentName;
import android.content.Context;
+import android.support.annotation.Nullable;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
import android.text.TextUtils;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.dialer.util.TelecomUtil;
+
import java.util.ArrayList;
import java.util.List;
@@ -34,13 +37,11 @@ public class PhoneAccountUtils {
* Return a list of phone accounts that are subscription/SIM accounts.
*/
public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
- final TelecomManager telecomManager =
- (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
-
List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<PhoneAccountHandle>();
- List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
+ final List<PhoneAccountHandle> accountHandles =
+ TelecomUtil.getCallCapablePhoneAccounts(context);
for (PhoneAccountHandle accountHandle : accountHandles) {
- PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
+ PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
subscriptionAccountHandles.add(accountHandle);
}
@@ -51,7 +52,9 @@ public class PhoneAccountUtils {
/**
* Compose PhoneAccount object from component name and account id.
*/
- public static PhoneAccountHandle getAccount(String componentString, String accountId) {
+ @Nullable
+ public static PhoneAccountHandle getAccount(@Nullable String componentString,
+ @Nullable String accountId) {
if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) {
return null;
}
@@ -62,7 +65,9 @@ public class PhoneAccountUtils {
/**
* Extract account label from PhoneAccount object.
*/
- public static String getAccountLabel(Context context, PhoneAccountHandle accountHandle) {
+ @Nullable
+ public static String getAccountLabel(Context context,
+ @Nullable PhoneAccountHandle accountHandle) {
PhoneAccount account = getAccountOrNull(context, accountHandle);
if (account != null && account.getLabel() != null) {
return account.getLabel().toString();
@@ -73,10 +78,8 @@ public class PhoneAccountUtils {
/**
* Extract account color from PhoneAccount object.
*/
- public static int getAccountColor(Context context, PhoneAccountHandle accountHandle) {
- TelecomManager telecomManager =
- (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
- final PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
+ public static int getAccountColor(Context context, @Nullable PhoneAccountHandle accountHandle) {
+ final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
// For single-sim devices the PhoneAccount will be NO_HIGHLIGHT_COLOR by default, so it is
// safe to always use the account highlight color.
@@ -89,10 +92,8 @@ public class PhoneAccountUtils {
* @return {@code true} if call subjects are supported, {@code false} otherwise.
*/
public static boolean getAccountSupportsCallSubject(Context context,
- PhoneAccountHandle accountHandle) {
- TelecomManager telecomManager =
- (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
- final PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
+ @Nullable PhoneAccountHandle accountHandle) {
+ final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle);
return account == null ? false :
account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
@@ -102,14 +103,12 @@ public class PhoneAccountUtils {
* Retrieve the account metadata, but if the account does not exist or the device has only a
* single registered and enabled account, return null.
*/
- static PhoneAccount getAccountOrNull(Context context,
- PhoneAccountHandle accountHandle) {
- TelecomManager telecomManager =
- (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
- final PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
- if (telecomManager.getCallCapablePhoneAccounts().size() <= 1) {
+ @Nullable
+ private static PhoneAccount getAccountOrNull(Context context,
+ @Nullable PhoneAccountHandle accountHandle) {
+ if (TelecomUtil.getCallCapablePhoneAccounts(context).size() <= 1) {
return null;
}
- return account;
+ return TelecomUtil.getPhoneAccount(context, accountHandle);
}
}
diff --git a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
index df5fe0606..7b149e24e 100644
--- a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
+++ b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
@@ -16,13 +16,15 @@
package com.android.dialer.calllog;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v4.content.ContextCompat;
import android.telecom.PhoneAccount;
import android.text.TextUtils;
import android.text.format.DateUtils;
@@ -33,17 +35,18 @@ import com.android.contacts.common.testing.NeededForTesting;
import com.android.contacts.common.util.PhoneNumberHelper;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.R;
+import com.android.dialer.calllog.calllogcache.CallLogCache;
import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.PhoneNumberUtil;
-
-import com.google.common.collect.Lists;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
/**
* Helper class to fill in the views in {@link PhoneCallDetailsViews}.
*/
public class PhoneCallDetailsHelper {
+
/** The maximum number of icons will be shown to represent the call types in a group. */
private static final int MAX_CALL_TYPE_ICONS = 3;
@@ -51,7 +54,13 @@ public class PhoneCallDetailsHelper {
private final Resources mResources;
/** The injected current time in milliseconds since the epoch. Used only by tests. */
private Long mCurrentTimeMillisForTest;
- private final TelecomCallLogCache mTelecomCallLogCache;
+
+ private CharSequence mPhoneTypeLabelForTest;
+
+ private final CallLogCache mCallLogCache;
+
+ /** Calendar used to construct dates */
+ private final Calendar mCalendar;
/**
* List of items to be concatenated together for accessibility descriptions
@@ -68,10 +77,11 @@ public class PhoneCallDetailsHelper {
public PhoneCallDetailsHelper(
Context context,
Resources resources,
- TelecomCallLogCache telecomCallLogCache) {
+ CallLogCache callLogCache) {
mContext = context;
mResources = resources;
- mTelecomCallLogCache = telecomCallLogCache;
+ mCallLogCache = callLogCache;
+ mCalendar = Calendar.getInstance();
}
/** Fills the call details views with content. */
@@ -101,18 +111,16 @@ public class PhoneCallDetailsHelper {
callCount = null;
}
- CharSequence callLocationAndDate = getCallLocationAndDate(details);
-
- // Set the call count, location and date.
- setCallCountAndDate(views, callCount, callLocationAndDate);
+ // Set the call count, location, date and if voicemail, set the duration.
+ setDetailText(views, callCount, details);
// Set the account label if it exists.
- String accountLabel = mTelecomCallLogCache.getAccountLabel(details.accountHandle);
+ String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
if (accountLabel != null) {
views.callAccountLabel.setVisibility(View.VISIBLE);
views.callAccountLabel.setText(accountLabel);
- int color = PhoneAccountUtils.getAccountColor(mContext, details.accountHandle);
+ int color = mCallLogCache.getAccountColor(details.accountHandle);
if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
int defaultColor = R.color.dialtacts_secondary_text_color;
views.callAccountLabel.setTextColor(mContext.getResources().getColor(defaultColor));
@@ -125,22 +133,19 @@ public class PhoneCallDetailsHelper {
final CharSequence nameText;
final CharSequence displayNumber = details.displayNumber;
- if (TextUtils.isEmpty(details.name)) {
+ if (TextUtils.isEmpty(details.getPreferredName())) {
nameText = displayNumber;
// We have a real phone number as "nameView" so make it always LTR
views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR);
} else {
- nameText = details.name;
+ nameText = details.getPreferredName();
}
views.nameView.setText(nameText);
- if (isVoicemail && !TextUtils.isEmpty(details.transcription)) {
- views.voicemailTranscriptionView.setText(details.transcription);
- views.voicemailTranscriptionView.setVisibility(View.VISIBLE);
- } else {
- views.voicemailTranscriptionView.setText(null);
- views.voicemailTranscriptionView.setVisibility(View.GONE);
+ if (isVoicemail) {
+ views.voicemailTranscriptionView.setText(TextUtils.isEmpty(details.transcription) ? null
+ : details.transcription);
}
// Bold if not read
@@ -148,10 +153,13 @@ public class PhoneCallDetailsHelper {
views.nameView.setTypeface(typeface);
views.voicemailTranscriptionView.setTypeface(typeface);
views.callLocationAndDate.setTypeface(typeface);
+ views.callLocationAndDate.setTextColor(ContextCompat.getColor(mContext, details.isRead ?
+ R.color.call_log_detail_color : R.color.call_log_unread_text_color));
}
/**
- * Builds a string containing the call location and date.
+ * Builds a string containing the call location and date. For voicemail logs only the call date
+ * is returned because location information is displayed in the call action button
*
* @param details The call details.
* @return The call location and date string.
@@ -159,15 +167,18 @@ public class PhoneCallDetailsHelper {
private CharSequence getCallLocationAndDate(PhoneCallDetails details) {
mDescriptionItems.clear();
- // Get type of call (ie mobile, home, etc) if known, or the caller's location.
- CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
+ if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) {
+ // Get type of call (ie mobile, home, etc) if known, or the caller's location.
+ CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
- // Only add the call type or location if its not empty. It will be empty for unknown
- // callers.
- if (!TextUtils.isEmpty(callTypeOrLocation)) {
- mDescriptionItems.add(callTypeOrLocation);
+ // Only add the call type or location if its not empty. It will be empty for unknown
+ // callers.
+ if (!TextUtils.isEmpty(callTypeOrLocation)) {
+ mDescriptionItems.add(callTypeOrLocation);
+ }
}
- // The date of this call, relative to the current time.
+
+ // The date of this call
mDescriptionItems.add(getCallDate(details));
// Create a comma separated list from the call type or location, and call date.
@@ -178,6 +189,7 @@ public class PhoneCallDetailsHelper {
* For a call, if there is an associated contact for the caller, return the known call type
* (e.g. mobile, home, work). If there is no associated contact, attempt to use the caller's
* location if known.
+ *
* @param details Call details to use.
* @return Type of call (mobile/home) if known, or the location of the caller (if known).
*/
@@ -186,43 +198,94 @@ public class PhoneCallDetailsHelper {
// Only show a label if the number is shown and it is not a SIP address.
if (!TextUtils.isEmpty(details.number)
&& !PhoneNumberHelper.isUriNumber(details.number.toString())
- && !mTelecomCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
+ && !mCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
- if (TextUtils.isEmpty(details.name) && !TextUtils.isEmpty(details.geocode)) {
+ if (TextUtils.isEmpty(details.namePrimary) && !TextUtils.isEmpty(details.geocode)) {
numberFormattedLabel = details.geocode;
} else if (!(details.numberType == Phone.TYPE_CUSTOM
&& TextUtils.isEmpty(details.numberLabel))) {
// Get type label only if it will not be "Custom" because of an empty number label.
- numberFormattedLabel = Phone.getTypeLabel(
- mResources, details.numberType, details.numberLabel);
+ numberFormattedLabel = MoreObjects.firstNonNull(mPhoneTypeLabelForTest,
+ Phone.getTypeLabel(mResources, details.numberType, details.numberLabel));
}
}
- if (!TextUtils.isEmpty(details.name) && TextUtils.isEmpty(numberFormattedLabel)) {
+ if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) {
numberFormattedLabel = details.displayNumber;
}
return numberFormattedLabel;
}
+ @NeededForTesting
+ public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) {
+ this.mPhoneTypeLabelForTest = phoneTypeLabel;
+ }
+
/**
- * Get the call date/time of the call, relative to the current time.
- * e.g. 3 minutes ago
+ * Get the call date/time of the call. For the call log this is relative to the current time.
+ * e.g. 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)}
+ *
* @param details Call details to use.
* @return String representing when the call occurred.
*/
public CharSequence getCallDate(PhoneCallDetails details) {
- return DateUtils.getRelativeTimeSpanString(details.date,
- getCurrentTimeMillis(),
- DateUtils.MINUTE_IN_MILLIS,
- DateUtils.FORMAT_ABBREV_RELATIVE);
+ if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) {
+ return getGranularDateTime(details);
+ }
+
+ return DateUtils.getRelativeTimeSpanString(details.date, getCurrentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
+ }
+
+ /**
+ * Get the granular version of the call date/time of the call. The result is always in the form
+ * 'DATE at TIME'. The date value changes based on when the call was created.
+ *
+ * If created today, DATE is 'Today'
+ * If created this year, DATE is 'MMM dd'
+ * Otherwise, DATE is 'MMM dd, yyyy'
+ *
+ * TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm'
+ *
+ * @param details Call details to use
+ * @return String representing when the call occurred
+ */
+ public CharSequence getGranularDateTime(PhoneCallDetails details) {
+ return mResources.getString(R.string.voicemailCallLogDateTimeFormat,
+ getGranularDate(details.date),
+ DateUtils.formatDateTime(mContext, details.date, DateUtils.FORMAT_SHOW_TIME));
+ }
+
+ /**
+ * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)}
+ */
+ private String getGranularDate(long date) {
+ if (DateUtils.isToday(date)) {
+ return mResources.getString(R.string.voicemailCallLogToday);
+ }
+ return DateUtils.formatDateTime(mContext, date, DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_ABBREV_MONTH
+ | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
+ }
+
+ /**
+ * Determines whether the year should be shown for the given date
+ *
+ * @return {@code true} if date is within the current year, {@code false} otherwise
+ */
+ private boolean shouldShowYear(long date) {
+ mCalendar.setTimeInMillis(getCurrentTimeMillis());
+ int currentYear = mCalendar.get(Calendar.YEAR);
+ mCalendar.setTimeInMillis(date);
+ return currentYear != mCalendar.get(Calendar.YEAR);
}
/** Sets the text of the header view for the details page of a phone call. */
@NeededForTesting
public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) {
final CharSequence nameText;
- if (!TextUtils.isEmpty(details.name)) {
- nameText = details.name;
+ if (!TextUtils.isEmpty(details.namePrimary)) {
+ nameText = details.namePrimary;
} else if (!TextUtils.isEmpty(details.displayNumber)) {
nameText = details.displayNumber;
} else {
@@ -250,10 +313,11 @@ public class PhoneCallDetailsHelper {
}
}
- /** Sets the call count and date. */
- private void setCallCountAndDate(PhoneCallDetailsViews views, Integer callCount,
- CharSequence dateText) {
+ /** Sets the call count, date, and if it is a voicemail, sets the duration. */
+ private void setDetailText(PhoneCallDetailsViews views, Integer callCount,
+ PhoneCallDetails details) {
// Combine the count (if present) and the date.
+ CharSequence dateText = getCallLocationAndDate(details);
final CharSequence text;
if (callCount != null) {
text = mResources.getString(
@@ -262,6 +326,22 @@ public class PhoneCallDetailsHelper {
text = dateText;
}
- views.callLocationAndDate.setText(text);
+ if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
+ views.callLocationAndDate.setText(mResources.getString(
+ R.string.voicemailCallLogDateTimeFormatWithDuration, text,
+ getVoicemailDuration(details)));
+ } else {
+ views.callLocationAndDate.setText(text);
+ }
+
+ }
+
+ private String getVoicemailDuration(PhoneCallDetails details) {
+ long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
+ long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds);
}
}
diff --git a/src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java b/src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java
index 5030efd48..5b1fc9e3a 100644
--- a/src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java
+++ b/src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java
@@ -17,10 +17,8 @@
package com.android.dialer.calllog;
import android.content.Context;
-import android.content.res.Resources;
import android.provider.CallLog.Calls;
import android.text.TextUtils;
-import android.util.Log;
import com.android.dialer.R;
import com.android.dialer.util.PhoneNumberUtil;
@@ -67,6 +65,7 @@ public class PhoneNumberDisplayUtil {
CharSequence number,
int presentation,
CharSequence formattedNumber,
+ CharSequence postDialDigits,
boolean isVoicemail) {
final CharSequence displayName = getDisplayName(context, number, presentation, isVoicemail);
if (!TextUtils.isEmpty(displayName)) {
@@ -76,9 +75,9 @@ public class PhoneNumberDisplayUtil {
if (!TextUtils.isEmpty(formattedNumber)) {
return formattedNumber;
} else if (!TextUtils.isEmpty(number)) {
- return number;
+ return number.toString() + postDialDigits;
} else {
- return "";
+ return context.getResources().getString(R.string.unknown);
}
}
}
diff --git a/src/com/android/dialer/calllog/PhoneQuery.java b/src/com/android/dialer/calllog/PhoneQuery.java
index 719052204..f1f14c66e 100644
--- a/src/com/android/dialer/calllog/PhoneQuery.java
+++ b/src/com/android/dialer/calllog/PhoneQuery.java
@@ -16,14 +16,27 @@
package com.android.dialer.calllog;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneLookupSdkCompat;
+import com.android.contacts.common.ContactsUtils;
+
/**
- * The query to look up the {@link ContactInfo} for a given number in the Call Log.
+ * The queries 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,
+
+ /**
+ * Projection to look up the ContactInfo. Does not include DISPLAY_NAME_ALTERNATIVE as that
+ * column isn't available in ContactsCommon.PhoneLookup.
+ * We should always use this projection starting from NYC onward.
+ */
+ private static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
+ PhoneLookupSdkCompat.CONTACT_ID,
PhoneLookup.DISPLAY_NAME,
PhoneLookup.TYPE,
PhoneLookup.LABEL,
@@ -31,7 +44,36 @@ final class PhoneQuery {
PhoneLookup.NORMALIZED_NUMBER,
PhoneLookup.PHOTO_ID,
PhoneLookup.LOOKUP_KEY,
- PhoneLookup.PHOTO_URI};
+ PhoneLookup.PHOTO_URI
+ };
+
+ /**
+ * Similar to {@link PHONE_LOOKUP_PROJECTION}. In pre-N, contact id is stored in
+ * {@link PhoneLookup#_ID} in non-sip query.
+ */
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_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 String[] getPhoneLookupProjection(Uri phoneLookupUri) {
+ if (CompatUtils.isNCompatible()) {
+ return PHONE_LOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip = phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PHONE_LOOKUP_PROJECTION
+ : BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION;
+ }
public static final int PERSON_ID = 0;
public static final int NAME = 1;
@@ -42,4 +84,13 @@ final class PhoneQuery {
public static final int PHOTO_ID = 6;
public static final int LOOKUP_KEY = 7;
public static final int PHOTO_URI = 8;
+
+ /**
+ * Projection to look up a contact's DISPLAY_NAME_ALTERNATIVE
+ */
+ public static final String[] DISPLAY_NAME_ALTERNATIVE_PROJECTION = new String[] {
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static final int NAME_ALTERNATIVE = 0;
}
diff --git a/src/com/android/dialer/calllog/PromoCardViewHolder.java b/src/com/android/dialer/calllog/PromoCardViewHolder.java
index 4c9602759..f5a7501fc 100644
--- a/src/com/android/dialer/calllog/PromoCardViewHolder.java
+++ b/src/com/android/dialer/calllog/PromoCardViewHolder.java
@@ -15,14 +15,17 @@
*/
package com.android.dialer.calllog;
-import com.android.dialer.R;
-
+import android.content.Context;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.View;
+import com.android.contacts.common.testing.NeededForTesting;
+import com.android.dialer.R;
+
/**
- * View holder class for a promo card which will appear in the voicemail tab.
+ * Generic ViewHolder class for a promo card with a primary and secondary action.
+ * Example: the promo card which appears in the voicemail tab.
*/
public class PromoCardViewHolder extends RecyclerView.ViewHolder {
public static PromoCardViewHolder create(View rootView) {
@@ -30,14 +33,15 @@ public class PromoCardViewHolder extends RecyclerView.ViewHolder {
}
/**
- * The "Settings" button view.
+ * The primary action button view.
*/
- private View mSettingsTextView;
+ private View mPrimaryActionView;
/**
+ * The secondary action button view.
* The "Ok" button view.
*/
- private View mOkTextView;
+ private View mSecondaryActionView;
/**
* Creates an instance of the {@link ViewHolder}.
@@ -47,25 +51,33 @@ public class PromoCardViewHolder extends RecyclerView.ViewHolder {
private PromoCardViewHolder(View rootView) {
super(rootView);
- mSettingsTextView = rootView.findViewById(R.id.settings_action);
- mOkTextView = rootView.findViewById(R.id.ok_action);
+ mPrimaryActionView = rootView.findViewById(R.id.primary_action);
+ mSecondaryActionView = rootView.findViewById(R.id.secondary_action);
}
- /**
- * Retrieves the "Settings" button.
+ /**
+ * Retrieves the "primary" action button (eg. "OK").
*
* @return The view.
*/
- public View getSettingsTextView() {
- return mSettingsTextView;
+ public View getPrimaryActionView() {
+ return mPrimaryActionView;
}
/**
- * Retrieves the "Ok" button.
+ * Retrieves the "secondary" action button (eg. "Cancel" or "More Info").
*
* @return The view.
*/
- public View getOkTextView() {
- return mOkTextView;
+ public View getSecondaryActionView() {
+ return mSecondaryActionView;
+ }
+
+ @NeededForTesting
+ public static PromoCardViewHolder createForTest(Context context) {
+ PromoCardViewHolder viewHolder = new PromoCardViewHolder(new View(context));
+ viewHolder.mPrimaryActionView = new View(context);
+ viewHolder.mSecondaryActionView = new View(context);
+ return viewHolder;
}
}
diff --git a/src/com/android/dialer/calllog/ShowCallHistoryViewHolder.java b/src/com/android/dialer/calllog/ShowCallHistoryViewHolder.java
deleted file mode 100644
index af36a4d33..000000000
--- a/src/com/android/dialer/calllog/ShowCallHistoryViewHolder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2015 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.content.Intent;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.dialer.R;
-
-public final class ShowCallHistoryViewHolder extends RecyclerView.ViewHolder {
-
- private ShowCallHistoryViewHolder(final Context context, View view) {
- super(view);
- view.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final Intent intent = new Intent(context, CallLogActivity.class);
- context.startActivity(intent);
- }
- });
- }
-
- public static ShowCallHistoryViewHolder create(Context context, ViewGroup parent) {
- LayoutInflater inflater = LayoutInflater.from(context);
- View view = inflater.inflate(R.layout.show_call_history_list_item, parent, false);
- return new ShowCallHistoryViewHolder(context, view);
- }
-}
diff --git a/src/com/android/dialer/calllog/VisualVoicemailCallLogFragment.java b/src/com/android/dialer/calllog/VisualVoicemailCallLogFragment.java
new file mode 100644
index 000000000..311ff7dc5
--- /dev/null
+++ b/src/com/android/dialer/calllog/VisualVoicemailCallLogFragment.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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.ContentObserver;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.dialer.R;
+import com.android.dialer.list.ListsFragment;
+import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
+
+public class VisualVoicemailCallLogFragment extends CallLogFragment {
+
+ private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
+
+ public VisualVoicemailCallLogFragment() {
+ super(CallLog.Calls.VOICEMAIL_TYPE);
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+ mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state);
+ getActivity().getContentResolver().registerContentObserver(
+ VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+ setupView(view, mVoicemailPlaybackPresenter);
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mVoicemailPlaybackPresenter.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ mVoicemailPlaybackPresenter.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ mVoicemailPlaybackPresenter.onDestroy();
+ getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void fetchCalls() {
+ super.fetchCalls();
+ ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+ }
+}
diff --git a/src/com/android/dialer/calllog/VoicemailQueryHandler.java b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
index 26f9bd172..c6e644c32 100644
--- a/src/com/android/dialer/calllog/VoicemailQueryHandler.java
+++ b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
@@ -59,7 +59,8 @@ public class VoicemailQueryHandler extends AsyncQueryHandler {
if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) {
if (mContext != null) {
Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class);
- serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
mContext.startService(serviceIntent);
} else {
Log.w(TAG, "Unknown update completed: ignoring: " + token);
diff --git a/src/com/android/dialer/calllog/calllogcache/CallLogCache.java b/src/com/android/dialer/calllog/calllogcache/CallLogCache.java
new file mode 100644
index 000000000..dc1217cf5
--- /dev/null
+++ b/src/com/android/dialer/calllog/calllogcache/CallLogCache.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 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.calllogcache;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.dialer.calllog.CallLogAdapter;
+
+/**
+ * This is the base class for the CallLogCaches.
+ *
+ * Keeps a cache of recently made queries to the Telecom/Telephony processes. The aim of this cache
+ * is to reduce the number of cross-process requests to TelecomManager, which can negatively affect
+ * performance.
+ *
+ * This is designed with the specific use case of the {@link CallLogAdapter} in mind.
+ */
+public abstract class CallLogCache {
+ // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of
+ // this writing, that was a much larger undertaking than creating this cache.
+
+ protected final Context mContext;
+
+ private boolean mHasCheckedForVideoEnabled;
+ private boolean mIsVideoEnabled;
+
+ public CallLogCache(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Return the most compatible version of the TelecomCallLogCache.
+ */
+ public static CallLogCache getCallLogCache(Context context) {
+ if (CompatUtils.isClassAvailable("android.telecom.PhoneAccountHandle")) {
+ return new CallLogCacheLollipopMr1(context);
+ }
+ return new CallLogCacheLollipop(context);
+ }
+
+ public void reset() {
+ mHasCheckedForVideoEnabled = false;
+ mIsVideoEnabled = false;
+ }
+
+ /**
+ * 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 abstract boolean isVoicemailNumber(PhoneAccountHandle accountHandle,
+ CharSequence number);
+
+ public boolean isVideoEnabled() {
+ if (!mHasCheckedForVideoEnabled) {
+ mIsVideoEnabled = CallUtil.isVideoEnabled(mContext);
+ mHasCheckedForVideoEnabled = true;
+ }
+ return mIsVideoEnabled;
+ }
+
+ /**
+ * Extract account label from PhoneAccount object.
+ */
+ public abstract String getAccountLabel(PhoneAccountHandle accountHandle);
+
+ /**
+ * Extract account color from PhoneAccount object.
+ */
+ public abstract int getAccountColor(PhoneAccountHandle accountHandle);
+
+ /**
+ * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note)
+ * for outgoing calls.
+ *
+ * @param accountHandle The PhoneAccount handle.
+ * @return {@code true} if calling with a note is supported, {@code false} otherwise.
+ */
+ public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle);
+}
diff --git a/src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipop.java b/src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipop.java
new file mode 100644
index 000000000..770cc9d3e
--- /dev/null
+++ b/src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipop.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 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.calllogcache;
+
+import android.content.Context;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/**
+ * This is a compatibility class for the CallLogCache for versions of dialer before Lollipop Mr1
+ * (the introduction of phone accounts).
+ *
+ * This class should not be initialized directly and instead be acquired from
+ * {@link CallLogCache#getCallLogCache}.
+ */
+class CallLogCacheLollipop extends CallLogCache {
+ private String mVoicemailNumber;
+
+ /* package */ CallLogCacheLollipop(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+
+ String numberString = number.toString();
+
+ if (!TextUtils.isEmpty(mVoicemailNumber)) {
+ return PhoneNumberUtils.compare(numberString, mVoicemailNumber);
+ }
+
+ if (PhoneNumberUtils.isVoiceMailNumber(numberString)) {
+ mVoicemailNumber = numberString;
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getAccountLabel(PhoneAccountHandle accountHandle) {
+ return null;
+ }
+
+ @Override
+ public int getAccountColor(PhoneAccountHandle accountHandle) {
+ return PhoneAccount.NO_HIGHLIGHT_COLOR;
+ }
+
+ @Override
+ public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) {
+ return false;
+ }
+}
diff --git a/src/com/android/dialer/calllog/TelecomCallLogCache.java b/src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipopMr1.java
index 7071669e5..d1e3f7bcf 100644
--- a/src/com/android/dialer/calllog/TelecomCallLogCache.java
+++ b/src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipopMr1.java
@@ -14,67 +14,50 @@
* limitations under the License
*/
-package com.android.dialer.calllog;
+package com.android.dialer.calllog.calllogcache;
import android.content.Context;
-import android.provider.CallLog;
-import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
import android.text.TextUtils;
-import android.util.Log;
import android.util.Pair;
-import com.android.contacts.common.CallUtil;
-import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.dialer.calllog.PhoneAccountUtils;
import com.android.dialer.util.PhoneNumberUtil;
-import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Map;
-import java.util.Set;
/**
- * Keeps a cache of recently made queries to the Telecom process. The aim of this cache is to
- * reduce the number of cross-process requests to TelecomManager, which can negatively affect
- * performance.
+ * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for
+ * multi-SIM devices.
*
- * This is designed with the specific use case of the {@link CallLogAdapter} in mind.
+ * This class should not be initialized directly and instead be acquired from
+ * {@link CallLogCache#getCallLogCache}.
*/
-public class TelecomCallLogCache {
- private final Context mContext;
-
+class CallLogCacheLollipopMr1 extends CallLogCache {
// Maps from a phone-account/number pair to a boolean because multiple numbers could return true
// for the voicemail number if those numbers are not pre-normalized.
- // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of
- // this writing, that was a much larger undertaking than creating this cache.
private final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache =
new HashMap<>();
private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>();
- private boolean mHasCheckedForVideoEnabled;
- private boolean mIsVideoEnabled;
-
- public TelecomCallLogCache(Context context) {
- mContext = context;
+ /* package */ CallLogCacheLollipopMr1(Context context) {
+ super(context);
}
+ @Override
public void reset() {
mVoicemailQueryCache.clear();
mPhoneAccountLabelCache.clear();
mPhoneAccountColorCache.clear();
mPhoneAccountCallWithNoteCache.clear();
- mHasCheckedForVideoEnabled = false;
- mIsVideoEnabled = false;
+ super.reset();
}
- /**
- * 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.
- */
+ @Override
public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) {
if (TextUtils.isEmpty(number)) {
return false;
@@ -91,9 +74,7 @@ public class TelecomCallLogCache {
}
}
- /**
- * Extract account label from PhoneAccount object.
- */
+ @Override
public String getAccountLabel(PhoneAccountHandle accountHandle) {
if (mPhoneAccountLabelCache.containsKey(accountHandle)) {
return mPhoneAccountLabelCache.get(accountHandle);
@@ -104,9 +85,7 @@ public class TelecomCallLogCache {
}
}
- /**
- * Extract account color from PhoneAccount object.
- */
+ @Override
public int getAccountColor(PhoneAccountHandle accountHandle) {
if (mPhoneAccountColorCache.containsKey(accountHandle)) {
return mPhoneAccountColorCache.get(accountHandle);
@@ -117,20 +96,7 @@ public class TelecomCallLogCache {
}
}
- public boolean isVideoEnabled() {
- if (!mHasCheckedForVideoEnabled) {
- mIsVideoEnabled = CallUtil.isVideoEnabled(mContext);
- }
- return mIsVideoEnabled;
- }
-
- /**
- * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note)
- * for outgoing calls.
- *
- * @param accountHandle The PhoneAccount handle.
- * @return {@code true} if calling with a note is supported, {@code false} otherwise.
- */
+ @Override
public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) {
if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) {
return mPhoneAccountCallWithNoteCache.get(accountHandle);