diff options
Diffstat (limited to 'java/com/android/dialer/app/calllog')
24 files changed, 1921 insertions, 1137 deletions
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java index 443171d3f..1bb894c59 100644 --- a/java/com/android/dialer/app/calllog/CallLogActivity.java +++ b/java/com/android/dialer/app/calllog/CallLogActivity.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.os.Bundle; import android.provider.CallLog; import android.provider.CallLog.Calls; +import android.support.design.widget.Snackbar; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; @@ -31,9 +32,14 @@ import android.view.ViewGroup; import com.android.contacts.common.list.ViewPagerTabs; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; +import com.android.dialer.calldetails.CallDetailsActivity; +import com.android.dialer.constants.ActivityRequestCodes; import com.android.dialer.database.CallLogQueryHandler; import com.android.dialer.logging.Logger; import com.android.dialer.logging.ScreenEvent; +import com.android.dialer.logging.UiAction; +import com.android.dialer.performancereport.PerformanceReport; +import com.android.dialer.postcall.PostCall; import com.android.dialer.util.TransactionSafeActivity; import com.android.dialer.util.ViewUtil; @@ -48,7 +54,6 @@ public class CallLogActivity extends TransactionSafeActivity private ViewPagerTabs mViewPagerTabs; private ViewPagerAdapter mViewPagerAdapter; private CallLogFragment mAllCallsFragment; - private CallLogFragment mMissedCallsFragment; private String[] mTabTitles; private boolean mIsResumed; @@ -93,9 +98,16 @@ public class CallLogActivity extends TransactionSafeActivity @Override protected void onResume() { + // Some calls may not be recorded (eg. from quick contact), + // so we should restart recording after these calls. (Recorded call is stopped) + PostCall.restartPerformanceRecordingIfARecentCallExist(this); + if (!PerformanceReport.isRecording()) { + PerformanceReport.startRecording(); + } + mIsResumed = true; super.onResume(); - sendScreenViewForChildFragment(mViewPager.getCurrentItem()); + sendScreenViewForChildFragment(); } @Override @@ -129,6 +141,7 @@ public class CallLogActivity extends TransactionSafeActivity } if (item.getItemId() == android.R.id.home) { + PerformanceReport.recordClick(UiAction.Type.CLOSE_CALL_HISTORY_WITH_CANCEL_BUTTON); final Intent intent = new Intent(this, DialtactsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); @@ -148,7 +161,7 @@ public class CallLogActivity extends TransactionSafeActivity @Override public void onPageSelected(int position) { if (mIsResumed) { - sendScreenViewForChildFragment(position); + sendScreenViewForChildFragment(); } mViewPagerTabs.onPageSelected(position); } @@ -158,7 +171,7 @@ public class CallLogActivity extends TransactionSafeActivity mViewPagerTabs.onPageScrollStateChanged(state); } - private void sendScreenViewForChildFragment(int position) { + private void sendScreenViewForChildFragment() { Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this); } @@ -169,6 +182,12 @@ public class CallLogActivity extends TransactionSafeActivity return position; } + @Override + public void onBackPressed() { + PerformanceReport.recordClick(UiAction.Type.PRESS_ANDROID_BACK_BUTTON); + super.onBackPressed(); + } + /** Adapter for the view pager. */ public class ViewPagerAdapter extends FragmentPagerAdapter { @@ -189,20 +208,16 @@ public class CallLogActivity extends TransactionSafeActivity CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */); case TAB_INDEX_MISSED: return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */); + default: + throw new IllegalStateException("No fragment at position " + position); } - throw new IllegalStateException("No fragment at position " + position); } @Override public Object instantiateItem(ViewGroup container, int position) { final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position); - switch (position) { - case TAB_INDEX_ALL: + if (position == TAB_INDEX_ALL) { mAllCallsFragment = fragment; - break; - case TAB_INDEX_MISSED: - mMissedCallsFragment = fragment; - break; } return fragment; } @@ -217,4 +232,22 @@ public class CallLogActivity extends TransactionSafeActivity return TAB_INDEX_COUNT; } } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == ActivityRequestCodes.DIALTACTS_CALL_DETAILS) { + if (resultCode == RESULT_OK + && data != null + && data.getBooleanExtra(CallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) { + String number = data.getStringExtra(CallDetailsActivity.EXTRA_PHONE_NUMBER); + Snackbar.make(findViewById(R.id.calllog_frame), getString(R.string.ec_data_deleted), 5_000) + .setAction( + R.string.view_conversation, + v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this))) + .setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color)) + .show(); + } + } + super.onActivityResult(requestCode, resultCode, data); + } } diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index 2f8a58c8a..61129a7ce 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -19,6 +19,7 @@ package com.android.dialer.app.calllog; import android.app.Activity; import android.content.ContentUris; import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; @@ -52,7 +53,6 @@ import android.view.ViewGroup; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.contacts.common.preference.ContactsPreferences; -import com.android.dialer.app.Bindings; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; @@ -63,31 +63,32 @@ import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDe import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; import com.android.dialer.calldetails.CallDetailsEntries; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.calllogutils.PhoneCallDetails; import com.android.dialer.common.Assert; -import com.android.dialer.common.ConfigProviderBindings; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.AsyncTaskExecutor; import com.android.dialer.common.concurrent.AsyncTaskExecutors; +import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.enrichedcall.EnrichedCallCapabilities; import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; import com.android.dialer.lightbringer.Lightbringer; import com.android.dialer.lightbringer.LightbringerComponent; import com.android.dialer.lightbringer.LightbringerListener; import com.android.dialer.logging.ContactSource; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; +import com.android.dialer.logging.UiAction; +import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.phonenumbercache.CallLogQuery; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.ContactInfoHelper; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.spam.Spam; import com.android.dialer.util.PermissionsUtil; -import java.util.Collections; -import java.util.List; +import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -105,11 +106,12 @@ public class CallLogAdapter extends GroupingListAdapter private static final String KEY_EXPANDED_POSITION = "expanded_position"; private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; + private static final String KEY_ACTION_MODE = "action_mode_selected_items"; public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data"; public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect"; - public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = false; + public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = true; protected final Activity mActivity; protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; @@ -117,6 +119,8 @@ public class CallLogAdapter extends GroupingListAdapter protected final CallLogCache mCallLogCache; private final CallFetcher mCallFetcher; + private final OnActionModeStateChangedListener mActionModeStateChangedListener; + private final MultiSelectRemoveView mMultiSelectRemoveView; @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; private final int mActivityType; @@ -136,6 +140,8 @@ public class CallLogAdapter extends GroupingListAdapter private final CallLogAlertManager mCallLogAlertManager; public ActionMode mActionMode = null; + public boolean selectAllMode = false; + public boolean deselectAllMode = false; private final SparseArray<String> selectedItems = new SparseArray<>(); private final ActionMode.Callback mActionModeCallback = @@ -144,10 +150,17 @@ public class CallLogAdapter extends GroupingListAdapter // Called when the action mode is created; startActionMode() was called @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (mActivity != null) { + announceforAccessibility( + mActivity.getCurrentFocus(), + mActivity.getString(R.string.description_entering_bulk_action_mode)); + } mActionMode = mode; // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.actionbar_delete, menu); + mMultiSelectRemoveView.showMultiSelectRemoveView(true); + mActionModeStateChangedListener.onActionModeStateChanged(true); return true; } @@ -162,10 +175,10 @@ public class CallLogAdapter extends GroupingListAdapter @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (item.getItemId() == R.id.action_bar_delete_menu_item) { + Logger.get(mActivity).logImpression(DialerImpression.Type.MULTISELECT_TAP_DELETE_ICON); if (selectedItems.size() > 0) { showDeleteSelectedItemsDialog(); } - mode.finish(); return true; } else { return false; @@ -175,53 +188,78 @@ public class CallLogAdapter extends GroupingListAdapter // Called when the user exits the action mode @Override public void onDestroyActionMode(ActionMode mode) { + if (mActivity != null) { + announceforAccessibility( + mActivity.getCurrentFocus(), + mActivity.getString(R.string.description_leaving_bulk_action_mode)); + } selectedItems.clear(); mActionMode = null; + selectAllMode = false; + deselectAllMode = false; + mMultiSelectRemoveView.showMultiSelectRemoveView(false); + mActionModeStateChangedListener.onActionModeStateChanged(false); notifyDataSetChanged(); } }; - // Todo (uabdullah): Use plurals http://b/37751831 private void showDeleteSelectedItemsDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); - Assert.checkArgument(selectedItems.size() > 0); - String voicemailString = - selectedItems.size() == 1 - ? mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemail) - : mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemails); - String deleteVoicemailTitle = - mActivity - .getResources() - .getString(R.string.voicemailMultiSelectDialogTitle, voicemailString); SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone(); - builder.setTitle(deleteVoicemailTitle); - - builder.setPositiveButton( - mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteConfirm), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - deleteSelectedItems(voicemailsToDeleteOnConfirmation); - dialog.cancel(); - } - }); - - builder.setNegativeButton( - mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteCancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - AlertDialog dialog = builder.create(); - dialog.show(); + new AlertDialog.Builder(mActivity, R.style.AlertDialogCustom) + .setCancelable(true) + .setTitle( + mActivity + .getResources() + .getQuantityString( + R.plurals.delete_voicemails_confirmation_dialog_title, selectedItems.size())) + .setPositiveButton( + R.string.voicemailMultiSelectDeleteConfirm, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int button) { + LogUtil.i( + "CallLogAdapter.showDeleteSelectedItemsDialog", + "onClick, these items to delete " + voicemailsToDeleteOnConfirmation); + deleteSelectedItems(voicemailsToDeleteOnConfirmation); + mActionMode.finish(); + dialog.cancel(); + Logger.get(mActivity) + .logImpression( + DialerImpression.Type.MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG); + } + }) + .setOnCancelListener( + new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + Logger.get(mActivity) + .logImpression( + DialerImpression.Type + .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH); + dialogInterface.cancel(); + } + }) + .setNegativeButton( + R.string.voicemailMultiSelectDeleteCancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int button) { + Logger.get(mActivity) + .logImpression( + DialerImpression.Type + .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON); + dialog.cancel(); + } + }) + .show(); + Logger.get(mActivity) + .logImpression(DialerImpression.Type.MULTISELECT_DISPLAY_DELETE_CONFIRMATION_DIALOG); } private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) { for (int i = 0; i < voicemailsToDelete.size(); i++) { String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i)); + LogUtil.i("CallLogAdapter.deleteSelectedItems", "deleting uri:" + voicemailUri); CallLogAsyncTaskUtil.deleteVoicemail(mActivity, Uri.parse(voicemailUri), null); } } @@ -235,8 +273,13 @@ public class CallLogAdapter extends GroupingListAdapter && mVoicemailPlaybackPresenter != null) { if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) { if (mActionMode == null) { + Logger.get(mActivity) + .logImpression( + DialerImpression.Type.MULTISELECT_LONG_PRESS_ENTER_MULTI_SELECT_MODE); mActionMode = v.startActionMode(mActionModeCallback); } + Logger.get(mActivity) + .logImpression(DialerImpression.Type.MULTISELECT_LONG_PRESS_TAP_ENTRY); CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); viewHolder.quickContactView.setVisibility(View.GONE); viewHolder.checkBoxView.setVisibility(View.VISIBLE); @@ -248,32 +291,45 @@ public class CallLogAdapter extends GroupingListAdapter } }; + @VisibleForTesting + public View.OnClickListener getExpandCollapseListener() { + return mExpandCollapseListener; + } + /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */ private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() { @Override public void onClick(View v) { + PerformanceReport.recordClick(UiAction.Type.CLICK_CALL_LOG_ITEM); + CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); if (viewHolder == null) { return; } if (mActionMode != null && viewHolder.voicemailUri != null) { + selectAllMode = false; + deselectAllMode = false; + mMultiSelectRemoveView.setSelectAllModeToFalse(); int id = getVoicemailId(viewHolder.voicemailUri); if (selectedItems.get(id) != null) { - selectedItems.delete(id); - viewHolder.checkBoxView.setVisibility(View.GONE); - viewHolder.quickContactView.setVisibility(View.VISIBLE); + Logger.get(mActivity) + .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_UNSELECT_ENTRY); + uncheckMarkCallLogEntry(viewHolder, id); } else { - viewHolder.quickContactView.setVisibility(View.GONE); - viewHolder.checkBoxView.setVisibility(View.VISIBLE); - selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri); - } - - if (selectedItems.size() == 0) { - mActionMode.finish(); - return; + Logger.get(mActivity) + .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_SELECT_ENTRY); + checkMarkCallLogEntry(viewHolder); + // select all check box logic + if (getItemCount() == selectedItems.size()) { + LogUtil.i( + "mExpandCollapseListener.onClick", + "getitem count %d is equal to items select count %d, check select all box", + getItemCount(), + selectedItems.size()); + mMultiSelectRemoveView.tapSelectAll(); + } } - mActionMode.setTitle(Integer.toString(selectedItems.size())); return; } @@ -285,14 +341,29 @@ public class CallLogAdapter extends GroupingListAdapter // If enriched call capabilities were unknown on the initial load, // viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities // as a last attempt at getting them before showing the expanded view to the user - EnrichedCallCapabilities capabilities = - getEnrichedCallManager().getCapabilities(viewHolder.number); - viewHolder.isCallComposerCapable = - capabilities != null && capabilities.supportsCallComposer(); - generateAndMapNewCallDetailsEntriesHistoryResults( - viewHolder.number, - viewHolder.getDetailedPhoneDetails(), - getAllHistoricalData(viewHolder.number, viewHolder.getDetailedPhoneDetails())); + EnrichedCallCapabilities capabilities = null; + + if (viewHolder.number != null) { + capabilities = getEnrichedCallManager().getCapabilities(viewHolder.number); + } + + if (capabilities == null) { + capabilities = EnrichedCallCapabilities.NO_CAPABILITIES; + } + + viewHolder.isCallComposerCapable = capabilities.isCallComposerCapable(); + + if (capabilities.isTemporarilyUnavailable()) { + LogUtil.i( + "mExpandCollapseListener.onClick", + "%s is temporarily unavailable, requesting capabilities", + LogUtil.sanitizePhoneNumber(viewHolder.number)); + // Refresh the capabilities when temporarily unavailable, see go/ec-temp-unavailable. + // Similarly to when we request capabilities the first time, the 'Share and call' button + // won't pop in with the new capabilities. Instead the row needs to be collapsed and + // expanded again. + getEnrichedCallManager().requestCapabilities(viewHolder.number); + } if (viewHolder.rowId == mCurrentlyExpandedRowId) { // Hide actions, if the clicked item is the expanded item. @@ -308,10 +379,77 @@ public class CallLogAdapter extends GroupingListAdapter } } expandViewHolderActions(viewHolder); + + if (isLightbringerCallButtonVisible(viewHolder.videoCallButtonView)) { + CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount(); + } + } + } + + private boolean isLightbringerCallButtonVisible(View videoCallButtonView) { + if (videoCallButtonView == null) { + return false; + } + if (videoCallButtonView.getVisibility() != View.VISIBLE) { + return false; + } + IntentProvider intentProvider = (IntentProvider) videoCallButtonView.getTag(); + if (intentProvider == null) { + return false; + } + String packageName = + LightbringerComponent.get(mActivity).getLightbringer().getPackageName(); + if (packageName == null) { + return false; } + return packageName.equals(intentProvider.getIntent(mActivity).getPackage()); } }; + private void checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder) { + announceforAccessibility( + mActivity.getCurrentFocus(), + mActivity.getString( + R.string.description_selecting_bulk_action_mode, viewHolder.nameOrNumber)); + viewHolder.quickContactView.setVisibility(View.GONE); + viewHolder.checkBoxView.setVisibility(View.VISIBLE); + selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri); + updateActionBar(); + } + + private void announceforAccessibility(View view, String announcement) { + if (view != null) { + view.announceForAccessibility(announcement); + } + } + + private void updateActionBar() { + if (mActionMode == null && selectedItems.size() > 0) { + Logger.get(mActivity) + .logImpression(DialerImpression.Type.MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE); + mActivity.startActionMode(mActionModeCallback); + } + if (mActionMode != null) { + mActionMode.setTitle( + mActivity + .getResources() + .getString( + R.string.voicemailMultiSelectActionBarTitle, + Integer.toString(selectedItems.size()))); + } + } + + private void uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id) { + announceforAccessibility( + mActivity.getCurrentFocus(), + mActivity.getString( + R.string.description_unselecting_bulk_action_mode, viewHolder.nameOrNumber)); + selectedItems.delete(id); + viewHolder.checkBoxView.setVisibility(View.GONE); + viewHolder.quickContactView.setVisibility(View.VISIBLE); + updateActionBar(); + } + private static int getVoicemailId(String voicemailUri) { Assert.checkArgument(voicemailUri != null); Assert.checkArgument(voicemailUri.length() > 0); @@ -328,7 +466,7 @@ public class CallLogAdapter extends GroupingListAdapter * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo * timeout, all of the pending URIs will be deleted. * - * <p>TODO: move this and OnVoicemailDeletedListener to somewhere like {@link + * <p>TODO(twyen): move this and OnVoicemailDeletedListener to somewhere like {@link * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with * hidden item or what to hide. */ @@ -358,6 +496,8 @@ public class CallLogAdapter extends GroupingListAdapter Activity activity, ViewGroup alertContainer, CallFetcher callFetcher, + MultiSelectRemoveView multiSelectRemoveView, + OnActionModeStateChangedListener actionModeStateChangedListener, CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, @@ -367,6 +507,8 @@ public class CallLogAdapter extends GroupingListAdapter mActivity = activity; mCallFetcher = callFetcher; + mActionModeStateChangedListener = actionModeStateChangedListener; + mMultiSelectRemoveView = multiSelectRemoveView; mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); @@ -426,6 +568,25 @@ public class CallLogAdapter extends GroupingListAdapter public void onSaveInstanceState(Bundle outState) { outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); + + ArrayList<String> listOfSelectedItems = new ArrayList<>(); + + if (selectedItems.size() > 0) { + for (int i = 0; i < selectedItems.size(); i++) { + int id = selectedItems.keyAt(i); + String voicemailUri = selectedItems.valueAt(i); + LogUtil.i( + "CallLogAdapter.onSaveInstanceState", "index %d, id=%d, uri=%s ", i, id, voicemailUri); + listOfSelectedItems.add(voicemailUri); + } + } + outState.putStringArrayList(KEY_ACTION_MODE, listOfSelectedItems); + + LogUtil.i( + "CallLogAdapter.onSaveInstanceState", + "saved: %d, selectedItemsSize:%d", + listOfSelectedItems.size(), + selectedItems.size()); } public void onRestoreInstanceState(Bundle savedInstanceState) { @@ -434,6 +595,33 @@ public class CallLogAdapter extends GroupingListAdapter savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); mCurrentlyExpandedRowId = savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); + // Restoring multi selected entries + ArrayList<String> listOfSelectedItems = + savedInstanceState.getStringArrayList(KEY_ACTION_MODE); + LogUtil.i( + "CallLogAdapter.onRestoreInstanceState", + "restored selectedItemsList:%d", + listOfSelectedItems.size()); + + if (!listOfSelectedItems.isEmpty()) { + for (int i = 0; i < listOfSelectedItems.size(); i++) { + String voicemailUri = listOfSelectedItems.get(i); + int id = getVoicemailId(voicemailUri); + LogUtil.i( + "CallLogAdapter.onRestoreInstanceState", + "restoring selected index %d, id=%d, uri=%s ", + i, + id, + voicemailUri); + selectedItems.put(id, voicemailUri); + } + + LogUtil.i( + "CallLogAdapter.onRestoreInstance", + "restored selectedItems %s", + selectedItems.toString()); + updateActionBar(); + } } } @@ -521,6 +709,7 @@ public class CallLogAdapter extends GroupingListAdapter mBlockReportSpamListener, mExpandCollapseListener, mLongPressListener, + mActionModeStateChangedListener, mCallLogCache, mCallLogListItemHelper, mVoicemailPlaybackPresenter); @@ -546,7 +735,7 @@ public class CallLogAdapter extends GroupingListAdapter Trace.beginSection("onBindViewHolder: " + position); switch (getItemViewType(position)) { case VIEW_TYPE_ALERT: - //Do nothing + // Do nothing break; default: bindCallLogListViewHolder(viewHolder, position); @@ -559,6 +748,8 @@ public class CallLogAdapter extends GroupingListAdapter public void onViewRecycled(ViewHolder viewHolder) { if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + updateCheckMarkedStatusOfEntry(views); + if (views.asyncTask != null) { views.asyncTask.cancel(true); } @@ -591,6 +782,8 @@ public class CallLogAdapter extends GroupingListAdapter return; } CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + updateCheckMarkedStatusOfEntry(views); + views.isLoaded = false; int groupSize = getGroupSize(position); CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize); @@ -609,6 +802,17 @@ public class CallLogAdapter extends GroupingListAdapter loadAndRender(views, views.rowId, details, callDetailsEntries); } + private void updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views) { + if (selectedItems.size() > 0 && views.voicemailUri != null) { + int id = getVoicemailId(views.voicemailUri); + if (selectedItems.get(id) != null) { + checkMarkCallLogEntry(views); + } else { + uncheckMarkCallLogEntry(views, id); + } + } + } + private void loadAndRender( final CallLogListItemViewHolder views, final long rowId, @@ -625,12 +829,7 @@ public class CallLogAdapter extends GroupingListAdapter // the value will be false while capabilities are requested. mExpandCollapseListener will // attempt to set the field properly in that case views.isCallComposerCapable = isCallComposerCapable(views.number); - CallDetailsEntries updatedCallDetailsEntries = - generateAndMapNewCallDetailsEntriesHistoryResults( - views.number, - callDetailsEntries, - getAllHistoricalData(views.number, callDetailsEntries)); - views.setDetailedPhoneDetails(updatedCallDetailsEntries); + views.setDetailedPhoneDetails(callDetailsEntries); views.lightbringerReady = getLightbringer().isReachable(mActivity, views.number); final AsyncTask<Void, Void, Boolean> loadDataTask = new AsyncTask<Void, Void, Boolean>() { @@ -687,46 +886,7 @@ public class CallLogAdapter extends GroupingListAdapter getEnrichedCallManager().requestCapabilities(number); return false; } - return capabilities.supportsCallComposer(); - } - - @NonNull - private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( - @Nullable String number, @NonNull CallDetailsEntries entries) { - if (number == null) { - return Collections.emptyMap(); - } - - Map<CallDetailsEntry, List<HistoryResult>> historicalData = - getEnrichedCallManager().getAllHistoricalData(number, entries); - if (historicalData == null) { - getEnrichedCallManager().requestAllHistoricalData(number, entries); - return Collections.emptyMap(); - } - return historicalData; - } - - private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( - @Nullable String number, - @NonNull CallDetailsEntries callDetailsEntries, - @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { - if (number == null) { - return callDetailsEntries; - } - CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); - for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { - CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); - List<HistoryResult> results = mappedResults.get(entry); - if (results != null) { - newEntry.addAllHistoryResults(mappedResults.get(entry)); - LogUtil.v( - "CallLogAdapter.generateAndMapNewCallDetailsEntriesHistoryResults", - "mapped %d results", - newEntry.getHistoryResultsList().size()); - } - mutableCallDetailsEntries.addEntries(newEntry.build()); - } - return mutableCallDetailsEntries.build(); + return capabilities.isCallComposerCapable(); } /** @@ -744,6 +904,10 @@ public class CallLogAdapter extends GroupingListAdapter (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor); + final int transcriptionState = + (VERSION.SDK_INT >= VERSION_CODES.O) + ? cursor.getInt(CallLogQuery.TRANSCRIPTION_STATE) + : PhoneCallDetailsHelper.TRANSCRIPTION_NOT_STARTED; final PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits); details.viaNumber = viaNumber; @@ -753,6 +917,7 @@ public class CallLogAdapter extends GroupingListAdapter details.features = getCallFeatures(cursor, count); details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION); details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION); + details.transcriptionState = transcriptionState; details.callTypes = getCallTypes(cursor, count); details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); @@ -785,7 +950,7 @@ public class CallLogAdapter extends GroupingListAdapter } @MainThread - private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) { + private CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) { Assert.isMainThread(); int position = cursor.getPosition(); CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder(); @@ -798,6 +963,16 @@ public class CallLogAdapter extends GroupingListAdapter .setDate(cursor.getLong(CallLogQuery.DATE)) .setDuration(cursor.getLong(CallLogQuery.DURATION)) .setFeatures(cursor.getInt(CallLogQuery.FEATURES)); + + String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + if (getLightbringer().getPhoneAccountComponentName() != null + && getLightbringer() + .getPhoneAccountComponentName() + .flattenToString() + .equals(phoneAccountComponentName)) { + entry.setIsLightbringerCall(true); + } + entries.addEntries(entry.build()); cursor.moveToNext(); } @@ -840,8 +1015,7 @@ public class CallLogAdapter extends GroupingListAdapter details.countryIso, details.cachedContactInfo, position - < Bindings.get(mActivity) - .getConfigProvider() + < ConfigProviderBindings.get(mActivity) .getLong("number_of_call_to_do_remote_lookup", 5L)); } CharSequence formattedNumber = @@ -917,6 +1091,12 @@ public class CallLogAdapter extends GroupingListAdapter views.workIconView.setVisibility( details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); + if (selectAllMode && views.voicemailUri != null) { + selectedItems.put(getVoicemailId(views.voicemailUri), views.voicemailUri); + } + if (deselectAllMode && views.voicemailUri != null) { + selectedItems.delete(getVoicemailId(views.voicemailUri)); + } if (views.voicemailUri != null && selectedItems.get(getVoicemailId(views.voicemailUri)) != null) { views.checkBoxView.setVisibility(View.VISIBLE); @@ -925,7 +1105,6 @@ public class CallLogAdapter extends GroupingListAdapter views.checkBoxView.setVisibility(View.GONE); views.quickContactView.setVisibility(View.VISIBLE); } - mCallLogListItemHelper.setPhoneCallDetails(views, details); if (mCurrentlyExpandedRowId == views.rowId) { // In case ViewHolders were added/removed, update the expanded position if the rowIds @@ -1192,9 +1371,51 @@ public class CallLogAdapter extends GroupingListAdapter notifyDataSetChanged(); } + public void onAllSelected() { + selectAllMode = true; + deselectAllMode = false; + selectedItems.clear(); + for (int i = 0; i < getItemCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + Assert.checkArgument(CallLogQuery.VOICEMAIL_URI == c.getColumnIndex("voicemail_uri")); + String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); + selectedItems.put(getVoicemailId(voicemailUri), voicemailUri); + } + } + updateActionBar(); + notifyDataSetChanged(); + } + + public void onAllDeselected() { + selectAllMode = false; + deselectAllMode = true; + selectedItems.clear(); + updateActionBar(); + notifyDataSetChanged(); + } + /** Interface used to initiate a refresh of the content. */ public interface CallFetcher { void fetchCalls(); } + + /** Interface used to allow single tap multi select for contact photos. */ + public interface OnActionModeStateChangedListener { + + void onActionModeStateChanged(boolean isEnabled); + + boolean isActionModeStateEnabled(); + } + + /** Interface used to hide the fragments. */ + public interface MultiSelectRemoveView { + + void showMultiSelectRemoveView(boolean show); + + void setSelectAllModeToFalse(); + + void tapSelectAll(); + } } diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java index a5553d134..78ec7a695 100644 --- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java +++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java @@ -28,6 +28,7 @@ import android.provider.VoicemailContract.Voicemails; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.AsyncTaskExecutor; import com.android.dialer.common.concurrent.AsyncTaskExecutors; import com.android.dialer.util.PermissionsUtil; @@ -45,6 +46,7 @@ public class CallLogAsyncTaskUtil { public static void markVoicemailAsRead( @NonNull final Context context, @NonNull final Uri voicemailUri) { + LogUtil.enterBlock("CallLogAsyncTaskUtil.markVoicemailAsRead, voicemailUri: " + voicemailUri); if (sAsyncTaskExecutor == null) { initTaskExecutor(); } @@ -64,11 +66,8 @@ public class CallLogAsyncTaskUtil { .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null) > 0) { uploadVoicemailLocalChangesToServer(context); + CallLogNotificationsService.markAllNewVoicemailsAsOld(context); } - - Intent intent = new Intent(context, CallLogNotificationsService.class); - intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); - context.startService(intent); return null; } }); @@ -110,7 +109,8 @@ public class CallLogAsyncTaskUtil { } public static void markCallAsRead(@NonNull final Context context, @NonNull final long[] callIds) { - if (!PermissionsUtil.hasPhonePermissions(context)) { + if (!PermissionsUtil.hasPhonePermissions(context) + || !PermissionsUtil.hasCallLogWritePermissions(context)) { return; } if (sAsyncTaskExecutor == null) { diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java index 6e4b23fc1..6d4aea91f 100644 --- a/java/com/android/dialer/app/calllog/CallLogFragment.java +++ b/java/com/android/dialer/app/calllog/CallLogFragment.java @@ -20,7 +20,6 @@ import static android.Manifest.permission.READ_CALL_LOG; import android.app.Activity; import android.app.Fragment; -import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; @@ -35,53 +34,68 @@ import android.provider.ContactsContract; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import android.support.v13.app.FragmentCompat; +import android.support.v13.app.FragmentCompat.OnRequestPermissionsResultCallback; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; import com.android.dialer.app.Bindings; import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogAdapter.CallFetcher; +import com.android.dialer.app.calllog.CallLogAdapter.MultiSelectRemoveView; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.app.contactinfo.ContactInfoCache; import com.android.dialer.app.contactinfo.ContactInfoCache.OnContactInfoChangedListener; import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment; import com.android.dialer.app.list.ListsFragment; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; -import com.android.dialer.app.widget.EmptyContentView; -import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.database.CallLogQueryHandler.Listener; import com.android.dialer.location.GeoUtil; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.oem.CequintCallerIdManager; +import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.phonenumbercache.ContactInfoHelper; import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.widget.EmptyContentView; +import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import java.util.Arrays; /** * 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, + implements Listener, + CallFetcher, + MultiSelectRemoveView, OnEmptyViewActionButtonClickedListener, - FragmentCompat.OnRequestPermissionsResultCallback, - CallLogModalAlertManager.Listener { + OnRequestPermissionsResultCallback, + CallLogModalAlertManager.Listener, + OnClickListener { 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"; private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission"; private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required"; + private static final String KEY_SELECT_ALL_MODE = "select_all_mode_checked"; // No limit specified for the number of logs to show; use the CallLogQueryHandler's default. private static final int NO_LOG_LIMIT = -1; // No date-based filtering. private static final int NO_DATE_LIMIT = 0; - private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; + private static final int PHONE_PERMISSIONS_REQUEST_CODE = 1; private static final int EVENT_UPDATE_DISPLAY = 1; @@ -90,13 +104,15 @@ public class CallLogFragment extends Fragment // See issue 6363009 private final ContentObserver mCallLogObserver = new CustomContentObserver(); private final ContentObserver mContactsObserver = new CustomContentObserver(); + private View mMultiSelectUnSelectAllViewContent; + private TextView mSelectUnselectAllViewText; + private ImageView mSelectUnselectAllIcon; private RecyclerView mRecyclerView; private LinearLayoutManager mLayoutManager; private CallLogAdapter mAdapter; private CallLogQueryHandler mCallLogQueryHandler; private boolean mScrollToTop; private EmptyContentView mEmptyListView; - private KeyguardManager mKeyguardManager; private ContactInfoCache mContactInfoCache; private final OnContactInfoChangedListener mOnContactInfoChangedListener = new OnContactInfoChangedListener() { @@ -123,6 +139,7 @@ public class CallLogFragment extends Fragment * True if this instance of the CallLogFragment shown in the CallLogActivity. */ private boolean mIsCallLogActivity = false; + private boolean selectAllMode; private final Handler mDisplayUpdateHandler = new Handler() { @Override @@ -194,12 +211,12 @@ public class CallLogFragment extends Fragment mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false); mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); + selectAllMode = state.getBoolean(KEY_SELECT_ALL_MODE, false); } final Activity activity = getActivity(); final ContentResolver resolver = activity.getContentResolver(); mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit); - mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); if (PermissionsUtil.hasCallLogReadPermissions(getContext())) { resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); @@ -290,12 +307,20 @@ public class CallLogFragment extends Fragment mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLayoutManager); + PerformanceReport.logOnScrollStateChange(mRecyclerView); mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); mEmptyListView.setImage(R.drawable.empty_call_log); mEmptyListView.setActionClickedListener(this); mModalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container); mModalAlertManager = new CallLogModalAlertManager(LayoutInflater.from(getContext()), mModalAlertView, this); + mMultiSelectUnSelectAllViewContent = + view.findViewById(R.id.multi_select_select_all_view_content); + mSelectUnselectAllViewText = (TextView) view.findViewById(R.id.select_all_view_text); + mSelectUnselectAllIcon = (ImageView) view.findViewById(R.id.select_all_view_icon); + mMultiSelectUnSelectAllViewContent.setOnClickListener(null); + mSelectUnselectAllIcon.setOnClickListener(this); + mSelectUnselectAllViewText.setOnClickListener(this); } protected void setupData() { @@ -317,7 +342,11 @@ public class CallLogFragment extends Fragment getActivity(), mRecyclerView, this, - CallLogCache.getCallLogCache(getActivity()), + this, + activityType == CallLogAdapter.ACTIVITY_TYPE_DIALTACTS + ? (CallLogAdapter.OnActionModeStateChangedListener) getActivity() + : null, + new CallLogCache(getActivity()), mContactInfoCache, getVoicemailPlaybackPresenter(), new FilteredNumberAsyncQueryHandler(getActivity()), @@ -335,9 +364,18 @@ public class CallLogFragment extends Fragment public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setupData(); + updateSelectAllState(savedInstanceState); mAdapter.onRestoreInstanceState(savedInstanceState); } + private void updateSelectAllState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + if (savedInstanceState.getBoolean(KEY_SELECT_ALL_MODE, false)) { + updateSelectAllIcon(); + } + } + } + @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -380,9 +418,17 @@ public class CallLogFragment extends Fragment } @Override - public void onStop() { - updateOnTransition(); + public void onStart() { + super.onStart(); + CequintCallerIdManager cequintCallerIdManager = null; + if (CequintCallerIdManager.isCequintCallerIdEnabled(getContext())) { + cequintCallerIdManager = CequintCallerIdManager.createInstanceForCallLog(); + } + mContactInfoCache.setCequintCallerIdManager(cequintCallerIdManager); + } + @Override + public void onStop() { super.onStop(); mAdapter.onStop(); mContactInfoCache.stop(); @@ -407,7 +453,7 @@ public class CallLogFragment extends Fragment outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission); outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); - + outState.putBoolean(KEY_SELECT_ALL_MODE, selectAllMode); mAdapter.onSaveInstanceState(outState); } @@ -451,6 +497,8 @@ public class CallLogFragment extends Fragment mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { mEmptyListView.setActionLabel(R.string.call_log_all_empty_action); + } else { + mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); } } @@ -463,9 +511,7 @@ public class CallLogFragment extends Fragment super.setMenuVisibility(menuVisible); if (mMenuVisible != menuVisible) { mMenuVisible = menuVisible; - if (!menuVisible) { - updateOnTransition(); - } else if (isResumed()) { + if (menuVisible && isResumed()) { refreshData(); } } @@ -483,7 +529,6 @@ public class CallLogFragment extends Fragment fetchCalls(); mCallLogQueryHandler.fetchVoicemailStatus(); mCallLogQueryHandler.fetchMissedCallsUnreadCount(); - updateOnTransition(); mRefreshDataRequired = false; } else { // Refresh the display of the existing data to update the timestamp text descriptions. @@ -491,22 +536,6 @@ public class CallLogFragment extends Fragment } } - /** - * Updates the voicemail notification state. - * - * <p>TODO: Move to CallLogActivity - */ - 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() - && mCallTypeFilter == Calls.VOICEMAIL_TYPE) { - CallLogNotificationsService.markNewVoicemailsAsOld(getActivity(), null); - } - } - @Override public void onEmptyViewActionButtonClicked() { final Activity activity = getActivity(); @@ -514,9 +543,14 @@ public class CallLogFragment extends Fragment return; } - if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { - FragmentCompat.requestPermissions( - this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); + String[] deniedPermissions = + PermissionsUtil.getPermissionsCurrentlyDenied( + getContext(), PermissionsUtil.allPhoneGroupPermissionsUsedInDialer); + if (deniedPermissions.length > 0) { + LogUtil.i( + "CallLogFragment.onEmptyViewActionButtonClicked", + "Requesting permissions: " + Arrays.toString(deniedPermissions)); + FragmentCompat.requestPermissions(this, deniedPermissions, PHONE_PERMISSIONS_REQUEST_CODE); } else if (!mIsCallLogActivity) { // Show dialpad if we are not in the call log activity. ((HostInterface) activity).showDialpad(); @@ -526,7 +560,7 @@ public class CallLogFragment extends Fragment @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { + if (requestCode == PHONE_PERMISSIONS_REQUEST_CODE) { if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { // Force a refresh of the data since we were missing the permission before this. mRefreshDataRequired = true; @@ -589,6 +623,51 @@ public class CallLogFragment extends Fragment } } + @Override + public void showMultiSelectRemoveView(boolean show) { + mMultiSelectUnSelectAllViewContent.setVisibility(show ? View.VISIBLE : View.GONE); + mMultiSelectUnSelectAllViewContent.setAlpha(show ? 0 : 1); + mMultiSelectUnSelectAllViewContent.animate().alpha(show ? 1 : 0).start(); + ((ListsFragment) getParentFragment()).showMultiSelectRemoveView(show); + } + + @Override + public void setSelectAllModeToFalse() { + selectAllMode = false; + mSelectUnselectAllIcon.setImageDrawable( + getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp)); + } + + @Override + public void tapSelectAll() { + LogUtil.i("CallLogFragment.tapSelectAll", "imitating select all"); + selectAllMode = true; + updateSelectAllIcon(); + } + + @Override + public void onClick(View v) { + selectAllMode = !selectAllMode; + if (selectAllMode) { + Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_SELECT_ALL); + } else { + Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_UNSELECT_ALL); + } + updateSelectAllIcon(); + } + + private void updateSelectAllIcon() { + if (selectAllMode) { + mSelectUnselectAllIcon.setImageDrawable( + getContext().getDrawable(R.drawable.ic_check_mark_blue_24dp)); + getAdapter().onAllSelected(); + } else { + mSelectUnselectAllIcon.setImageDrawable( + getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp)); + getAdapter().onAllDeselected(); + } + } + public interface HostInterface { void showDialpad(); diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java index 1daccd1a4..60ed7dd09 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -17,40 +17,45 @@ package com.android.dialer.app.calllog; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; +import android.os.Bundle; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; +import android.telecom.VideoProfile; import android.telephony.PhoneNumberUtils; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.view.ContextMenu; +import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewStub; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.QuickContactBadge; import android.widget.TextView; +import android.widget.Toast; import com.android.contacts.common.ClipboardUtils; -import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.contacts.common.dialog.CallSubjectDialog; -import com.android.contacts.common.util.UriUtils; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogAdapter.OnActionModeStateChangedListener; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.app.voicemail.VoicemailPlaybackLayout; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; @@ -58,23 +63,39 @@ import com.android.dialer.blocking.BlockedNumbersMigrator; import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.blocking.FilteredNumbersUtil; import com.android.dialer.callcomposer.CallComposerActivity; -import com.android.dialer.callcomposer.CallComposerContact; +import com.android.dialer.calldetails.CallDetailsActivity; import com.android.dialer.calldetails.CallDetailsEntries; -import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; +import com.android.dialer.compat.telephony.TelephonyManagerCompat; +import com.android.dialer.configprovider.ConfigProviderBindings; +import com.android.dialer.constants.ActivityRequestCodes; +import com.android.dialer.contactphoto.ContactPhotoManager; +import com.android.dialer.dialercontact.DialerContact; +import com.android.dialer.dialercontact.SimDetails; +import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.lettertile.LetterTileDrawable.ContactType; import com.android.dialer.lightbringer.Lightbringer; import com.android.dialer.lightbringer.LightbringerComponent; import com.android.dialer.logging.ContactSource; import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.InteractionEvent; import com.android.dialer.logging.Logger; import com.android.dialer.logging.ScreenEvent; +import com.android.dialer.logging.UiAction; +import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.phonenumbercache.CachedNumberLookupService; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.PhoneNumberCache; import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.CallUtil; import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.UriUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * This is an object containing references to views contained by the call log list item. This @@ -90,7 +111,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder /** The root view of the call log list item */ public final View rootView; /** The quick contact badge for the contact. */ - public final QuickContactBadge quickContactView; + public final DialerQuickContactBadge quickContactView; /** The primary action view of the entry. */ public final View primaryActionView; /** The details of the phone call. */ @@ -103,11 +124,13 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public final ImageView primaryActionButtonView; private final Context mContext; + @Nullable private final PhoneAccountHandle mDefaultPhoneAccountHandle; private final CallLogCache mCallLogCache; private final CallLogListItemHelper mCallLogListItemHelper; private final CachedNumberLookupService mCachedNumberLookupService; private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; private final OnClickListener mBlockReportListener; + @HostUi private final int hostUi; /** Whether the data fields are populated by the worker thread, ready to be shown. */ public boolean isLoaded; /** The view containing call log item actions. Null until the ViewStub is inflated. */ @@ -144,7 +167,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder * The callable phone number for the current call log entry. Cached here as the call back intent * is set only when the actions ViewStub is inflated. */ - public String number; + @Nullable public String number; /** The post-dial numbers that are dialed following the phone number. */ public String postDialDigits; /** The formatted phone number to display. */ @@ -201,6 +224,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public boolean lightbringerReady; private View.OnClickListener mExpandCollapseListener; + private final OnActionModeStateChangedListener onActionModeStateChangedListener; private final View.OnLongClickListener longPressListener; private boolean mVoicemailPrimaryActionButtonClicked; @@ -216,11 +240,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder OnClickListener blockReportListener, View.OnClickListener expandCollapseListener, View.OnLongClickListener longClickListener, + CallLogAdapter.OnActionModeStateChangedListener actionModeStateChangedListener, CallLogCache callLogCache, CallLogListItemHelper callLogListItemHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, View rootView, - QuickContactBadge quickContactView, + DialerQuickContactBadge dialerQuickContactView, View primaryActionView, PhoneCallDetailsViews phoneCallDetailsViews, CardView callLogEntryView, @@ -230,6 +255,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder mContext = context; mExpandCollapseListener = expandCollapseListener; + onActionModeStateChangedListener = actionModeStateChangedListener; longPressListener = longClickListener; mCallLogCache = callLogCache; mCallLogListItemHelper = callLogListItemHelper; @@ -237,8 +263,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder mBlockReportListener = blockReportListener; mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService(); + // Cache this to avoid having to look it up each time we bind to a call log entry + mDefaultPhoneAccountHandle = + TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); + this.rootView = rootView; - this.quickContactView = quickContactView; + this.quickContactView = dialerQuickContactView; this.primaryActionView = primaryActionView; this.phoneCallDetailsViews = phoneCallDetailsViews; this.callLogEntryView = callLogEntryView; @@ -251,6 +281,23 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder phoneCallDetailsViews.nameView.setElegantTextHeight(false); phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); + if (mContext instanceof CallLogActivity) { + hostUi = HostUi.CALL_HISTORY; + Logger.get(mContext) + .logQuickContactOnTouch( + quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_HISTORY, true); + } else if (mVoicemailPlaybackPresenter == null) { + hostUi = HostUi.CALL_LOG; + Logger.get(mContext) + .logQuickContactOnTouch( + quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_LOG, true); + } else { + hostUi = HostUi.VOICEMAIL; + Logger.get(mContext) + .logQuickContactOnTouch( + quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_VOICEMAIL, false); + } + quickContactView.setOverlay(null); if (CompatUtils.hasPrioritizedMimeType()) { quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); @@ -264,6 +311,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder CallLogAdapter.ENABLE_CALL_LOG_MULTI_SELECT_FLAG)) { primaryActionView.setOnLongClickListener(longPressListener); quickContactView.setOnLongClickListener(longPressListener); + quickContactView.setMulitSelectListeners( + mExpandCollapseListener, onActionModeStateChangedListener); } else { primaryActionView.setOnCreateContextMenuListener(this); } @@ -275,6 +324,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder OnClickListener blockReportListener, View.OnClickListener expandCollapseListener, View.OnLongClickListener longClickListener, + CallLogAdapter.OnActionModeStateChangedListener actionModeStateChangeListener, CallLogCache callLogCache, CallLogListItemHelper callLogListItemHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter) { @@ -284,11 +334,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder blockReportListener, expandCollapseListener, longClickListener, + actionModeStateChangeListener, callLogCache, callLogListItemHelper, voicemailPlaybackPresenter, view, - (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), + (DialerQuickContactBadge) 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), @@ -297,8 +348,15 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder } public static CallLogListItemViewHolder createForTest(Context context) { + return createForTest(context, null, null); + } + + public static CallLogListItemViewHolder createForTest( + Context context, + View.OnClickListener expandCollapseListener, + VoicemailPlaybackPresenter voicemailPlaybackPresenter) { Resources resources = context.getResources(); - CallLogCache callLogCache = CallLogCache.getCallLogCache(context); + CallLogCache callLogCache = new CallLogCache(context); PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(context, resources, callLogCache); @@ -306,13 +364,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder new CallLogListItemViewHolder( context, null, - null /* expandCollapseListener */, + expandCollapseListener /* expandCollapseListener */, + null, null, callLogCache, new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache), - null /* voicemailPlaybackPresenter */, - new View(context), - new QuickContactBadge(context), + voicemailPlaybackPresenter, + LayoutInflater.from(context).inflate(R.layout.call_log_list_item, null), + new DialerQuickContactBadge(context), new View(context), PhoneCallDetailsViews.createForTest(context), new CardView(context), @@ -456,6 +515,18 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder // Treat as normal list item; show call button, if possible. if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) { boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + + if (!isVoicemailNumber && showLightbringerPrimaryButton()) { + CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount(); + primaryActionButtonView.setTag(IntentProvider.getLightbringerIntentProvider(number)); + primaryActionButtonView.setContentDescription( + TextUtils.expandTemplate( + mContext.getString(R.string.description_video_call_action), validNameOrNumber)); + primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); + primaryActionButtonView.setVisibility(View.VISIBLE); + return; + } + if (isVoicemailNumber) { // Call to generic voicemail number, in case there are multiple accounts. primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider()); @@ -467,7 +538,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder primaryActionButtonView.setContentDescription( TextUtils.expandTemplate( mContext.getString(R.string.description_call_action), validNameOrNumber)); - primaryActionButtonView.setImageResource(R.drawable.quantum_ic_call_white_24); + primaryActionButtonView.setImageResource(R.drawable.quantum_ic_call_vd_theme_24); primaryActionButtonView.setVisibility(View.VISIBLE); } else { primaryActionButtonView.setTag(null); @@ -483,12 +554,15 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder private void bindActionButtons() { boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation); + // Hide the call buttons by default. We then set it to be visible when appropriate below. + // This saves us having to remember to set it to GONE in multiple places. + callButtonView.setVisibility(View.GONE); + videoCallButtonView.setVisibility(View.GONE); + if (isFullyUndialableVoicemail()) { // Sometimes the voicemail server will report the message is from some non phone number // source. If the number does not contains any dialable digit treat it as it is from a unknown // number, remove all action buttons but still show the voicemail playback layout. - callButtonView.setVisibility(View.GONE); - videoCallButtonView.setVisibility(View.GONE); detailsButtonView.setVisibility(View.GONE); createNewContactButtonView.setVisibility(View.GONE); addToExistingContactButtonView.setVisibility(View.GONE); @@ -513,34 +587,40 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return; } - if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { + TextView callTypeOrLocationView = + ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text)); + + if (canPlaceCallToNumber) { callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); + callTypeOrLocationView.setVisibility(View.GONE); + } + + if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { ((TextView) callButtonView.findViewById(R.id.call_action_text)) .setText( TextUtils.expandTemplate( mContext.getString(R.string.call_log_action_call), nameOrNumber == null ? "" : 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 (hasPlacedCarrierVideoCall() || canSupportCarrierVideoCall()) { + // We need to check if we are showing the Lightbringer primary button. If we are, then we should + // show the "Call" button here regardless of IMS availability. + if (showLightbringerPrimaryButton()) { + callButtonView.setVisibility(View.VISIBLE); + videoCallButtonView.setVisibility(View.GONE); + } else if (CallUtil.isVideoEnabled(mContext) + && (hasPlacedCarrierVideoCall() || canSupportCarrierVideoCall())) { videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); videoCallButtonView.setVisibility(View.VISIBLE); } else if (lightbringerReady) { videoCallButtonView.setTag(IntentProvider.getLightbringerIntentProvider(number)); videoCallButtonView.setVisibility(View.VISIBLE); - } else { - videoCallButtonView.setVisibility(View.GONE); } // For voicemail calls, show the voicemail playback layout; hide otherwise. @@ -567,8 +647,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder detailsButtonView.setVisibility(View.GONE); } else { detailsButtonView.setVisibility(View.VISIBLE); + boolean canReportCallerId = + mCachedNumberLookupService != null + && mCachedNumberLookupService.canReportAsInvalid(info.sourceType, info.objectId); detailsButtonView.setTag( - IntentProvider.getCallDetailIntentProvider(callDetailsEntries, buildContact())); + IntentProvider.getCallDetailIntentProvider( + callDetailsEntries, buildContact(), canReportCallerId)); } boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); @@ -616,6 +700,12 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return false; } + private boolean showLightbringerPrimaryButton() { + return accountHandle != null + && accountHandle.getComponentName().equals(getLightbringer().getPhoneAccountComponentName()) + && lightbringerReady; + } + private static boolean hasDialableChar(CharSequence number) { if (TextUtils.isEmpty(number)) { return false; @@ -635,12 +725,10 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder if (accountHandle == null) { return false; } - if (accountHandle - .getComponentName() - .equals(getLightbringer().getPhoneAccountComponentName(mContext))) { + if (mDefaultPhoneAccountHandle == null) { return false; } - return true; + return accountHandle.getComponentName().equals(mDefaultPhoneAccountHandle.getComponentName()); } private boolean canSupportCarrierVideoCall() { @@ -690,12 +778,23 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return; } - final TextView view = phoneCallDetailsViews.voicemailTranscriptionView; - if (!isExpanded || TextUtils.isEmpty(view.getText())) { - view.setVisibility(View.GONE); + View transcriptContainerView = phoneCallDetailsViews.transcriptionView; + TextView transcriptView = phoneCallDetailsViews.voicemailTranscriptionView; + TextView transcriptBrandingView = phoneCallDetailsViews.voicemailTranscriptionBrandingView; + if (TextUtils.isEmpty(transcriptView.getText())) { + Assert.checkArgument(TextUtils.isEmpty(transcriptBrandingView.getText())); + } + if (!isExpanded || TextUtils.isEmpty(transcriptView.getText())) { + transcriptContainerView.setVisibility(View.GONE); return; } - view.setVisibility(View.VISIBLE); + transcriptContainerView.setVisibility(View.VISIBLE); + transcriptView.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(transcriptBrandingView.getText())) { + phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.GONE); + } else { + phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.VISIBLE); + } } public void updatePhoto() { @@ -717,19 +816,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder getContactType()); } - private int getContactType() { - int contactType = ContactPhotoManager.TYPE_DEFAULT; - if (mCallLogCache.isVoicemailNumber(accountHandle, number)) { - contactType = ContactPhotoManager.TYPE_VOICEMAIL; - } else if (isSpam) { - contactType = ContactPhotoManager.TYPE_SPAM; - } else if (mCachedNumberLookupService != null - && mCachedNumberLookupService.isBusiness(info.sourceType)) { - contactType = ContactPhotoManager.TYPE_BUSINESS; - } else if (numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) { - contactType = ContactPhotoManager.TYPE_GENERIC_AVATAR; - } - return contactType; + private @ContactType int getContactType() { + return LetterTileDrawable.getContactTypeFromPrimitives( + mCallLogCache.isVoicemailNumber(accountHandle, number), + isSpam, + mCachedNumberLookupService != null + && mCachedNumberLookupService.isBusiness(info.sourceType), + numberPresentation, + false); } @Override @@ -789,25 +883,67 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder Activity activity = (Activity) mContext; activity.startActivityForResult( CallComposerActivity.newIntent(activity, buildContact()), - DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE); + ActivityRequestCodes.DIALTACTS_CALL_COMPOSER); } else if (view.getId() == R.id.share_voicemail) { Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED); mVoicemailPlaybackPresenter.shareVoicemail(); } else { logCallLogAction(view.getId()); + final IntentProvider intentProvider = (IntentProvider) view.getTag(); - if (intentProvider != null) { - final Intent intent = intentProvider.getIntent(mContext); - // See IntentProvider.getCallDetailIntentProvider() for why this may be null. - if (intent != null) { - DialerUtils.startActivityWithErrorToast(mContext, intent); + if (intentProvider == null) { + return; + } + + final Intent intent = intentProvider.getIntent(mContext); + // See IntentProvider.getCallDetailIntentProvider() for why this may be null. + if (intent == null) { + return; + } + + if (info != null && info.lookupKey != null) { + Bundle extras = new Bundle(); + if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) { + extras = intent.getParcelableExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); } + extras.putBoolean(TelephonyManagerCompat.ALLOW_ASSISTED_DIAL, true); + intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); } + + // We check to see if we are starting a Lightbringer intent. The reason is Lightbringer + // intents need to be started using startActivityForResult instead of the usual startActivity + String packageName = intent.getPackage(); + if (packageName != null && packageName.equals(getLightbringer().getPackageName())) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG); + startLightbringerActivity(intent); + } else if (CallDetailsActivity.isLaunchIntent(intent)) { + PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_DETAIL); + ((Activity) mContext) + .startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_CALL_DETAILS); + } else { + if (Intent.ACTION_CALL.equals(intent.getAction()) + && intent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, -1) + == VideoProfile.STATE_BIDIRECTIONAL) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_CALL_LOG); + } + DialerUtils.startActivityWithErrorToast(mContext, intent); + } + } + } + + private void startLightbringerActivity(Intent intent) { + try { + Activity activity = (Activity) mContext; + activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_LIGHTBRINGER); + } catch (ActivityNotFoundException e) { + Toast.makeText(mContext, R.string.activity_not_available, Toast.LENGTH_SHORT).show(); } } - private CallComposerContact buildContact() { - CallComposerContact.Builder contact = CallComposerContact.newBuilder(); + private DialerContact buildContact() { + DialerContact.Builder contact = DialerContact.newBuilder(); contact.setPhotoId(info.photoId); if (info.photoUri != null) { contact.setPhotoUri(info.photoUri.toString()); @@ -819,13 +955,23 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder contact.setNameOrNumber((String) nameOrNumber); } contact.setContactType(getContactType()); - contact.setNumber(number); + if (number != null) { + contact.setNumber(number); + } /* second line of contact view. */ if (!TextUtils.isEmpty(info.name)) { contact.setDisplayNumber(displayNumber); } /* phone number type (e.g. mobile) in second line of contact view */ contact.setNumberLabel(numberType); + + /* third line of contact view. */ + String accountLabel = mCallLogCache.getAccountLabel(accountHandle); + if (!TextUtils.isEmpty(accountLabel)) { + SimDetails.Builder simDetails = SimDetails.newBuilder().setNetwork(accountLabel); + simDetails.setColor(mCallLogCache.getAccountColor(accountHandle)); + contact.setSimDetails(simDetails.build()); + } return contact.build(); } @@ -834,8 +980,38 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE); } else if (id == R.id.add_to_existing_contact_action) { Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT); + switch (hostUi) { + case HostUi.CALL_HISTORY: + Logger.get(mContext) + .logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_HISTORY); + break; + case HostUi.CALL_LOG: + Logger.get(mContext).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_LOG); + break; + case HostUi.VOICEMAIL: + Logger.get(mContext).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_VOICEMAIL); + break; + default: + throw Assert.createIllegalStateFailException(); + } } else if (id == R.id.create_new_contact_action) { Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT); + switch (hostUi) { + case HostUi.CALL_HISTORY: + Logger.get(mContext) + .logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_HISTORY); + break; + case HostUi.CALL_LOG: + Logger.get(mContext) + .logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_LOG); + break; + case HostUi.VOICEMAIL: + Logger.get(mContext) + .logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_VOICEMAIL); + break; + default: + throw Assert.createIllegalStateFailException(); + } } } @@ -987,6 +1163,15 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext); } + /** Specifies where the view holder belongs. */ + @IntDef({HostUi.CALL_LOG, HostUi.CALL_HISTORY, HostUi.VOICEMAIL}) + @Retention(RetentionPolicy.SOURCE) + private @interface HostUi { + int CALL_LOG = 0; + int CALL_HISTORY = 1; + int VOICEMAIL = 2; + } + public interface OnClickListener { void onBlockReportSpam( diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java index e169b8de9..43e03e9fd 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java @@ -18,7 +18,6 @@ package com.android.dialer.app.calllog; import android.Manifest; import android.annotation.TargetApi; -import android.app.NotificationManager; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -27,6 +26,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build.VERSION_CODES; import android.provider.CallLog.Calls; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.support.v4.os.UserManagerCompat; @@ -36,7 +36,6 @@ import com.android.dialer.app.R; import com.android.dialer.calllogutils.PhoneNumberDisplayUtil; import com.android.dialer.common.LogUtil; import com.android.dialer.location.GeoUtil; -import com.android.dialer.notification.GroupedNotificationUtil; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.ContactInfoHelper; import com.android.dialer.util.PermissionsUtil; @@ -46,7 +45,6 @@ import java.util.List; /** Helper class operating on call log notifications. */ public class CallLogNotificationsQueryHelper { - private static final String TAG = "CallLogNotifHelper"; private final Context mContext; private final NewCallsQuery mNewCallsQuery; private final ContactInfoHelper mContactInfoHelper; @@ -74,44 +72,58 @@ public class CallLogNotificationsQueryHelper { countryIso); } + public static void markAllMissedCallsInCallLogAsRead(@NonNull Context context) { + markMissedCallsInCallLogAsRead(context, null); + } + + public static void markSingleMissedCallInCallLogAsRead( + @NonNull Context context, @Nullable Uri callUri) { + if (callUri == null) { + LogUtil.e( + "CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead", + "call URI is null, unable to mark call as read"); + } else { + markMissedCallsInCallLogAsRead(context, callUri); + } + } + /** - * Removes the missed call notifications and marks calls as read. If a callUri is provided, only - * that call is marked as read. + * If callUri is null then calls with a matching callUri are marked as read, otherwise all calls + * are marked as read. */ @WorkerThread - public static void removeMissedCallNotifications(Context context, @Nullable Uri callUri) { - // 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(context) && PermissionsUtil.hasPhonePermissions(context)) { - 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 { - context - .getContentResolver() - .update( - callUri == null ? Calls.CONTENT_URI : callUri, - values, - where.toString(), - new String[] {Integer.toString(Calls.MISSED_TYPE)}); - } catch (IllegalArgumentException e) { - LogUtil.e( - "CallLogNotificationsQueryHelper.removeMissedCallNotifications", - "contacts provider update command failed", - e); - } + private static void markMissedCallsInCallLogAsRead(Context context, @Nullable Uri callUri) { + if (!UserManagerCompat.isUserUnlocked(context)) { + LogUtil.e("CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead", "locked"); + return; + } + if (!PermissionsUtil.hasPhonePermissions(context)) { + LogUtil.e("CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead", "no permission"); + return; } - GroupedNotificationUtil.removeNotification( - context.getSystemService(NotificationManager.class), - callUri != null ? callUri.toString() : null, - R.id.notification_missed_call, - MissedCallNotifier.NOTIFICATION_TAG); + 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 { + context + .getContentResolver() + .update( + callUri == null ? Calls.CONTENT_URI : callUri, + values, + where.toString(), + new String[] {Integer.toString(Calls.MISSED_TYPE)}); + } catch (IllegalArgumentException e) { + LogUtil.e( + "CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead", + "contacts provider update command failed", + e); + } } /** Create a new instance of {@link NewCallsQuery}. */ @@ -281,7 +293,9 @@ public class CallLogNotificationsQueryHelper { @TargetApi(VERSION_CODES.M) public List<NewCall> query(int type) { if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { - LogUtil.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); + LogUtil.w( + "CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query", + "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); @@ -302,7 +316,9 @@ public class CallLogNotificationsQueryHelper { } return newCalls; } catch (RuntimeException e) { - LogUtil.w(TAG, "Exception when querying Contacts Provider for calls lookup"); + LogUtil.w( + "CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query", + "exception when querying Contacts Provider for calls lookup"); return null; } } diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java index 7dfd2cb69..0490b9932 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -17,13 +17,20 @@ package com.android.dialer.app.calllog; import android.app.IntentService; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Handler; -import android.os.Looper; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.app.voicemail.LegacyVoicemailNotificationReceiver; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; @@ -43,28 +50,37 @@ import com.android.dialer.util.PermissionsUtil; */ public class CallLogNotificationsService extends IntentService { - /** Action to mark all the new voicemails as old. */ - public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD = - "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD"; + @VisibleForTesting + static final String ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD"; - /** 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"; + private static final String ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD "; - /** Action to update missed call notifications with a post call note. */ - public static final String ACTION_INCOMING_POST_CALL = + @VisibleForTesting + static final String ACTION_CANCEL_ALL_MISSED_CALLS = + "com.android.dialer.calllog.ACTION_CANCEL_ALL_MISSED_CALLS"; + + private static final String ACTION_CANCEL_SINGLE_MISSED_CALL = + "com.android.dialer.calllog.ACTION_CANCEL_SINGLE_MISSED_CALL"; + + private static final String ACTION_INCOMING_POST_CALL = "com.android.dialer.calllog.INCOMING_POST_CALL"; /** 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"; + /** Action mark legacy voicemail as dismissed. */ + public static final String ACTION_LEGACY_VOICEMAIL_DISMISSED = + "com.android.dialer.calllog.ACTION_LEGACY_VOICEMAIL_DISMISSED"; + /** * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent a post call note. * * <p>It must be a {@link String} */ - public static final String EXTRA_POST_CALL_NOTE = "POST_CALL_NOTE"; + private static final String EXTRA_POST_CALL_NOTE = "POST_CALL_NOTE"; /** * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent the phone number the @@ -72,10 +88,11 @@ public class CallLogNotificationsService extends IntentService { * * <p>It must be a {@link String} */ - public static final String EXTRA_POST_CALL_NUMBER = "POST_CALL_NUMBER"; + private static final String EXTRA_POST_CALL_NUMBER = "POST_CALL_NUMBER"; + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE"; public static final int UNKNOWN_MISSED_CALL_COUNT = -1; - private VoicemailQueryHandler mVoicemailQueryHandler; public CallLogNotificationsService() { super("CallLogNotificationsService"); @@ -89,52 +106,107 @@ public class CallLogNotificationsService extends IntentService { context.startService(serviceIntent); } - public static void markNewVoicemailsAsOld(Context context, @Nullable Uri voicemailUri) { + public static void markAllNewVoicemailsAsOld(Context context) { + LogUtil.enterBlock("CallLogNotificationsService.markAllNewVoicemailsAsOld"); Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); - serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); - serviceIntent.setData(voicemailUri); + serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD); context.startService(serviceIntent); } - public static void markNewMissedCallsAsOld(Context context, @Nullable Uri callUri) { + public static void markSingleNewVoicemailAsOld(Context context, @Nullable Uri voicemailUri) { + LogUtil.enterBlock("CallLogNotificationsService.markSingleNewVoicemailAsOld"); Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); - serviceIntent.setAction(ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); - serviceIntent.setData(callUri); + serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD); + serviceIntent.setData(voicemailUri); context.startService(serviceIntent); } + public static void cancelAllMissedCalls(Context context) { + LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCalls"); + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new CancelAllMissedCallsWorker()) + .build() + .executeSerial(context); + } + + public static PendingIntent createMarkAllNewVoicemailsAsOldIntent(@NonNull Context context) { + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD); + return PendingIntent.getService(context, 0, intent, 0); + } + + public static PendingIntent createMarkSingleNewVoicemailAsOldIntent( + @NonNull Context context, @Nullable Uri voicemailUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD); + intent.setData(voicemailUri); + return PendingIntent.getService(context, 0, intent, 0); + } + + public static PendingIntent createCancelAllMissedCallsPendingIntent(@NonNull Context context) { + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(ACTION_CANCEL_ALL_MISSED_CALLS); + return PendingIntent.getService(context, 0, intent, 0); + } + + public static PendingIntent createCancelSingleMissedCallPendingIntent( + @NonNull Context context, @Nullable Uri callUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(ACTION_CANCEL_SINGLE_MISSED_CALL); + intent.setData(callUri); + return PendingIntent.getService(context, 0, intent, 0); + } + + public static PendingIntent createLegacyVoicemailDismissedPendingIntent( + @NonNull Context context, PhoneAccountHandle phoneAccountHandle) { + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(ACTION_LEGACY_VOICEMAIL_DISMISSED); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + return PendingIntent.getService(context, 0, intent, 0); + } + @Override protected void onHandleIntent(Intent intent) { if (intent == null) { - LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle null intent"); + LogUtil.e("CallLogNotificationsService.onHandleIntent", "could not handle null intent"); return; } - if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG)) { + if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG) + || !PermissionsUtil.hasPermission(this, android.Manifest.permission.WRITE_CALL_LOG)) { + LogUtil.e("CallLogNotificationsService.onHandleIntent", "no READ_CALL_LOG permission"); return; } String action = intent.getAction(); + LogUtil.i("CallLogNotificationsService.onHandleIntent", "action: " + action); switch (action) { - case ACTION_MARK_NEW_VOICEMAILS_AS_OLD: - // VoicemailQueryHandler cannot be created on the IntentService worker thread. The completed - // callback might happen when the thread is dead. - Handler handler = new Handler(Looper.getMainLooper()); - handler.post( - () -> { - if (mVoicemailQueryHandler == null) { - mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver()); - } - mVoicemailQueryHandler.markNewVoicemailsAsOld(intent.getData()); - }); + case ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD: + VoicemailQueryHandler.markAllNewVoicemailsAsRead(this); + VisualVoicemailNotifier.cancelAllVoicemailNotifications(this); + break; + case ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD: + Uri voicemailUri = intent.getData(); + VoicemailQueryHandler.markSingleNewVoicemailAsRead(this, voicemailUri); + VisualVoicemailNotifier.cancelSingleVoicemailNotification(this, voicemailUri); + break; + case ACTION_LEGACY_VOICEMAIL_DISMISSED: + LegacyVoicemailNotificationReceiver.setDismissed( + this, intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE), true); break; case ACTION_INCOMING_POST_CALL: String note = intent.getStringExtra(EXTRA_POST_CALL_NOTE); String phoneNumber = intent.getStringExtra(EXTRA_POST_CALL_NUMBER); MissedCallNotifier.getIstance(this).insertPostCallNotification(phoneNumber, note); break; - case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD: - CallLogNotificationsQueryHelper.removeMissedCallNotifications(this, intent.getData()); + case ACTION_CANCEL_ALL_MISSED_CALLS: + cancelAllMissedCalls(this); + break; + case ACTION_CANCEL_SINGLE_MISSED_CALL: + Uri callUri = intent.getData(); + CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(this, callUri); + MissedCallNotifier.cancelSingleMissedCallNotification(this, callUri); TelecomUtil.cancelMissedCallsNotification(this); break; case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: @@ -145,8 +217,30 @@ public class CallLogNotificationsService extends IntentService { intent.getData()); break; default: - LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent); + LogUtil.e("CallLogNotificationsService.onHandleIntent", "no handler for action: " + action); break; } } + + @WorkerThread + private static void cancelAllMissedCallsBackground(Context context) { + LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCallsBackground"); + Assert.isWorkerThread(); + CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context); + MissedCallNotifier.cancelAllMissedCallNotifications(context); + TelecomUtil.cancelMissedCallsNotification(context); + } + + /** Worker that cancels all missed call notifications and updates call log entries. */ + private static class CancelAllMissedCallsWorker implements Worker<Context, Void> { + + @Nullable + @Override + public Void doInBackground(@Nullable Context context) throws Throwable { + if (context != null) { + cancelAllMissedCallsBackground(context); + } + return null; + } + } } diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java index 172d00100..ce3132d12 100644 --- a/java/com/android/dialer/app/calllog/CallLogReceiver.java +++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java @@ -39,10 +39,10 @@ public class CallLogReceiver extends BroadcastReceiver { if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) { checkVoicemailStatus(context); PendingResult pendingResult = goAsync(); - DefaultVoicemailNotifier.updateVoicemailNotifications(context, pendingResult::finish); + VisualVoicemailUpdateTask.scheduleTask(context, pendingResult::finish); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { PendingResult pendingResult = goAsync(); - DefaultVoicemailNotifier.updateVoicemailNotifications(context, pendingResult::finish); + VisualVoicemailUpdateTask.scheduleTask(context, pendingResult::finish); } else { LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent); } diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java index a01b89527..b16eb1beb 100644 --- a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java +++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java @@ -22,20 +22,27 @@ import android.app.Dialog; import android.app.DialogFragment; import android.app.FragmentManager; import android.app.ProgressDialog; -import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; -import android.os.AsyncTask; import android.os.Bundle; import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; import com.android.dialer.app.R; +import com.android.dialer.common.Assert; +import com.android.dialer.common.concurrent.DialerExecutor; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.phonenumbercache.CachedNumberLookupService; import com.android.dialer.phonenumbercache.PhoneNumberCache; /** Dialog that clears the call log after confirming with the user */ public class ClearCallLogDialog extends DialogFragment { + private DialerExecutor<Void> clearCallLogTask; + private ProgressDialog progressDialog; + /** Preferred way to show this dialog */ public static void show(FragmentManager fragmentManager) { ClearCallLogDialog dialog = new ClearCallLogDialog(); @@ -43,49 +50,35 @@ public class ClearCallLogDialog extends DialogFragment { } @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + clearCallLogTask = + DialerExecutorComponent.get(getContext()) + .dialerExecutorFactory() + .createUiTaskBuilder( + getFragmentManager(), + "clearCallLogTask", + new ClearCallLogWorker(getActivity().getApplicationContext())) + .onSuccess(this::onSuccess) + .build(); + } + + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final ContentResolver resolver = getActivity().getContentResolver(); - final Context context = getActivity().getApplicationContext(); - final OnClickListener okListener = - new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final ProgressDialog progressDialog = - ProgressDialog.show( - getActivity(), getString(R.string.clearCallLogProgress_title), "", true, false); - progressDialog.setOwnerActivity(getActivity()); - CallLogNotificationsService.markNewMissedCallsAsOld(getContext(), null); - final AsyncTask<Void, Void, Void> task = - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - resolver.delete(Calls.CONTENT_URI, null, null); - CachedNumberLookupService cachedNumberLookupService = - PhoneNumberCache.get(context).getCachedNumberLookupService(); - if (cachedNumberLookupService != null) { - cachedNumberLookupService.clearAllCacheEntries(context); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - final Activity activity = progressDialog.getOwnerActivity(); - - if (activity == null || activity.isDestroyed() || activity.isFinishing()) { - return; - } - - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } - }; - // TODO: Once we have the API, we should configure this ProgressDialog - // to only show up after a certain time (e.g. 150ms) - progressDialog.show(); - task.execute(); - } + OnClickListener okListener = + (dialog, which) -> { + progressDialog = + ProgressDialog.show( + getActivity(), getString(R.string.clearCallLogProgress_title), "", true, false); + progressDialog.setOwnerActivity(getActivity()); + CallLogNotificationsService.cancelAllMissedCalls(getContext()); + + // TODO: Once we have the API, we should configure this ProgressDialog + // to only show up after a certain time (e.g. 150ms) + progressDialog.show(); + + clearCallLogTask.executeSerial(null); }; return new AlertDialog.Builder(getActivity()) .setTitle(R.string.clearCallLogConfirmation_title) @@ -96,4 +89,49 @@ public class ClearCallLogDialog extends DialogFragment { .setCancelable(true) .create(); } + + private static class ClearCallLogWorker implements Worker<Void, Void> { + private final Context appContext; + + private ClearCallLogWorker(Context appContext) { + this.appContext = appContext; + } + + @Nullable + @Override + public Void doInBackground(@Nullable Void unused) throws Throwable { + appContext.getContentResolver().delete(Calls.CONTENT_URI, null, null); + CachedNumberLookupService cachedNumberLookupService = + PhoneNumberCache.get(appContext).getCachedNumberLookupService(); + if (cachedNumberLookupService != null) { + cachedNumberLookupService.clearAllCacheEntries(appContext); + } + return null; + } + } + + private void onSuccess(Void unused) { + Assert.isNotNull(progressDialog); + Activity activity = progressDialog.getOwnerActivity(); + + if (activity == null || activity.isDestroyed() || activity.isFinishing()) { + return; + } + + maybeShowEnrichedCallSnackbar(activity); + + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + + private void maybeShowEnrichedCallSnackbar(Activity activity) { + if (EnrichedCallComponent.get(activity).getEnrichedCallManager().hasStoredData()) { + Snackbar.make( + activity.findViewById(R.id.calllog_frame), + getString(R.string.multiple_ec_data_deleted), + 5_000) + .show(); + } + } } diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java deleted file mode 100644 index 58fe6fa2c..000000000 --- a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.dialer.app.calllog; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.PersistableBundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.support.v4.os.BuildCompat; -import android.support.v4.util.Pair; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telephony.CarrierConfigManager; -import android.telephony.PhoneNumberUtils; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.util.ArrayMap; -import com.android.contacts.common.compat.TelephonyManagerCompat; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.app.DialtactsActivity; -import com.android.dialer.app.R; -import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; -import com.android.dialer.app.contactinfo.ContactPhotoLoader; -import com.android.dialer.app.list.DialtactsPagerAdapter; -import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; -import com.android.dialer.blocking.FilteredNumbersUtil; -import com.android.dialer.calllogutils.PhoneAccountUtils; -import com.android.dialer.common.Assert; -import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.DialerExecutor.Worker; -import com.android.dialer.common.concurrent.DialerExecutors; -import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; -import com.android.dialer.notification.NotificationChannelManager; -import com.android.dialer.notification.NotificationChannelManager.Channel; -import com.android.dialer.phonenumbercache.ContactInfo; -import com.android.dialer.telecom.TelecomUtil; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** Shows a voicemail notification in the status bar. */ -public class DefaultVoicemailNotifier implements Worker<Void, Void> { - - public static final String TAG = "VoicemailNotifier"; - - /** The tag used to identify notifications from this class. */ - static final String VISUAL_VOICEMAIL_NOTIFICATION_TAG = "DefaultVoicemailNotifier"; - /** The identifier of the notification of new voicemails. */ - private static final int VISUAL_VOICEMAIL_NOTIFICATION_ID = R.id.notification_visual_voicemail; - - private static final int LEGACY_VOICEMAIL_NOTIFICATION_ID = R.id.notification_legacy_voicemail; - private static final String LEGACY_VOICEMAIL_NOTIFICATION_TAG = "legacy_voicemail"; - - private final Context context; - private final CallLogNotificationsQueryHelper queryHelper; - private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler; - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - DefaultVoicemailNotifier( - Context context, - CallLogNotificationsQueryHelper queryHelper, - FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { - this.context = context; - this.queryHelper = queryHelper; - this.filteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; - } - - public DefaultVoicemailNotifier(Context context) { - this( - context, - CallLogNotificationsQueryHelper.getInstance(context), - new FilteredNumberAsyncQueryHandler(context)); - } - - @Nullable - @Override - public Void doInBackground(@Nullable Void input) throws Throwable { - updateNotification(); - return null; - } - - /** - * Updates the notification and notifies of the call with the given URI. - * - * <p>Clears the notification if there are no new voicemails, and notifies if the given URI - * corresponds to a new voicemail. - * - * <p>It is not safe to call this method from the main thread. - */ - @VisibleForTesting - @WorkerThread - void updateNotification() { - Assert.isWorkerThread(); - // Lookup the list of new voicemails to include in the notification. - final List<NewCall> newCalls = queryHelper.getNewVoicemails(); - - if (newCalls == null) { - // Query failed, just return. - return; - } - - Resources resources = context.getResources(); - - // This represents a list of names to include in the notification. - String callers = null; - - // Maps each number into a name: if a number is in the map, it has already left a more - // recent voicemail. - final Map<String, ContactInfo> contactInfos = new ArrayMap<>(); - - // Iterate over the new voicemails to determine all the information above. - Iterator<NewCall> itr = newCalls.iterator(); - while (itr.hasNext()) { - NewCall newCall = itr.next(); - - // Skip notifying for numbers which are blocked. - if (!FilteredNumbersUtil.hasRecentEmergencyCall(context) - && filteredNumberAsyncQueryHandler.getBlockedIdSynchronous( - newCall.number, newCall.countryIso) - != null) { - itr.remove(); - - if (newCall.voicemailUri != null) { - // Delete the voicemail. - CallLogAsyncTaskUtil.deleteVoicemailSynchronous(context, newCall.voicemailUri); - } - continue; - } - - // Check if we already know the name associated with this number. - ContactInfo contactInfo = contactInfos.get(newCall.number); - if (contactInfo == null) { - contactInfo = - queryHelper.getContactInfo( - newCall.number, newCall.numberPresentation, newCall.countryIso); - contactInfos.put(newCall.number, contactInfo); - // This is a new caller. Add it to the back of the list of callers. - if (TextUtils.isEmpty(callers)) { - callers = contactInfo.name; - } else { - callers = - resources.getString( - R.string.notification_voicemail_callers_list, callers, contactInfo.name); - } - } - } - - if (newCalls.isEmpty()) { - // No voicemails to notify about - return; - } - - Notification.Builder groupSummary = - createNotificationBuilder() - .setContentTitle( - resources.getQuantityString( - R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size())) - .setContentText(callers) - .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(null)) - .setGroupSummary(true) - .setContentIntent(newVoicemailIntent(null)); - - if (BuildCompat.isAtLeastO()) { - groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN); - } - - NotificationChannelManager.applyChannel( - groupSummary, - context, - Channel.VOICEMAIL, - PhoneAccountHandles.getAccount(context, newCalls.get(0))); - - LogUtil.i(TAG, "Creating visual voicemail notification"); - getNotificationManager() - .notify( - VISUAL_VOICEMAIL_NOTIFICATION_TAG, - VISUAL_VOICEMAIL_NOTIFICATION_ID, - groupSummary.build()); - - for (NewCall voicemail : newCalls) { - getNotificationManager() - .notify( - voicemail.callsUri.toString(), - VISUAL_VOICEMAIL_NOTIFICATION_ID, - createNotificationForVoicemail(voicemail, contactInfos)); - } - } - - /** - * Replicates how packages/services/Telephony/NotificationMgr.java handles legacy voicemail - * notification. The notification will not be stackable because no information is available for - * individual voicemails. - */ - @TargetApi(VERSION_CODES.O) - public void notifyLegacyVoicemail( - @NonNull PhoneAccountHandle phoneAccountHandle, - int count, - String voicemailNumber, - PendingIntent callVoicemailIntent, - PendingIntent voicemailSettingIntent) { - Assert.isNotNull(phoneAccountHandle); - Assert.checkArgument(BuildCompat.isAtLeastO()); - TelephonyManager telephonyManager = - context - .getSystemService(TelephonyManager.class) - .createForPhoneAccountHandle(phoneAccountHandle); - Assert.isNotNull(telephonyManager); - LogUtil.i(TAG, "Creating legacy voicemail notification"); - - PersistableBundle carrierConfig = telephonyManager.getCarrierConfig(); - - String notificationTitle = - context - .getResources() - .getQuantityString(R.plurals.notification_voicemail_title, count, count); - - TelecomManager telecomManager = context.getSystemService(TelecomManager.class); - PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); - - String notificationText; - PendingIntent pendingIntent; - - if (voicemailSettingIntent != null) { - // If the voicemail number if unknown, instead of calling voicemail, take the user - // to the voicemail settings. - notificationText = context.getString(R.string.notification_voicemail_no_vm_number); - pendingIntent = voicemailSettingIntent; - } else { - if (PhoneAccountUtils.getSubscriptionPhoneAccounts(context).size() > 1) { - notificationText = phoneAccount.getShortDescription().toString(); - } else { - notificationText = - String.format( - context.getString(R.string.notification_voicemail_text_format), - PhoneNumberUtils.formatNumber(voicemailNumber)); - } - pendingIntent = callVoicemailIntent; - } - Notification.Builder builder = new Notification.Builder(context); - builder - .setSmallIcon(android.R.drawable.stat_notify_voicemail) - .setColor(context.getColor(R.color.dialer_theme_color)) - .setWhen(System.currentTimeMillis()) - .setContentTitle(notificationTitle) - .setContentText(notificationText) - .setContentIntent(pendingIntent) - .setSound(telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle)) - .setOngoing( - carrierConfig.getBoolean( - CarrierConfigManager.KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL)); - - if (telephonyManager.isVoicemailVibrationEnabled(phoneAccountHandle)) { - builder.setDefaults(Notification.DEFAULT_VIBRATE); - } - - NotificationChannelManager.applyChannel( - builder, context, Channel.VOICEMAIL, phoneAccountHandle); - Notification notification = builder.build(); - getNotificationManager() - .notify(LEGACY_VOICEMAIL_NOTIFICATION_TAG, LEGACY_VOICEMAIL_NOTIFICATION_ID, notification); - } - - public void cancelLegacyNotification() { - LogUtil.i(TAG, "Clearing legacy voicemail notification"); - getNotificationManager() - .cancel(LEGACY_VOICEMAIL_NOTIFICATION_TAG, LEGACY_VOICEMAIL_NOTIFICATION_ID); - } - - /** - * Determines which ringtone Uri and Notification defaults to use when updating the notification - * for the given call. - */ - private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) { - LogUtil.v(TAG, "getNotificationInfo"); - if (callToNotify == null) { - LogUtil.i(TAG, "callToNotify == null"); - return new Pair<>(null, 0); - } - PhoneAccountHandle accountHandle = PhoneAccountHandles.getAccount(context, callToNotify); - if (accountHandle == null) { - LogUtil.i(TAG, "No default phone account found, using default notification ringtone"); - return new Pair<>(null, Notification.DEFAULT_ALL); - } - return new Pair<>( - TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle), - getNotificationDefaults(accountHandle)); - } - - private int getNotificationDefaults(PhoneAccountHandle accountHandle) { - if (VERSION.SDK_INT >= VERSION_CODES.N) { - return TelephonyManagerCompat.isVoicemailVibrationEnabled( - getTelephonyManager(), accountHandle) - ? Notification.DEFAULT_VIBRATE - : 0; - } - return Notification.DEFAULT_ALL; - } - - /** Creates a pending intent that marks all new voicemails as old. */ - private PendingIntent createMarkNewVoicemailsAsOldIntent(@Nullable Uri voicemailUri) { - Intent intent = new Intent(context, CallLogNotificationsService.class); - intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); - intent.setData(voicemailUri); - return PendingIntent.getService(context, 0, intent, 0); - } - - private NotificationManager getNotificationManager() { - return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - } - - private TelephonyManager getTelephonyManager() { - return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - } - - private Notification createNotificationForVoicemail( - @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos) { - Pair<Uri, Integer> notificationInfo = getNotificationInfo(voicemail); - ContactInfo contactInfo = contactInfos.get(voicemail.number); - - Notification.Builder notificationBuilder = - createNotificationBuilder() - .setContentTitle( - context - .getResources() - .getQuantityString(R.plurals.notification_voicemail_title, 1, 1)) - .setContentText( - ContactDisplayUtils.getTtsSpannedPhoneNumber( - context.getResources(), - R.string.notification_new_voicemail_ticker, - contactInfo.name)) - .setWhen(voicemail.dateMs) - .setSound(notificationInfo.first) - .setDefaults(notificationInfo.second); - - if (voicemail.voicemailUri != null) { - notificationBuilder.setDeleteIntent( - createMarkNewVoicemailsAsOldIntent(voicemail.voicemailUri)); - } - - NotificationChannelManager.applyChannel( - notificationBuilder, - context, - Channel.VOICEMAIL, - PhoneAccountHandles.getAccount(context, voicemail)); - - ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); - Bitmap photoIcon = loader.loadPhotoIcon(); - if (photoIcon != null) { - notificationBuilder.setLargeIcon(photoIcon); - } - if (!TextUtils.isEmpty(voicemail.transcription)) { - Logger.get(context) - .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION); - notificationBuilder.setStyle( - new Notification.BigTextStyle().bigText(voicemail.transcription)); - } - notificationBuilder.setContentIntent(newVoicemailIntent(voicemail)); - Logger.get(context).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED); - return notificationBuilder.build(); - } - - private Notification.Builder createNotificationBuilder() { - return new Notification.Builder(context) - .setSmallIcon(android.R.drawable.stat_notify_voicemail) - .setColor(context.getColor(R.color.dialer_theme_color)) - .setGroup(VISUAL_VOICEMAIL_NOTIFICATION_TAG) - .setOnlyAlertOnce(true) - .setAutoCancel(true); - } - - private PendingIntent newVoicemailIntent(@Nullable NewCall voicemail) { - Intent intent = - DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); - // TODO (b/35486204): scroll to this voicemail - if (voicemail != null) { - intent.setData(voicemail.voicemailUri); - } - intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - /** - * Updates the voicemail notifications displayed. - * - * @param runnable Called when the async update task completes no matter if it succeeds or fails. - * May be null. - */ - static void updateVoicemailNotifications(Context context, Runnable runnable) { - if (!TelecomUtil.isDefaultDialer(context)) { - LogUtil.i( - "DefaultVoicemailNotifier.updateVoicemailNotifications", - "not default dialer, not scheduling update to voicemail notifications"); - return; - } - - DialerExecutors.createNonUiTaskBuilder(new DefaultVoicemailNotifier(context)) - .onSuccess( - output -> { - LogUtil.i( - "DefaultVoicemailNotifier.updateVoicemailNotifications", - "update voicemail notifications successful"); - if (runnable != null) { - runnable.run(); - } - }) - .onFailure( - throwable -> { - LogUtil.i( - "DefaultVoicemailNotifier.updateVoicemailNotifications", - "update voicemail notifications failed"); - if (runnable != null) { - runnable.run(); - } - }) - .build() - .executeParallel(null); - } -} diff --git a/java/com/android/dialer/app/calllog/DialerQuickContactBadge.java b/java/com/android/dialer/app/calllog/DialerQuickContactBadge.java new file mode 100644 index 000000000..a3aac41fa --- /dev/null +++ b/java/com/android/dialer/app/calllog/DialerQuickContactBadge.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 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.app.calllog; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.QuickContactBadge; +import com.android.dialer.app.calllog.CallLogAdapter.OnActionModeStateChangedListener; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; + +/** Allows us to click the contact badge for non multi select mode. */ +class DialerQuickContactBadge extends QuickContactBadge { + + private View.OnClickListener mExtraOnClickListener; + private OnActionModeStateChangedListener onActionModeStateChangeListener; + + public DialerQuickContactBadge(Context context) { + super(context); + } + + public DialerQuickContactBadge(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialerQuickContactBadge(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onClick(View v) { + if (mExtraOnClickListener != null + && onActionModeStateChangeListener.isActionModeStateEnabled()) { + Logger.get(v.getContext()) + .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_TAP_VIA_CONTACT_BADGE); + mExtraOnClickListener.onClick(v); + } else { + super.onClick(v); + } + } + + public void setMulitSelectListeners( + View.OnClickListener extraOnClickListener, + OnActionModeStateChangedListener actionModeStateChangeListener) { + mExtraOnClickListener = extraOnClickListener; + onActionModeStateChangeListener = actionModeStateChangeListener; + } +} diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java index a94c6781e..55fdbbace 100644 --- a/java/com/android/dialer/app/calllog/IntentProvider.java +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -24,11 +24,11 @@ import android.provider.ContactsContract; import android.telecom.PhoneAccountHandle; import com.android.contacts.common.model.Contact; import com.android.contacts.common.model.ContactLoader; -import com.android.dialer.callcomposer.CallComposerContact; import com.android.dialer.calldetails.CallDetailsActivity; import com.android.dialer.calldetails.CallDetailsEntries; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.lightbringer.LightbringerComponent; import com.android.dialer.util.CallUtil; import com.android.dialer.util.IntentUtil; @@ -112,11 +112,12 @@ public abstract class IntentProvider { * @return The call details intent provider. */ public static IntentProvider getCallDetailIntentProvider( - CallDetailsEntries callDetailsEntries, CallComposerContact contact) { + CallDetailsEntries callDetailsEntries, DialerContact contact, boolean canReportCallerId) { return new IntentProvider() { @Override public Intent getIntent(Context context) { - return CallDetailsActivity.newInstance(context, callDetailsEntries, contact); + return CallDetailsActivity.newInstance( + context, callDetailsEntries, contact, canReportCallerId); } }; } diff --git a/java/com/android/dialer/app/calllog/LegacyVoicemailNotifier.java b/java/com/android/dialer/app/calllog/LegacyVoicemailNotifier.java new file mode 100644 index 000000000..584f07fe3 --- /dev/null +++ b/java/com/android/dialer/app/calllog/LegacyVoicemailNotifier.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 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.app.calllog; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.support.v4.os.BuildCompat; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.CarrierConfigManager; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.app.R; +import com.android.dialer.calllogutils.PhoneAccountUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.DialerNotificationManager; +import com.android.dialer.notification.NotificationChannelManager; + +/** Shows a notification in the status bar for legacy vociemail. */ +@TargetApi(VERSION_CODES.O) +public final class LegacyVoicemailNotifier { + private static final String NOTIFICATION_TAG = "LegacyVoicemail"; + private static final int NOTIFICATION_ID = 1; + + /** + * Replicates how packages/services/Telephony/NotificationMgr.java handles legacy voicemail + * notification. The notification will not be stackable because no information is available for + * individual voicemails. + */ + public static void showNotification( + @NonNull Context context, + @NonNull PhoneAccountHandle handle, + int count, + String voicemailNumber, + PendingIntent callVoicemailIntent, + PendingIntent voicemailSettingsIntent, + boolean isRefresh) { + LogUtil.enterBlock("LegacyVoicemailNotifier.showNotification"); + Assert.isNotNull(handle); + Assert.checkArgument(BuildCompat.isAtLeastO()); + + TelephonyManager pinnedTelephonyManager = + context.getSystemService(TelephonyManager.class).createForPhoneAccountHandle(handle); + if (pinnedTelephonyManager == null) { + LogUtil.e("LegacyVoicemailNotifier.showNotification", "invalid PhoneAccountHandle"); + return; + } + + Notification notification = + createNotification( + context, + pinnedTelephonyManager, + handle, + count, + voicemailNumber, + callVoicemailIntent, + voicemailSettingsIntent, + isRefresh); + DialerNotificationManager.notify(context, NOTIFICATION_TAG, NOTIFICATION_ID, notification); + } + + @NonNull + private static Notification createNotification( + @NonNull Context context, + @NonNull TelephonyManager pinnedTelephonyManager, + @NonNull PhoneAccountHandle handle, + int count, + String voicemailNumber, + PendingIntent callVoicemailIntent, + PendingIntent voicemailSettingsIntent, + boolean isRefresh) { + String notificationTitle = + context + .getResources() + .getQuantityString(R.plurals.notification_voicemail_title, count, count); + boolean isOngoing = + pinnedTelephonyManager + .getCarrierConfig() + .getBoolean(CarrierConfigManager.KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL); + + String contentText; + PendingIntent contentIntent; + if (!TextUtils.isEmpty(voicemailNumber) && callVoicemailIntent != null) { + contentText = getNotificationText(context, handle, voicemailNumber); + contentIntent = callVoicemailIntent; + } else { + contentText = context.getString(R.string.notification_voicemail_no_vm_number); + contentIntent = voicemailSettingsIntent; + } + + Notification.Builder builder = + new Notification.Builder(context) + .setSmallIcon(android.R.drawable.stat_notify_voicemail) + .setColor(context.getColor(R.color.dialer_theme_color)) + .setWhen(System.currentTimeMillis()) + .setContentTitle(notificationTitle) + .setContentText(contentText) + .setContentIntent(contentIntent) + .setSound(pinnedTelephonyManager.getVoicemailRingtoneUri(handle)) + .setOngoing(isOngoing) + .setOnlyAlertOnce(isRefresh) + .setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle)) + .setDeleteIntent( + CallLogNotificationsService.createLegacyVoicemailDismissedPendingIntent( + context, handle)); + + if (pinnedTelephonyManager.isVoicemailVibrationEnabled(handle)) { + builder.setDefaults(Notification.DEFAULT_VIBRATE); + } + + return builder.build(); + } + + @NonNull + private static String getNotificationText( + @NonNull Context context, PhoneAccountHandle handle, String voicemailNumber) { + if (PhoneAccountUtils.getSubscriptionPhoneAccounts(context).size() > 1) { + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle); + return phoneAccount.getShortDescription().toString(); + } else { + return String.format( + context.getString(R.string.notification_voicemail_text_format), + PhoneNumberUtils.formatNumber(voicemailNumber)); + } + } + + public static void cancelNotification(@NonNull Context context) { + LogUtil.enterBlock("LegacyVoicemailNotifier.cancelNotification"); + Assert.checkArgument(BuildCompat.isAtLeastO()); + DialerNotificationManager.cancel(context, NOTIFICATION_TAG, NOTIFICATION_ID); + } + + private LegacyVoicemailNotifier() {} +} diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java index dd13298bc..b363b5ab6 100644 --- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -17,7 +17,6 @@ package com.android.dialer.app.calllog; import android.app.Notification; import android.app.Notification.Builder; -import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -30,11 +29,13 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; +import android.support.v4.os.BuildCompat; import android.support.v4.os.UserManagerCompat; import android.support.v4.util.Pair; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; +import android.util.ArraySet; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.dialer.app.DialtactsActivity; @@ -46,23 +47,30 @@ import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; -import com.android.dialer.notification.NotificationChannelManager; -import com.android.dialer.notification.NotificationChannelManager.Channel; +import com.android.dialer.notification.DialerNotificationManager; +import com.android.dialer.notification.NotificationChannelId; +import com.android.dialer.notification.NotificationManagerUtils; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.IntentUtil; -import java.util.HashSet; import java.util.List; import java.util.Set; /** Creates a notification for calls that the user missed (neither answered nor rejected). */ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { - /** The tag used to identify notifications from this class. */ - static final String NOTIFICATION_TAG = "MissedCallNotifier"; - /** The identifier of the notification of new missed calls. */ - private static final int NOTIFICATION_ID = R.id.notification_missed_call; + /** Prefix used to generate a unique tag for each missed call notification. */ + private static final String NOTIFICATION_TAG_PREFIX = "MissedCall_"; + /** Common ID for all missed call notifications. */ + private static final int NOTIFICATION_ID = 1; + /** Tag for the group summary notification. */ + private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_MissedCall"; + /** + * Key used to associate all missed call notifications and the summary as belonging to a single + * group. + */ + private static final String GROUP_KEY = "MissedCallGroup"; private final Context context; private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper; @@ -104,7 +112,8 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { if ((newCalls != null && newCalls.isEmpty()) || count == 0) { // No calls to notify about: clear the notification. - CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null); + CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context); + cancelAllMissedCallNotifications(context); return; } @@ -146,7 +155,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { null, System.currentTimeMillis()); - //TODO: look up caller ID that is not in contacts. + // TODO: look up caller ID that is not in contacts. ContactInfo contactInfo = callLogNotificationsQueryHelper.getContactInfo( call.number, call.numberPresentation, call.countryIso); @@ -181,52 +190,84 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { publicSummaryBuilder .setContentTitle(context.getText(titleResId)) .setContentIntent(createCallLogPendingIntent()) - .setDeleteIntent(createClearMissedCallsPendingIntent(null)); + .setDeleteIntent( + CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context)); // Create the notification summary suitable for display when sensitive information is showing. groupSummary .setContentTitle(context.getText(titleResId)) .setContentText(expandedText) .setContentIntent(createCallLogPendingIntent()) - .setDeleteIntent(createClearMissedCallsPendingIntent(null)) + .setDeleteIntent( + CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context)) .setGroupSummary(useCallList) .setOnlyAlertOnce(useCallList) .setPublicVersion(publicSummaryBuilder.build()); - - NotificationChannelManager.applyChannel(groupSummary, context, Channel.MISSED_CALL, null); + if (BuildCompat.isAtLeastO()) { + groupSummary.setChannelId(NotificationChannelId.MISSED_CALL); + } Notification notification = groupSummary.build(); configureLedOnNotification(notification); LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); - getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); + DialerNotificationManager.notify( + context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, notification); if (useCallList) { // Do not repost active notifications to prevent erasing post call notes. - NotificationManager manager = getNotificationMgr(); - Set<String> activeTags = new HashSet<>(); - for (StatusBarNotification activeNotification : manager.getActiveNotifications()) { + Set<String> activeTags = new ArraySet<>(); + for (StatusBarNotification activeNotification : + DialerNotificationManager.getActiveNotifications(context)) { activeTags.add(activeNotification.getTag()); } for (NewCall call : newCalls) { - String callTag = call.callsUri.toString(); + String callTag = getNotificationTagForCall(call); if (!activeTags.contains(callTag)) { - manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null)); + DialerNotificationManager.notify( + context, callTag, NOTIFICATION_ID, getNotificationForCall(call, null)); } } } } + public static void cancelAllMissedCallNotifications(@NonNull Context context) { + NotificationManagerUtils.cancelAllInGroup(context, GROUP_KEY); + } + + public static void cancelSingleMissedCallNotification( + @NonNull Context context, @Nullable Uri callUri) { + if (callUri == null) { + LogUtil.e( + "MissedCallNotifier.cancelSingleMissedCallNotification", + "unable to cancel notification, uri is null"); + return; + } + // This will also dismiss the group summary if there are no more missed call notifications. + DialerNotificationManager.cancel( + context, getNotificationTagForCallUri(callUri), NOTIFICATION_ID); + } + + private static String getNotificationTagForCall(@NonNull NewCall call) { + return getNotificationTagForCallUri(call.callsUri); + } + + private static String getNotificationTagForCallUri(@NonNull Uri callUri) { + return NOTIFICATION_TAG_PREFIX + callUri; + } + public void insertPostCallNotification(@NonNull String number, @NonNull String note) { List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); if (newCalls != null && !newCalls.isEmpty()) { for (NewCall call : newCalls) { if (call.number.equals(number.replace("tel:", ""))) { // Update the first notification that matches our post call note sender. - getNotificationMgr() - .notify( - call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note)); + DialerNotificationManager.notify( + context, + getNotificationTagForCall(call), + NOTIFICATION_ID, + getNotificationForCall(call, note)); break; } } @@ -308,7 +349,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { private Notification.Builder createNotificationBuilder() { return new Notification.Builder(context) - .setGroup(NOTIFICATION_TAG) + .setGroup(GROUP_KEY) .setSmallIcon(android.R.drawable.stat_notify_missed_call) .setColor(context.getResources().getColor(R.color.dialer_theme_color, null)) .setAutoCancel(true) @@ -321,10 +362,14 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { Builder builder = createNotificationBuilder() .setWhen(call.dateMs) - .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri)) + .setDeleteIntent( + CallLogNotificationsService.createCancelSingleMissedCallPendingIntent( + context, call.callsUri)) .setContentIntent(createCallLogPendingIntent(call.callsUri)); + if (BuildCompat.isAtLeastO()) { + builder.setChannelId(NotificationChannelId.MISSED_CALL); + } - NotificationChannelManager.applyChannel(builder, context, Channel.MISSED_CALL, null); return builder; } @@ -332,7 +377,8 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { @WorkerThread public void callBackFromMissedCall(String number, Uri callUri) { closeSystemDialogs(context); - CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); + CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); + cancelSingleMissedCallNotification(context, callUri); DialerUtils.startActivityWithErrorToast( context, new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION) @@ -343,7 +389,8 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { /** Trigger an intent to send an sms from a missed call number. */ public void sendSmsFromMissedCall(String number, Uri callUri) { closeSystemDialogs(context); - CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); + CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); + cancelSingleMissedCallNotification(context, callUri); DialerUtils.startActivityWithErrorToast( context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } @@ -371,14 +418,6 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); } - /** Creates a pending intent that marks all new missed calls as old. */ - private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) { - Intent intent = new Intent(context, CallLogNotificationsService.class); - intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); - intent.setData(callUri); - return PendingIntent.getService(context, 0, intent, 0); - } - private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) { Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); @@ -410,8 +449,4 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { private void closeSystemDialogs(Context context) { context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); } - - private NotificationManager getNotificationMgr() { - return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - } } diff --git a/java/com/android/dialer/app/calllog/PhoneAccountHandles.java b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java deleted file mode 100644 index acffffb1d..000000000 --- a/java/com/android/dialer/app/calllog/PhoneAccountHandles.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2017 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.app.calllog; - -import android.content.ComponentName; -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; -import com.android.dialer.common.LogUtil; -import com.android.dialer.telecom.TelecomUtil; -import java.util.List; - -/** Methods to help extract {@link PhoneAccount} information from database and Telecomm sources. */ -class PhoneAccountHandles { - - @Nullable - public static PhoneAccountHandle getAccount(@NonNull Context context, @Nullable NewCall call) { - PhoneAccountHandle handle; - if (call == null || call.accountComponentName == null || call.accountId == null) { - LogUtil.v( - "PhoneAccountUtils.getAccount", - "accountComponentName == null || callToNotify.accountId == null"); - handle = TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); - if (handle == null) { - List<PhoneAccountHandle> callCapablePhoneAccounts = - TelecomUtil.getCallCapablePhoneAccounts(context); - if (!callCapablePhoneAccounts.isEmpty()) { - return callCapablePhoneAccounts.get(0); - } - return null; - } - } else { - handle = - new PhoneAccountHandle( - ComponentName.unflattenFromString(call.accountComponentName), call.accountId); - } - if (handle.getComponentName() != null) { - LogUtil.v( - "PhoneAccountUtils.getAccount", - "PhoneAccountHandle.ComponentInfo:" + handle.getComponentName()); - } else { - LogUtil.i("PhoneAccountUtils.getAccount", "PhoneAccountHandle.ComponentInfo: null"); - } - return handle; - } -} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java index 0c720775a..c1a00e58d 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -45,6 +45,13 @@ 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; + // TODO(mdooley): remove when these api's become public + // Copied from android.provider.VoicemailContract + static final int TRANSCRIPTION_NOT_STARTED = 0; + static final int TRANSCRIPTION_IN_PROGRESS = 1; + static final int TRANSCRIPTION_FAILED = 2; + static final int TRANSCRIPTION_AVAILABLE = 3; + private final Context mContext; private final Resources mResources; private final CallLogCache mCallLogCache; @@ -145,14 +152,37 @@ public class PhoneCallDetailsHelper { if (isVoicemail) { int relevantLinkTypes = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS; views.voicemailTranscriptionView.setAutoLinkMask(relevantLinkTypes); - views.voicemailTranscriptionView.setText( - TextUtils.isEmpty(details.transcription) ? null : details.transcription); + boolean showTranscriptBranding = false; + if (!TextUtils.isEmpty(details.transcription)) { + views.voicemailTranscriptionView.setText(details.transcription); + + // Set the branding text if the voicemail was transcribed by google + // TODO(mdooley): the transcription state is only set by the google transcription code, + // but a better solution would be to check the SOURCE_PACKAGE + showTranscriptBranding = details.transcriptionState == TRANSCRIPTION_AVAILABLE; + } else { + if (details.transcriptionState == TRANSCRIPTION_IN_PROGRESS) { + views.voicemailTranscriptionView.setText( + mResources.getString(R.string.voicemail_transcription_in_progress)); + } else if (details.transcriptionState == TRANSCRIPTION_FAILED) { + views.voicemailTranscriptionView.setText( + mResources.getString(R.string.voicemail_transcription_failed)); + } + } + + if (showTranscriptBranding) { + views.voicemailTranscriptionBrandingView.setText( + mResources.getString(R.string.voicemail_transcription_branding_text)); + } else { + views.voicemailTranscriptionBrandingView.setText(""); + } } // Bold if not read Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD; views.nameView.setTypeface(typeface); views.voicemailTranscriptionView.setTypeface(typeface); + views.voicemailTranscriptionBrandingView.setTypeface(typeface); views.callLocationAndDate.setTypeface(typeface); views.callLocationAndDate.setTextColor( ContextCompat.getColor( diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java index e2e27a179..40c0894f0 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -29,7 +29,9 @@ public final class PhoneCallDetailsViews { public final View callTypeView; public final CallTypeIconsView callTypeIcons; public final TextView callLocationAndDate; + public final View transcriptionView; public final TextView voicemailTranscriptionView; + public final TextView voicemailTranscriptionBrandingView; public final TextView callAccountLabel; private PhoneCallDetailsViews( @@ -37,13 +39,17 @@ public final class PhoneCallDetailsViews { View callTypeView, CallTypeIconsView callTypeIcons, TextView callLocationAndDate, + View transcriptionView, TextView voicemailTranscriptionView, + TextView voicemailTranscriptionBrandingView, TextView callAccountLabel) { this.nameView = nameView; this.callTypeView = callTypeView; this.callTypeIcons = callTypeIcons; this.callLocationAndDate = callLocationAndDate; + this.transcriptionView = transcriptionView; this.voicemailTranscriptionView = voicemailTranscriptionView; + this.voicemailTranscriptionBrandingView = voicemailTranscriptionBrandingView; this.callAccountLabel = callAccountLabel; } @@ -60,7 +66,9 @@ public final class PhoneCallDetailsViews { view.findViewById(R.id.call_type), (CallTypeIconsView) view.findViewById(R.id.call_type_icons), (TextView) view.findViewById(R.id.call_location_and_date), + view.findViewById(R.id.transcription), (TextView) view.findViewById(R.id.voicemail_transcription), + (TextView) view.findViewById(R.id.voicemail_transcription_branding), (TextView) view.findViewById(R.id.call_account_label)); } @@ -70,6 +78,8 @@ public final class PhoneCallDetailsViews { new View(context), new CallTypeIconsView(context), new TextView(context), + new View(context), + new TextView(context), new TextView(context), new TextView(context)); } diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java index 893d6bed9..8bfd48b05 100644 --- a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java +++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java @@ -16,12 +16,15 @@ package com.android.dialer.app.calllog; +import android.app.KeyguardManager; +import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.media.AudioManager; import android.os.Bundle; import android.provider.CallLog; import android.provider.VoicemailContract; +import android.support.annotation.VisibleForTesting; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,15 +33,22 @@ import com.android.dialer.app.list.ListsFragment; import com.android.dialer.app.voicemail.VoicemailAudioManager; import com.android.dialer.app.voicemail.VoicemailErrorManager; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.app.voicemail.error.VoicemailErrorMessageCreator; +import com.android.dialer.app.voicemail.error.VoicemailStatus; +import com.android.dialer.app.voicemail.error.VoicemailStatusWorker; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor; +import com.android.dialer.common.concurrent.DialerExecutors; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.util.PermissionsUtil; +import java.util.List; public class VisualVoicemailCallLogFragment extends CallLogFragment { private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private DialerExecutor<Context> mPreSyncVoicemailStatusCheckExecutor; private VoicemailErrorManager mVoicemailErrorManager; @@ -55,7 +65,6 @@ public class VisualVoicemailCallLogFragment extends CallLogFragment { public void onActivityCreated(Bundle savedInstanceState) { mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), savedInstanceState); - if (PermissionsUtil.hasReadVoicemailPermissions(getContext()) && PermissionsUtil.hasAddVoicemailPermissions(getContext())) { getActivity() @@ -68,6 +77,15 @@ public class VisualVoicemailCallLogFragment extends CallLogFragment { "read voicemail permission unavailable."); } super.onActivityCreated(savedInstanceState); + + mPreSyncVoicemailStatusCheckExecutor = + DialerExecutors.createUiTaskBuilder( + getActivity().getFragmentManager(), + "fetchVoicemailStatus", + new VoicemailStatusWorker()) + .onSuccess(this::onPreSyncVoicemailStatusChecked) + .build(); + mVoicemailErrorManager = new VoicemailErrorManager(getContext(), getAdapter().getAlertManager(), mModalAlertManager); @@ -132,23 +150,52 @@ public class VisualVoicemailCallLogFragment extends CallLogFragment { @Override public void onVisible() { - LogUtil.enterBlock("VisualVoicemailCallLogFragment.onPageSelected"); + LogUtil.enterBlock("VisualVoicemailCallLogFragment.onVisible"); super.onVisible(); if (getActivity() != null) { - Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); - intent.setPackage(getActivity().getPackageName()); - getActivity().sendBroadcast(intent); + mPreSyncVoicemailStatusCheckExecutor.executeParallel(getActivity()); Logger.get(getActivity()).logImpression(DialerImpression.Type.VVM_TAB_VIEWED); getActivity().setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM); } } + private void onPreSyncVoicemailStatusChecked(List<VoicemailStatus> statuses) { + if (!shouldAutoSync(new VoicemailErrorMessageCreator(), statuses)) { + return; + } + + Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); + intent.setPackage(getActivity().getPackageName()); + getActivity().sendBroadcast(intent); + } + + @VisibleForTesting + static boolean shouldAutoSync( + VoicemailErrorMessageCreator errorMessageCreator, List<VoicemailStatus> statuses) { + for (VoicemailStatus status : statuses) { + if (!status.isActive()) { + continue; + } + if (errorMessageCreator.isSyncBlockingError(status)) { + LogUtil.i( + "VisualVoicemailCallLogFragment.shouldAutoSync", "auto-sync blocked due to " + status); + return false; + } + } + return true; + } + @Override public void onNotVisible() { - LogUtil.enterBlock("VisualVoicemailCallLogFragment.onPageUnselected"); + LogUtil.enterBlock("VisualVoicemailCallLogFragment.onNotVisible"); super.onNotVisible(); if (getActivity() != null) { getActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); + // onNotVisible will be called in the lock screen when the call ends + if (!getActivity().getSystemService(KeyguardManager.class).inKeyguardRestrictedInputMode()) { + LogUtil.i("VisualVoicemailCallLogFragment.onNotVisible", "clearing all new voicemails"); + CallLogNotificationsService.markAllNewVoicemailsAsOld(getActivity()); + } } } } diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java new file mode 100644 index 000000000..cbadfd317 --- /dev/null +++ b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2017 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.app.calllog; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; +import com.android.dialer.app.contactinfo.ContactPhotoLoader; +import com.android.dialer.app.list.DialtactsPagerAdapter; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.notification.DialerNotificationManager; +import com.android.dialer.notification.NotificationChannelManager; +import com.android.dialer.notification.NotificationManagerUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.telecom.TelecomUtil; +import java.util.List; +import java.util.Map; + +/** Shows a notification in the status bar for visual voicemail. */ +final class VisualVoicemailNotifier { + /** Prefix used to generate a unique tag for each voicemail notification. */ + private static final String NOTIFICATION_TAG_PREFIX = "VisualVoicemail_"; + /** Common ID for all voicemail notifications. */ + private static final int NOTIFICATION_ID = 1; + /** Tag for the group summary notification. */ + private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_VisualVoicemail"; + /** + * Key used to associate all voicemail notifications and the summary as belonging to a single + * group. + */ + private static final String GROUP_KEY = "VisualVoicemailGroup"; + + public static void showNotifications( + @NonNull Context context, + @NonNull List<NewCall> newCalls, + @NonNull Map<String, ContactInfo> contactInfos, + @Nullable String callers) { + LogUtil.enterBlock("VisualVoicemailNotifier.showNotifications"); + PendingIntent deleteIntent = + CallLogNotificationsService.createMarkAllNewVoicemailsAsOldIntent(context); + String contentTitle = + context + .getResources() + .getQuantityString( + R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()); + Notification.Builder groupSummary = + createNotificationBuilder(context) + .setContentTitle(contentTitle) + .setContentText(callers) + .setDeleteIntent(deleteIntent) + .setGroupSummary(true) + .setContentIntent(newVoicemailIntent(context, null)); + + if (BuildCompat.isAtLeastO()) { + groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN); + PhoneAccountHandle handle = getAccountForCall(context, newCalls.get(0)); + groupSummary.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle)); + } + + DialerNotificationManager.notify( + context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, groupSummary.build()); + + for (NewCall voicemail : newCalls) { + DialerNotificationManager.notify( + context, + getNotificationTagForVoicemail(voicemail), + NOTIFICATION_ID, + createNotificationForVoicemail(context, voicemail, contactInfos)); + } + } + + public static void cancelAllVoicemailNotifications(@NonNull Context context) { + LogUtil.enterBlock("VisualVoicemailNotifier.cancelAllVoicemailNotifications"); + NotificationManagerUtils.cancelAllInGroup(context, GROUP_KEY); + } + + public static void cancelSingleVoicemailNotification( + @NonNull Context context, @Nullable Uri voicemailUri) { + LogUtil.enterBlock("VisualVoicemailNotifier.cancelSingleVoicemailNotification"); + if (voicemailUri == null) { + LogUtil.e("VisualVoicemailNotifier.cancelSingleVoicemailNotification", "uri is null"); + return; + } + // This will also dismiss the group summary if there are no more voicemail notifications. + DialerNotificationManager.cancel( + context, getNotificationTagForUri(voicemailUri), NOTIFICATION_ID); + } + + private static String getNotificationTagForVoicemail(@NonNull NewCall voicemail) { + return getNotificationTagForUri(voicemail.voicemailUri); + } + + private static String getNotificationTagForUri(@NonNull Uri voicemailUri) { + return NOTIFICATION_TAG_PREFIX + voicemailUri; + } + + private static Notification.Builder createNotificationBuilder(@NonNull Context context) { + return new Notification.Builder(context) + .setSmallIcon(android.R.drawable.stat_notify_voicemail) + .setColor(context.getColor(R.color.dialer_theme_color)) + .setGroup(GROUP_KEY) + .setOnlyAlertOnce(true) + .setAutoCancel(true); + } + + private static Notification createNotificationForVoicemail( + @NonNull Context context, + @NonNull NewCall voicemail, + @NonNull Map<String, ContactInfo> contactInfos) { + PhoneAccountHandle handle = getAccountForCall(context, voicemail); + ContactInfo contactInfo = contactInfos.get(voicemail.number); + + Notification.Builder builder = + createNotificationBuilder(context) + .setContentTitle( + context + .getResources() + .getQuantityString(R.plurals.notification_voicemail_title, 1, 1)) + .setContentText( + ContactDisplayUtils.getTtsSpannedPhoneNumber( + context.getResources(), + R.string.notification_new_voicemail_ticker, + contactInfo.name)) + .setWhen(voicemail.dateMs) + .setSound(getVoicemailRingtoneUri(context, handle)) + .setDefaults(getNotificationDefaultFlags(context, handle)); + + if (voicemail.voicemailUri != null) { + builder.setDeleteIntent( + CallLogNotificationsService.createMarkSingleNewVoicemailAsOldIntent( + context, voicemail.voicemailUri)); + } + + if (BuildCompat.isAtLeastO()) { + builder.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle)); + } + + ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); + Bitmap photoIcon = loader.loadPhotoIcon(); + if (photoIcon != null) { + builder.setLargeIcon(photoIcon); + } + if (!TextUtils.isEmpty(voicemail.transcription)) { + Logger.get(context) + .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION); + builder.setStyle(new Notification.BigTextStyle().bigText(voicemail.transcription)); + } + builder.setContentIntent(newVoicemailIntent(context, voicemail)); + Logger.get(context).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED); + return builder.build(); + } + + @Nullable + private static Uri getVoicemailRingtoneUri( + @NonNull Context context, @Nullable PhoneAccountHandle handle) { + if (VERSION.SDK_INT < VERSION_CODES.N) { + return null; + } + if (handle == null) { + LogUtil.i("VisualVoicemailNotifier.getVoicemailRingtoneUri", "null handle, getting fallback"); + handle = getFallbackAccount(context); + if (handle == null) { + LogUtil.i( + "VisualVoicemailNotifier.getVoicemailRingtoneUri", + "no fallback handle, using null (default) ringtone"); + return null; + } + } + return context.getSystemService(TelephonyManager.class).getVoicemailRingtoneUri(handle); + } + + private static int getNotificationDefaultFlags( + @NonNull Context context, @Nullable PhoneAccountHandle handle) { + if (VERSION.SDK_INT < VERSION_CODES.N) { + return Notification.DEFAULT_ALL; + } + if (handle == null) { + LogUtil.i( + "VisualVoicemailNotifier.getNotificationDefaultFlags", "null handle, getting fallback"); + handle = getFallbackAccount(context); + if (handle == null) { + LogUtil.i( + "VisualVoicemailNotifier.getNotificationDefaultFlags", + "no fallback handle, using default vibration"); + return Notification.DEFAULT_ALL; + } + } + if (context.getSystemService(TelephonyManager.class).isVoicemailVibrationEnabled(handle)) { + return Notification.DEFAULT_VIBRATE; + } + return 0; + } + + private static PendingIntent newVoicemailIntent( + @NonNull Context context, @Nullable NewCall voicemail) { + Intent intent = + DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); + // TODO (b/35486204): scroll to this voicemail + if (voicemail != null) { + intent.setData(voicemail.voicemailUri); + } + intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Gets a phone account for the given call entry. This could be null if SIM associated with the + * entry is no longer in the device or for other reasons (for example, modem reboot). + */ + @Nullable + public static PhoneAccountHandle getAccountForCall( + @NonNull Context context, @Nullable NewCall call) { + if (call == null || call.accountComponentName == null || call.accountId == null) { + return null; + } + return new PhoneAccountHandle( + ComponentName.unflattenFromString(call.accountComponentName), call.accountId); + } + + /** + * Gets any available phone account that can be used to get sound settings for voicemail. This is + * only called if the phone account for the voicemail entry can't be found. + */ + @Nullable + public static PhoneAccountHandle getFallbackAccount(@NonNull Context context) { + PhoneAccountHandle handle = + TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); + if (handle == null) { + List<PhoneAccountHandle> handles = TelecomUtil.getCallCapablePhoneAccounts(context); + if (!handles.isEmpty()) { + handle = handles.get(0); + } + } + return handle; + } + + private VisualVoicemailNotifier() {} +} diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java new file mode 100644 index 000000000..d6601be36 --- /dev/null +++ b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.text.TextUtils; +import android.util.ArrayMap; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutors; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Updates voicemail notifications in the background. */ +class VisualVoicemailUpdateTask implements Worker<VisualVoicemailUpdateTask.Input, Void> { + @Nullable + @Override + public Void doInBackground(@NonNull Input input) throws Throwable { + updateNotification(input.context, input.queryHelper, input.queryHandler); + return null; + } + + /** + * Updates the notification and notifies of the call with the given URI. + * + * <p>Clears the notification if there are no new voicemails, and notifies if the given URI + * corresponds to a new voicemail. + */ + @WorkerThread + private static void updateNotification( + Context context, + CallLogNotificationsQueryHelper queryHelper, + FilteredNumberAsyncQueryHandler queryHandler) { + Assert.isWorkerThread(); + + List<NewCall> newCalls = queryHelper.getNewVoicemails(); + if (newCalls == null) { + return; + } + newCalls = filterBlockedNumbers(context, queryHandler, newCalls); + if (newCalls.isEmpty()) { + return; + } + + // This represents a list of names to include in the notification. + String callers = null; + + // Maps each number into a name: if a number is in the map, it has already left a more + // recent voicemail. + Map<String, ContactInfo> contactInfos = new ArrayMap<>(); + for (NewCall newCall : newCalls) { + if (!contactInfos.containsKey(newCall.number)) { + ContactInfo contactInfo = + queryHelper.getContactInfo( + newCall.number, newCall.numberPresentation, newCall.countryIso); + contactInfos.put(newCall.number, contactInfo); + + // This is a new caller. Add it to the back of the list of callers. + if (TextUtils.isEmpty(callers)) { + callers = contactInfo.name; + } else { + callers = + context.getString( + R.string.notification_voicemail_callers_list, callers, contactInfo.name); + } + } + } + VisualVoicemailNotifier.showNotifications(context, newCalls, contactInfos, callers); + } + + @WorkerThread + private static List<NewCall> filterBlockedNumbers( + Context context, FilteredNumberAsyncQueryHandler queryHandler, List<NewCall> newCalls) { + Assert.isWorkerThread(); + if (FilteredNumbersUtil.hasRecentEmergencyCall(context)) { + LogUtil.i( + "VisualVoicemailUpdateTask.filterBlockedNumbers", + "not filtering due to recent emergency call"); + return newCalls; + } + + List<NewCall> result = new ArrayList<>(); + for (NewCall newCall : newCalls) { + if (queryHandler.getBlockedIdSynchronous(newCall.number, newCall.countryIso) != null) { + LogUtil.i( + "VisualVoicemailUpdateTask.filterBlockedNumbers", + "found voicemail from blocked number, deleting"); + if (newCall.voicemailUri != null) { + // Delete the voicemail. + CallLogAsyncTaskUtil.deleteVoicemailSynchronous(context, newCall.voicemailUri); + } + } else { + result.add(newCall); + } + } + return result; + } + + /** Updates the voicemail notifications displayed. */ + static void scheduleTask(@NonNull Context context, @NonNull Runnable callback) { + Assert.isNotNull(context); + Assert.isNotNull(callback); + if (!TelecomUtil.isDefaultDialer(context)) { + LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "not default dialer, not running"); + callback.run(); + return; + } + + Input input = + new Input( + context, + CallLogNotificationsQueryHelper.getInstance(context), + new FilteredNumberAsyncQueryHandler(context)); + DialerExecutors.createNonUiTaskBuilder(new VisualVoicemailUpdateTask()) + .onSuccess( + output -> { + LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "update successful"); + callback.run(); + }) + .onFailure( + throwable -> { + LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "update failed: " + throwable); + callback.run(); + }) + .build() + .executeParallel(input); + } + + static class Input { + @NonNull final Context context; + @NonNull final CallLogNotificationsQueryHelper queryHelper; + @NonNull final FilteredNumberAsyncQueryHandler queryHandler; + + Input( + Context context, + CallLogNotificationsQueryHelper queryHelper, + FilteredNumberAsyncQueryHandler queryHandler) { + this.context = context; + this.queryHelper = queryHelper; + this.queryHandler = queryHandler; + } + } +} diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java index 777f4c79f..2fbebdd30 100644 --- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java +++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java @@ -15,7 +15,6 @@ */ package com.android.dialer.app.calllog; -import android.app.NotificationManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentValues; @@ -23,30 +22,49 @@ import android.content.Context; import android.net.Uri; import android.provider.CallLog.Calls; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.android.dialer.app.R; +import android.support.annotation.WorkerThread; import com.android.dialer.common.Assert; -import com.android.dialer.notification.GroupedNotificationUtil; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; /** Handles asynchronous queries to the call log for voicemail. */ public class VoicemailQueryHandler extends AsyncQueryHandler { - private static final String TAG = "VoicemailQueryHandler"; - /** The token for the query to mark all new voicemails as old. */ private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 50; - private Context mContext; - @MainThread - public VoicemailQueryHandler(Context context, ContentResolver contentResolver) { + private VoicemailQueryHandler(ContentResolver contentResolver) { super(contentResolver); Assert.isMainThread(); - mContext = context; + } + + @WorkerThread + public static void markAllNewVoicemailsAsRead(final @NonNull Context context) { + ThreadUtil.postOnUiThread( + () -> { + new VoicemailQueryHandler(context.getContentResolver()).markNewVoicemailsAsOld(null); + }); + } + + @WorkerThread + public static void markSingleNewVoicemailAsRead( + final @NonNull Context context, final Uri voicemailUri) { + if (voicemailUri == null) { + LogUtil.e("VoicemailQueryHandler.markSingleNewVoicemailAsRead", "voicemail URI is null"); + return; + } + ThreadUtil.postOnUiThread( + () -> { + new VoicemailQueryHandler(context.getContentResolver()) + .markNewVoicemailsAsOld(voicemailUri); + }); } /** Updates all new voicemails to mark them as old. */ - public void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) { + private void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) { // Mark all "new" voicemails as not new anymore. StringBuilder where = new StringBuilder(); where.append(Calls.NEW); @@ -70,11 +88,5 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { voicemailUri == null ? new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)} : new String[] {Integer.toString(Calls.VOICEMAIL_TYPE), voicemailUri.toString()}); - - GroupedNotificationUtil.removeNotification( - mContext.getSystemService(NotificationManager.class), - voicemailUri != null ? voicemailUri.toString() : null, - R.id.notification_visual_voicemail, - DefaultVoicemailNotifier.VISUAL_VOICEMAIL_NOTIFICATION_TAG); } } diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java index 7645a333e..15de14318 100644 --- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java @@ -17,10 +17,16 @@ package com.android.dialer.app.calllog.calllogcache; import android.content.Context; +import android.support.annotation.Nullable; import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.ArrayMap; import com.android.dialer.app.calllog.CallLogAdapter; -import com.android.dialer.compat.CompatUtils; +import com.android.dialer.calllogutils.PhoneAccountUtils; +import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.CallUtil; +import java.util.Map; +import javax.annotation.concurrent.ThreadSafe; /** * This is the base class for the CallLogCaches. @@ -31,7 +37,8 @@ import com.android.dialer.util.CallUtil; * * <p>This is designed with the specific use case of the {@link CallLogAdapter} in mind. */ -public abstract class CallLogCache { +@ThreadSafe +public 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. @@ -39,20 +46,18 @@ public abstract class CallLogCache { private boolean mHasCheckedForVideoAvailability; private int mVideoAvailability; + private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new ArrayMap<>(); + private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new ArrayMap<>(); + private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new ArrayMap<>(); 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() { + public synchronized void reset() { + mPhoneAccountLabelCache.clear(); + mPhoneAccountColorCache.clear(); + mPhoneAccountCallWithNoteCache.clear(); mHasCheckedForVideoAvailability = false; mVideoAvailability = 0; } @@ -61,19 +66,12 @@ public abstract class CallLogCache { * 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); - - /** - * Returns {@code true} when the current sim supports video calls, regardless of the value in a - * contact's {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} - * column. - */ - public boolean isVideoEnabled() { - if (!mHasCheckedForVideoAvailability) { - mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); - mHasCheckedForVideoAvailability = true; + public synchronized boolean isVoicemailNumber( + PhoneAccountHandle accountHandle, @Nullable CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; } - return (mVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) != 0; + return TelecomUtil.isVoicemailNumber(mContext, accountHandle, number.toString()); } /** @@ -89,10 +87,26 @@ public abstract class CallLogCache { } /** Extract account label from PhoneAccount object. */ - public abstract String getAccountLabel(PhoneAccountHandle accountHandle); + public synchronized String getAccountLabel(PhoneAccountHandle accountHandle) { + if (mPhoneAccountLabelCache.containsKey(accountHandle)) { + return mPhoneAccountLabelCache.get(accountHandle); + } else { + String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle); + mPhoneAccountLabelCache.put(accountHandle, label); + return label; + } + } /** Extract account color from PhoneAccount object. */ - public abstract int getAccountColor(PhoneAccountHandle accountHandle); + public synchronized int getAccountColor(PhoneAccountHandle accountHandle) { + if (mPhoneAccountColorCache.containsKey(accountHandle)) { + return mPhoneAccountColorCache.get(accountHandle); + } else { + Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle); + mPhoneAccountColorCache.put(accountHandle, color); + return color; + } + } /** * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note) @@ -101,5 +115,14 @@ public abstract class CallLogCache { * @param accountHandle The PhoneAccount handle. * @return {@code true} if calling with a note is supported, {@code false} otherwise. */ - public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle); + public synchronized boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) { + return mPhoneAccountCallWithNoteCache.get(accountHandle); + } else { + Boolean supportsCallWithNote = + PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle); + mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote); + return supportsCallWithNote; + } + } } diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java deleted file mode 100644 index 78aaa4193..000000000 --- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java +++ /dev/null @@ -1,74 +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.app.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). - * - * <p>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/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java deleted file mode 100644 index 039998780..000000000 --- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2013 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.app.calllog.calllogcache; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; -import android.telecom.PhoneAccountHandle; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.Pair; -import com.android.dialer.calllogutils.PhoneAccountUtils; -import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for multi-SIM - * devices. - * - * <p>This class should not be initialized directly and instead be acquired from {@link - * CallLogCache#getCallLogCache}. - */ -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. Access must be synchronzied - * as it's used in the background thread in CallLogAdapter. {@see CallLogAdapter#loadData} - */ - @VisibleForTesting - final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache = - new ConcurrentHashMap<>(); - - private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new ArrayMap<>(); - private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new ArrayMap<>(); - private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new ArrayMap<>(); - - /* package */ CallLogCacheLollipopMr1(Context context) { - super(context); - } - - @Override - public void reset() { - mVoicemailQueryCache.clear(); - mPhoneAccountLabelCache.clear(); - mPhoneAccountColorCache.clear(); - mPhoneAccountCallWithNoteCache.clear(); - - super.reset(); - } - - @Override - public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { - if (TextUtils.isEmpty(number)) { - return false; - } - - Pair<PhoneAccountHandle, CharSequence> key = new Pair<>(accountHandle, number); - Boolean value = mVoicemailQueryCache.get(key); - if (value != null) { - return value; - } - boolean isVoicemail = - PhoneNumberHelper.isVoicemailNumber(mContext, accountHandle, number.toString()); - mVoicemailQueryCache.put(key, isVoicemail); - return isVoicemail; - } - - @Override - public String getAccountLabel(PhoneAccountHandle accountHandle) { - if (mPhoneAccountLabelCache.containsKey(accountHandle)) { - return mPhoneAccountLabelCache.get(accountHandle); - } else { - String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle); - mPhoneAccountLabelCache.put(accountHandle, label); - return label; - } - } - - @Override - public int getAccountColor(PhoneAccountHandle accountHandle) { - if (mPhoneAccountColorCache.containsKey(accountHandle)) { - return mPhoneAccountColorCache.get(accountHandle); - } else { - Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle); - mPhoneAccountColorCache.put(accountHandle, color); - return color; - } - } - - @Override - public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { - if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) { - return mPhoneAccountCallWithNoteCache.get(accountHandle); - } else { - Boolean supportsCallWithNote = - PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle); - mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote); - return supportsCallWithNote; - } - } -} |