diff options
Diffstat (limited to 'src/com/android/dialer/calllog')
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); |