From 86135a2f4d9b6dc5facf5e33e554c23a31824c41 Mon Sep 17 00:00:00 2001 From: linyuh Date: Mon, 12 Feb 2018 09:58:34 -0800 Subject: Add bottom sheet options for blocked and/or spam numbers in the new call log. Bug: 70989605 Test: ModulesTest PiperOrigin-RevId: 185392711 Change-Id: I709a1e307925f1c99d2740ed52dc2b7784bca986 --- .../android/dialer/calllog/ui/menu/Modules.java | 39 ++++-- .../dialer/contactactions/IntentModule.java | 2 +- .../dialer/contactactions/SharedModules.java | 156 +++++++++++++++++++-- .../res/drawable-xxxhdpi/ic_unblock.png | Bin 0 -> 1034 bytes .../dialer/contactactions/res/values/strings.xml | 22 ++- .../android/dialer/theme/res/values/strings.xml | 3 - .../dialer/voicemail/listui/menu/Modules.java | 15 +- 7 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png (limited to 'java/com/android') diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java index d59155810..c85a9fddd 100644 --- a/java/com/android/dialer/calllog/ui/menu/Modules.java +++ b/java/com/android/dialer/calllog/ui/menu/Modules.java @@ -52,15 +52,8 @@ final class Modules { if (canPlaceCalls) { addModuleForVideoOrAudioCall(context, modules, row, normalizedNumber); - - SharedModules.maybeAddModuleForAddingToContacts( - context, - modules, - row.number(), - row.numberAttributes().getName(), - row.numberAttributes().getLookupUri()); - - SharedModules.maybeAddModuleForSendingTextMessage(context, modules, normalizedNumber); + SharedModules.maybeAddModuleForSendingTextMessage( + context, modules, normalizedNumber, row.numberAttributes().getIsBlocked()); } if (!modules.isEmpty()) { @@ -68,10 +61,23 @@ final class Modules { } - // TODO(zachh): Module for blocking/unblocking spam. // TODO(zachh): Module for CallComposer. if (canPlaceCalls) { + SharedModules.maybeAddModuleForAddingToContacts( + context, + modules, + row.number(), + row.numberAttributes().getName(), + row.numberAttributes().getLookupUri(), + row.numberAttributes().getIsBlocked(), + row.numberAttributes().getIsSpam()); + SharedModules.addModulesHandlingBlockedOrSpamNumber( + context, + modules, + normalizedNumber, + row.numberAttributes().getIsBlocked(), + row.numberAttributes().getIsSpam()); SharedModules.maybeAddModuleForCopyingNumber(context, modules, normalizedNumber); } @@ -89,10 +95,23 @@ final class Modules { List modules, CoalescedRow row, String normalizedNumber) { + // If a number is blocked, skip this menu item. + if (row.numberAttributes().getIsBlocked()) { + return; + } + PhoneAccountHandle phoneAccountHandle = TelecomUtil.composePhoneAccountHandle( row.phoneAccountComponentName(), row.phoneAccountId()); + // For a spam number, only audio calls are allowed. + if (row.numberAttributes().getIsSpam()) { + modules.add( + IntentModule.newCallModule( + context, normalizedNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG)); + return; + } + if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { // Add an audio call item for video calls. Clicking the top entry on the bottom sheet will // trigger a video call. diff --git a/java/com/android/dialer/contactactions/IntentModule.java b/java/com/android/dialer/contactactions/IntentModule.java index aa7fd25a6..9a345c669 100644 --- a/java/com/android/dialer/contactactions/IntentModule.java +++ b/java/com/android/dialer/contactactions/IntentModule.java @@ -72,7 +72,7 @@ public class IntentModule implements ContactActionModule { context, new CallIntentBuilder(number, initiationType) .setPhoneAccountHandle(phoneAccountHandle)), - R.string.call, + R.string.voice_call, R.drawable.quantum_ic_call_white_24); } diff --git a/java/com/android/dialer/contactactions/SharedModules.java b/java/com/android/dialer/contactactions/SharedModules.java index 7e72863aa..6d97fcb61 100644 --- a/java/com/android/dialer/contactactions/SharedModules.java +++ b/java/com/android/dialer/contactactions/SharedModules.java @@ -20,14 +20,15 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.ContactsContract; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.widget.Toast; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.clipboard.ClipboardUtils; import com.android.dialer.util.IntentUtil; import com.android.dialer.util.UriUtils; import java.util.List; +import java.util.Locale; /** * Modules for the bottom sheet that are shared between NewVoicemailFragment and NewCallLogFragment @@ -37,10 +38,15 @@ public class SharedModules { public static void maybeAddModuleForAddingToContacts( Context context, List modules, - @NonNull DialerPhoneNumber number, - @Nullable String name, - @Nullable String lookupUri) { - // TODO(zachh): Only show this for non-spam/blocked numbers. + DialerPhoneNumber number, + String name, + String lookupUri, + boolean isBlocked, + boolean isSpam) { + // Skip showing the menu item for a spam/blocked number. + if (isBlocked || isSpam) { + return; + } // Skip showing the menu item for existing contacts. if (isExistingContact(lookupUri)) { @@ -83,22 +89,148 @@ public class SharedModules { } public static void maybeAddModuleForSendingTextMessage( - Context context, List modules, String originalNumber) { + Context context, + List modules, + String normalizedNumber, + boolean isBlocked) { + // Don't show the option to send a text message if the number is blocked. + if (isBlocked) { + return; + } + // TODO(zachh): There are some conditions where this module should not be shown; consider - // voicemail, business numbers, blocked numbers, spam numbers, etc. - if (!TextUtils.isEmpty(originalNumber)) { + // voicemail, business numbers, etc. + + if (!TextUtils.isEmpty(normalizedNumber)) { modules.add( new IntentModule( context, - IntentUtil.getSendSmsIntent(originalNumber), + IntentUtil.getSendSmsIntent(normalizedNumber), R.string.send_a_message, R.drawable.quantum_ic_message_vd_theme_24)); } } + public static void addModulesHandlingBlockedOrSpamNumber( + Context context, + List modules, + String normalizedNumber, + boolean isBlocked, + boolean isSpam) { + // For a spam number, add two options: + // (1) "Not spam" and "Block", or + // (2) "Not spam" and "Unblock". + if (isSpam) { + addModuleForMarkingNumberAsNonSpam(context, modules, normalizedNumber); + addModuleForBlockingOrUnblockingNumber(context, modules, normalizedNumber, isBlocked); + return; + } + + // For a blocked non-spam number, add "Unblock" option. + if (isBlocked) { + addModuleForBlockingOrUnblockingNumber(context, modules, normalizedNumber, isBlocked); + return; + } + + // For a number that is neither a spam number nor blocked, add "Block/Report spam" option. + addModuleForBlockingNumberAndOptionallyReportingSpam(context, modules, normalizedNumber); + } + + private static void addModuleForMarkingNumberAsNonSpam( + Context context, List modules, String normalizedNumber) { + modules.add( + new ContactActionModule() { + @Override + public int getStringId() { + return R.string.not_spam; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_report_off_vd_theme_24; + } + + @Override + public boolean onClick() { + // TODO(a bug): implement this method. + Toast.makeText( + context, + String.format(Locale.ENGLISH, "TODO: Report %s as non-spam", normalizedNumber), + Toast.LENGTH_SHORT) + .show(); + return true; // Close the bottom sheet. + } + }); + } + + private static void addModuleForBlockingOrUnblockingNumber( + Context context, + List modules, + String normalizedNumber, + boolean isBlocked) { + modules.add( + new ContactActionModule() { + @Override + public int getStringId() { + return isBlocked ? R.string.unblock_number : R.string.block_number; + } + + @Override + public int getDrawableId() { + return isBlocked + ? R.drawable.ic_unblock // TODO(a bug): use a vector icon + : R.drawable.quantum_ic_block_vd_theme_24; + } + + @Override + public boolean onClick() { + // TODO(a bug): implement this method. + Toast.makeText( + context, + String.format( + Locale.ENGLISH, + "TODO: " + (isBlocked ? "Unblock " : "Block ") + " number %s.", + normalizedNumber), + Toast.LENGTH_SHORT) + .show(); + return true; // Close the bottom sheet. + } + }); + } + + private static void addModuleForBlockingNumberAndOptionallyReportingSpam( + Context context, List modules, String normalizedNumber) { + modules.add( + new ContactActionModule() { + @Override + public int getStringId() { + return R.string.block_and_optionally_report_spam; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_block_vd_theme_24; + } + + @Override + public boolean onClick() { + // TODO(a bug): implement this method. + Toast.makeText( + context, + String.format( + Locale.ENGLISH, + "TODO: Block and optionally report as spam %s.", + normalizedNumber), + Toast.LENGTH_SHORT) + .show(); + return true; // Close the bottom sheet. + } + }); + } + public static void maybeAddModuleForCopyingNumber( - Context context, List modules, String originalNumber) { - if (TextUtils.isEmpty(originalNumber)) { + Context context, List modules, String normalizedNumber) { + if (TextUtils.isEmpty(normalizedNumber)) { return; } modules.add( @@ -115,7 +247,7 @@ public class SharedModules { @Override public boolean onClick() { - ClipboardUtils.copyText(context, null, originalNumber, true); + ClipboardUtils.copyText(context, null, normalizedNumber, true); return false; } }); diff --git a/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png b/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png new file mode 100644 index 000000000..01551e2fc Binary files /dev/null and b/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/contactactions/res/values/strings.xml b/java/com/android/dialer/contactactions/res/values/strings.xml index 0e953a56d..4d598a930 100644 --- a/java/com/android/dialer/contactactions/res/values/strings.xml +++ b/java/com/android/dialer/contactactions/res/values/strings.xml @@ -16,13 +16,31 @@ --> + + Voice call + + + Video call + - Add to contacts + Add contact - Send a message + Message + + + Not spam + + + Block + + + Unblock + + + Block/Report spam Copy number diff --git a/java/com/android/dialer/theme/res/values/strings.xml b/java/com/android/dialer/theme/res/values/strings.xml index 74cabadf7..a14693f51 100644 --- a/java/com/android/dialer/theme/res/values/strings.xml +++ b/java/com/android/dialer/theme/res/values/strings.xml @@ -30,9 +30,6 @@ used in the Launcher icon. --> Phone - - Video call - Call diff --git a/java/com/android/dialer/voicemail/listui/menu/Modules.java b/java/com/android/dialer/voicemail/listui/menu/Modules.java index 665031a1f..7254ad651 100644 --- a/java/com/android/dialer/voicemail/listui/menu/Modules.java +++ b/java/com/android/dialer/voicemail/listui/menu/Modules.java @@ -41,16 +41,25 @@ final class Modules { modules, voicemailEntry.number(), voicemailEntry.numberAttributes().getName(), - voicemailEntry.numberAttributes().getLookupUri()); + voicemailEntry.numberAttributes().getLookupUri(), + voicemailEntry.numberAttributes().getIsBlocked(), + voicemailEntry.numberAttributes().getIsSpam()); String normalizedNumber = voicemailEntry.number().getNormalizedNumber(); - SharedModules.maybeAddModuleForSendingTextMessage(context, modules, normalizedNumber); + SharedModules.maybeAddModuleForSendingTextMessage( + context, modules, normalizedNumber, voicemailEntry.numberAttributes().getIsBlocked()); if (!modules.isEmpty()) { modules.add(new DividerModule()); } - // TODO(zachh): Module for blocking/unblocking spam. + SharedModules.addModulesHandlingBlockedOrSpamNumber( + context, + modules, + normalizedNumber, + voicemailEntry.numberAttributes().getIsBlocked(), + voicemailEntry.numberAttributes().getIsSpam()); + // TODO(zachh): Module for CallComposer. SharedModules.maybeAddModuleForCopyingNumber(context, modules, normalizedNumber); -- cgit v1.2.3 From b2d5e7caf51de7805b98295e97c37a91792cbddf Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Mon, 12 Feb 2018 11:07:47 -0800 Subject: Clicking on a missed call in the call log no longer crashes the app. Bug: 72956783 Test: DialtactsActivityIntegrationTest PiperOrigin-RevId: 185404033 Change-Id: I486f7b1a6739bf49c6f19bba82227dd4d9794e1f --- java/com/android/dialer/app/calllog/CallLogAdapter.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index 51df70219..baca590b5 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -54,6 +54,7 @@ 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.DialtactsActivity; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener; import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; @@ -381,9 +382,7 @@ public class CallLogAdapter extends GroupingListAdapter if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { CallLogAsyncTaskUtil.markCallAsRead(activity, viewHolder.callIds); if (activityType == ACTIVITY_TYPE_DIALTACTS) { - if (v.getContext() instanceof CallLogFragmentListener) { - ((CallLogFragmentListener) v.getContext()).updateTabUnreadCounts(); - } else if (v.getContext() instanceof MainActivityPeer.PeerSupplier) { + if (v.getContext() instanceof MainActivityPeer.PeerSupplier) { // This is really bad, but we must do this to prevent a dependency cycle, enforce // best practices in new code, and avoid refactoring DialtactsActivity. ((FragmentUtilListener) @@ -391,8 +390,7 @@ public class CallLogAdapter extends GroupingListAdapter .getImpl(CallLogFragmentListener.class) .updateTabUnreadCounts(); } else { - throw Assert.createIllegalStateFailException( - "View parent does not implement CallLogFragmentListener"); + ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); } } } -- cgit v1.2.3 From c266566db55647ac1e27f686b6f03440c5eee36b Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Mon, 12 Feb 2018 11:40:20 -0800 Subject: Restored work profile contacts to Dialer search. We were intentionally leaving out work profile contacts to gauge user impact. Unfortunatly, no enterprise users left feedback in the play store but luckily, they raised the concern internally and it was escalated to the Android for Work team. Bug: 73083054 Test: manual PiperOrigin-RevId: 185409607 Change-Id: Iae40d0abdbe7f209ba3ad12823e07eac748632a5 --- .../contacts/common/compat/DirectoryCompat.java | 4 ++++ .../common/res/layout/search_contact_row.xml | 11 +++++++++++ .../remote/RemoteContactViewHolder.java | 8 ++++++++ .../searchfragment/remote/RemoteContactsCursor.java | 8 +++++++- .../remote/RemoteContactsCursorLoader.java | 21 +++++++++------------ 5 files changed, 39 insertions(+), 13 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java index 85f4a4202..e67087659 100644 --- a/java/com/android/contacts/common/compat/DirectoryCompat.java +++ b/java/com/android/contacts/common/compat/DirectoryCompat.java @@ -48,4 +48,8 @@ public class DirectoryCompat { public static boolean isEnterpriseDirectoryId(long directoryId) { return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId); } + + public static boolean isOnlyEnterpriseDirectoryId(long directoryId) { + return isEnterpriseDirectoryId(directoryId) && !isRemoteDirectoryId(directoryId); + } } diff --git a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml index 407207a83..9be7fa046 100644 --- a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml +++ b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml @@ -65,4 +65,15 @@ android:tint="@color/dialer_secondary_text_color" android:visibility="gone" android:scaleType="center"/> + + \ No newline at end of file diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java index 9d369003d..4be96fe58 100644 --- a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java +++ b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java @@ -26,8 +26,10 @@ import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.View; +import android.widget.ImageView; import android.widget.QuickContactBadge; import android.widget.TextView; +import com.android.contacts.common.compat.DirectoryCompat; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.contactphoto.ContactPhotoManager; @@ -46,6 +48,7 @@ public final class RemoteContactViewHolder extends RecyclerView.ViewHolder private final TextView nameView; private final TextView numberView; private final QuickContactBadge photo; + private final ImageView workBadge; private String number; @@ -55,6 +58,7 @@ public final class RemoteContactViewHolder extends RecyclerView.ViewHolder photo = view.findViewById(R.id.photo); nameView = view.findViewById(R.id.primary); numberView = view.findViewById(R.id.secondary); + workBadge = view.findViewById(R.id.work_icon); context = view.getContext(); } @@ -74,6 +78,10 @@ public final class RemoteContactViewHolder extends RecyclerView.ViewHolder nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context)); numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo, context)); + workBadge.setVisibility( + DirectoryCompat.isOnlyEnterpriseDirectoryId(cursor.getDirectoryId()) + ? View.VISIBLE + : View.GONE); if (shouldShowPhoto(cursor)) { nameView.setVisibility(View.VISIBLE); diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java index 9510443b9..653c67041 100644 --- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java +++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java @@ -22,6 +22,7 @@ import android.database.MatrixCursor; import android.database.MergeCursor; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import com.android.contacts.common.compat.DirectoryCompat; import com.android.dialer.common.Assert; import com.android.dialer.searchfragment.common.SearchCursor; import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader; @@ -101,7 +102,12 @@ public final class RemoteContactsCursor extends MergeCursor implements SearchCur private static MatrixCursor createHeaderCursor(Context context, String name, long id) { MatrixCursor headerCursor = new MatrixCursor(PROJECTION, 1); - headerCursor.addRow(new Object[] {context.getString(R.string.directory, name), id}); + if (DirectoryCompat.isOnlyEnterpriseDirectoryId(id)) { + headerCursor.addRow( + new Object[] {context.getString(R.string.directory_search_label_work), id}); + } else { + headerCursor.addRow(new Object[] {context.getString(R.string.directory, name), id}); + } return headerCursor; } diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java index 9feeb7e99..cf495e49c 100644 --- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java @@ -27,6 +27,7 @@ import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import com.android.contacts.common.compat.DirectoryCompat; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory; import java.util.ArrayList; @@ -71,7 +72,14 @@ public final class RemoteContactsCursorLoader extends CursorLoader { Directory directory = directories.get(i); // Filter out local directories - if (!isRemoteDirectory(directory.getId())) { + if (!DirectoryCompat.isRemoteDirectoryId(directory.getId()) + && !DirectoryCompat.isEnterpriseDirectoryId(directory.getId())) { + cursors[i] = null; + continue; + } + + // Filter out invisible directories + if (DirectoryCompat.isInvisibleDirectory(directory.getId())) { cursors[i] = null; continue; } @@ -93,17 +101,6 @@ public final class RemoteContactsCursorLoader extends CursorLoader { return RemoteContactsCursor.newInstance(getContext(), cursors, directories); } - private static boolean isRemoteDirectory(long directoryId) { - return VERSION.SDK_INT >= VERSION_CODES.N - ? ContactsContract.Directory.isRemoteDirectoryId(directoryId) - : (directoryId != ContactsContract.Directory.DEFAULT - && directoryId != ContactsContract.Directory.LOCAL_INVISIBLE - // Directory.ENTERPRISE_DEFAULT is the default work profile directory for locally stored - // contacts - && directoryId != ContactsContract.Directory.ENTERPRISE_DEFAULT - && directoryId != ContactsContract.Directory.ENTERPRISE_LOCAL_INVISIBLE); - } - private MatrixCursor createMatrixCursorFilteringNullNumbers(Cursor cursor) { if (cursor == null) { return null; -- cgit v1.2.3 From 39009b4ad73d5017295b30fb18a77224195f06af Mon Sep 17 00:00:00 2001 From: zachh Date: Mon, 12 Feb 2018 16:49:00 -0800 Subject: Mark calls as read in new call log. Playing with the existing app, the missed call becomes unbolded when: 1) Expanding the row. The closest analog of this is in the new UI is opening the bottom sheet, I've done that. 2) Swiping away from the call history tab. This can't be done in NewCallLogFragment because it doesn't know if the user is selected a new tab or pressed Home. So, I implemented this in NewMainActivityPeer. 3) After viewing the call log for 3(ish) seconds and leaving the activity pressing Home/Back. This is best done in NewCallLogFragment#onResume since MainActivity doesn't always know when the fragment is being displayed (it could be done after the user comes back to the app after pressing Home for example). Note that the notification is also removed in all of these cases. Also note that dismissing the notification makes the call unbolded (but this case already appears to be handled via the system call log content observer). Also, as part of writing tests for this, I made TestCallLogProvider more realistic. Bug: 70989622 Test: manual PiperOrigin-RevId: 185457438 Change-Id: Ib360d3bc73083bd1a018ed98e2b7d9a69fb7fafb --- .../app/calllog/CallLogNotificationsService.java | 13 +- .../dialer/app/calllog/MissedCallNotifier.java | 59 +++----- .../android/dialer/calllog/CallLogComponent.java | 2 + .../android/dialer/calllog/ClearMissedCalls.java | 166 +++++++++++++++++++++ .../dialer/calllog/ui/NewCallLogFragment.java | 31 +++- .../dialer/calllog/ui/menu/NewCallLogMenu.java | 29 +++- .../common/concurrent/DefaultFutureCallback.java | 44 ++++++ .../dialer/common/concurrent/UiThreadExecutor.java | 2 +- .../dialer/main/impl/NewMainActivityPeer.java | 28 +++- .../dialer/main/impl/bottomnav/BottomNavBar.java | 1 + .../missedcalls/MissedCallConstants.java | 36 +++++ .../MissedCallNotificationCanceller.java | 48 ++++++ .../missedcalls/MissedCallNotificationTags.java | 29 ++++ .../simulator/impl/SimulatorSimCallManager.java | 19 ++- 14 files changed, 436 insertions(+), 71 deletions(-) create mode 100644 java/com/android/dialer/calllog/ClearMissedCalls.java create mode 100644 java/com/android/dialer/common/concurrent/DefaultFutureCallback.java create mode 100644 java/com/android/dialer/notification/missedcalls/MissedCallConstants.java create mode 100644 java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java create mode 100644 java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java index 5949141f1..10e30ff72 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -31,6 +31,7 @@ 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.notification.missedcalls.MissedCallNotificationCanceller; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; @@ -87,14 +88,6 @@ public class CallLogNotificationsService extends IntentService { context.startService(serviceIntent); } - public static void markSingleNewVoicemailAsOld(Context context, @Nullable Uri voicemailUri) { - LogUtil.enterBlock("CallLogNotificationsService.markSingleNewVoicemailAsOld"); - Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); - 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) @@ -175,7 +168,7 @@ public class CallLogNotificationsService extends IntentService { case ACTION_CANCEL_SINGLE_MISSED_CALL: Uri callUri = intent.getData(); CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(this, callUri); - MissedCallNotifier.cancelSingleMissedCallNotification(this, callUri); + MissedCallNotificationCanceller.cancelSingle(this, callUri); TelecomUtil.cancelMissedCallsNotification(this); break; case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: @@ -196,7 +189,7 @@ public class CallLogNotificationsService extends IntentService { LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCallsBackground"); Assert.isWorkerThread(); CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context); - MissedCallNotifier.cancelAllMissedCallNotifications(context); + MissedCallNotificationCanceller.cancelAll(context); TelecomUtil.cancelMissedCallsNotification(context); } diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java index 417f8f0f9..14bbdfa56 100644 --- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -58,7 +58,9 @@ import com.android.dialer.duo.DuoConstants; import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher; import com.android.dialer.notification.DialerNotificationManager; import com.android.dialer.notification.NotificationChannelId; -import com.android.dialer.notification.NotificationManagerUtils; +import com.android.dialer.notification.missedcalls.MissedCallConstants; +import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller; +import com.android.dialer.notification.missedcalls.MissedCallNotificationTags; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.precall.PreCall; @@ -71,18 +73,6 @@ import java.util.Set; /** Creates a notification for calls that the user missed (neither answered nor rejected). */ public class MissedCallNotifier implements Worker, Void> { - /** 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; @@ -126,7 +116,7 @@ public class MissedCallNotifier implements Worker, Void> { if ((newCalls != null && newCalls.isEmpty()) || count == 0) { // No calls to notify about: clear the notification. CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context); - cancelAllMissedCallNotifications(context); + MissedCallNotificationCanceller.cancelAll(context); return; } @@ -226,7 +216,10 @@ public class MissedCallNotifier implements Worker, Void> { LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); DialerNotificationManager.notify( - context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, notification); + context, + MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG, + MissedCallConstants.NOTIFICATION_ID, + notification); if (useCallList) { // Do not repost active notifications to prevent erasing post call notes. @@ -240,7 +233,10 @@ public class MissedCallNotifier implements Worker, Void> { String callTag = getNotificationTagForCall(call); if (!activeTags.contains(callTag)) { DialerNotificationManager.notify( - context, callTag, NOTIFICATION_ID, getNotificationForCall(call, null)); + context, + callTag, + MissedCallConstants.NOTIFICATION_ID, + getNotificationForCall(call, null)); } } } @@ -286,29 +282,8 @@ public class MissedCallNotifier implements Worker, Void> { } } - 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; + return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri); } @WorkerThread @@ -324,7 +299,7 @@ public class MissedCallNotifier implements Worker, Void> { DialerNotificationManager.notify( context, getNotificationTagForCall(call), - NOTIFICATION_ID, + MissedCallConstants.NOTIFICATION_ID, getNotificationForCall(call, note)); return; } @@ -408,7 +383,7 @@ public class MissedCallNotifier implements Worker, Void> { private Notification.Builder createNotificationBuilder() { return new Notification.Builder(context) - .setGroup(GROUP_KEY) + .setGroup(MissedCallConstants.GROUP_KEY) .setSmallIcon(android.R.drawable.stat_notify_missed_call) .setColor(context.getResources().getColor(R.color.dialer_theme_color, null)) .setAutoCancel(true) @@ -437,7 +412,7 @@ public class MissedCallNotifier implements Worker, Void> { public void callBackFromMissedCall(String number, Uri callUri) { closeSystemDialogs(context); CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); - cancelSingleMissedCallNotification(context, callUri); + MissedCallNotificationCanceller.cancelSingle(context, callUri); DialerUtils.startActivityWithErrorToast( context, PreCall.getIntent( @@ -450,7 +425,7 @@ public class MissedCallNotifier implements Worker, Void> { public void sendSmsFromMissedCall(String number, Uri callUri) { closeSystemDialogs(context); CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); - cancelSingleMissedCallNotification(context, callUri); + MissedCallNotificationCanceller.cancelSingle(context, callUri); DialerUtils.startActivityWithErrorToast( context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } diff --git a/java/com/android/dialer/calllog/CallLogComponent.java b/java/com/android/dialer/calllog/CallLogComponent.java index c7db2a1b8..bb5bfee2a 100644 --- a/java/com/android/dialer/calllog/CallLogComponent.java +++ b/java/com/android/dialer/calllog/CallLogComponent.java @@ -27,6 +27,8 @@ public abstract class CallLogComponent { public abstract RefreshAnnotatedCallLogWorker getRefreshAnnotatedCallLogWorker(); + public abstract ClearMissedCalls getClearMissedCalls(); + public static CallLogComponent get(Context context) { return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) .callLogComponent(); diff --git a/java/com/android/dialer/calllog/ClearMissedCalls.java b/java/com/android/dialer/calllog/ClearMissedCalls.java new file mode 100644 index 000000000..d216e7b88 --- /dev/null +++ b/java/com/android/dialer/calllog/ClearMissedCalls.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.calllog; + +import android.annotation.SuppressLint; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.provider.CallLog.Calls; +import android.support.v4.os.UserManagerCompat; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.Ui; +import com.android.dialer.common.database.Selection; +import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller; +import com.android.dialer.util.PermissionsUtil; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Collection; +import javax.inject.Inject; + +/** + * Clears missed calls. This includes cancelling notifications and updating the "NEW" status in the + * system call log. + */ +public final class ClearMissedCalls { + + private final Context appContext; + private final ListeningExecutorService backgroundExecutor; + private final ListeningExecutorService uiThreadExecutor; + + @Inject + ClearMissedCalls( + @ApplicationContext Context appContext, + @BackgroundExecutor ListeningExecutorService backgroundExecutor, + @Ui ListeningExecutorService uiThreadExecutor) { + this.appContext = appContext; + this.backgroundExecutor = backgroundExecutor; + this.uiThreadExecutor = uiThreadExecutor; + } + + /** + * Cancels all missed call notifications and marks all "new" missed calls in the system call log + * as "not new". + */ + public ListenableFuture clearAll() { + ListenableFuture markNewFuture = markNotNew(ImmutableSet.of()); + ListenableFuture cancelNotificationsFuture = + uiThreadExecutor.submit( + () -> { + MissedCallNotificationCanceller.cancelAll(appContext); + return null; + }); + + // Note on this usage of whenAllComplete: + // -The returned future completes when all sub-futures complete (whether they fail or not) + // -The returned future fails if any sub-future fails + return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture) + .call( + () -> { + // Calling get() is necessary to propagate failures. + markNewFuture.get(); + cancelNotificationsFuture.get(); + return null; + }, + MoreExecutors.directExecutor()); + } + + /** + * For the provided set of IDs from the system call log, cancels their missed call notifications + * and marks them "not new". + * + * @param ids IDs from the system call log (see {@link Calls#_ID}}. + */ + public ListenableFuture clearBySystemCallLogId(Collection ids) { + ListenableFuture markNewFuture = markNotNew(ids); + ListenableFuture cancelNotificationsFuture = + uiThreadExecutor.submit( + () -> { + for (long id : ids) { + Uri callUri = Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(id)).build(); + MissedCallNotificationCanceller.cancelSingle(appContext, callUri); + } + return null; + }); + + // Note on this usage of whenAllComplete: + // -The returned future completes when all sub-futures complete (whether they fail or not) + // -The returned future fails if any sub-future fails + return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture) + .call( + () -> { + // Calling get() is necessary to propagate failures. + markNewFuture.get(); + cancelNotificationsFuture.get(); + return null; + }, + MoreExecutors.directExecutor()); + } + + /** + * Marks all provided system call log IDs as not new, or if the provided collection is empty, + * marks all calls as not new. + */ + @SuppressLint("MissingPermission") + private ListenableFuture markNotNew(Collection ids) { + return backgroundExecutor.submit( + () -> { + if (!UserManagerCompat.isUserUnlocked(appContext)) { + LogUtil.e("ClearMissedCalls.markNotNew", "locked"); + return null; + } + if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) { + LogUtil.e("ClearMissedCalls.markNotNew", "no permission"); + return null; + } + + ContentValues values = new ContentValues(); + values.put(Calls.NEW, 0); + + Selection.Builder selectionBuilder = + Selection.builder() + .and(Selection.column(Calls.NEW).is("=", 1)) + .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE)); + if (!ids.isEmpty()) { + selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids))); + } + Selection selection = selectionBuilder.build(); + appContext + .getContentResolver() + .update( + Calls.CONTENT_URI, + values, + selection.getSelection(), + selection.getSelectionArgs()); + return null; + }); + } + + private static String[] toStrings(Collection longs) { + String[] strings = new String[longs.size()]; + int i = 0; + for (long value : longs) { + strings[i++] = Long.toString(value); + } + return strings; + } +} diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index 10f75ef07..5e676f072 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -18,6 +18,7 @@ package com.android.dialer.calllog.ui; import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; @@ -31,10 +32,14 @@ import com.android.dialer.calllog.CallLogFramework; import com.android.dialer.calllog.CallLogFramework.CallLogUi; import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DefaultFutureCallback; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.common.concurrent.UiListener; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.TimeUnit; /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ public final class NewCallLogFragment extends Fragment @@ -46,13 +51,19 @@ public final class NewCallLogFragment extends Fragment * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 * call log entries. */ - private static final long WAIT_MILLIS = 100L; + private static final long REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L; + + @VisibleForTesting + static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3); private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; private UiListener refreshAnnotatedCallLogListener; private RecyclerView recyclerView; @Nullable private Runnable refreshAnnotatedCallLogRunnable; + private boolean shouldMarkCallsRead = false; + private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true; + public NewCallLogFragment() { LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment"); } @@ -103,6 +114,13 @@ public final class NewCallLogFragment extends Fragment ((NewCallLogAdapter) recyclerView.getAdapter()).clearCache(); recyclerView.getAdapter().notifyDataSetChanged(); } + + // We shouldn't mark the calls as read immediately when the 3 second timer expires because we + // don't want to disrupt the UI; instead we set a bit indicating to mark them read when the user + // leaves the fragment (in onPause). + shouldMarkCallsRead = false; + ThreadUtil.getUiThreadHandler() + .postDelayed(setShouldMarkCallsReadTrue, MARK_ALL_CALLS_READ_WAIT_MILLIS); } @Override @@ -113,9 +131,17 @@ public final class NewCallLogFragment extends Fragment // This is pending work that we don't actually need to follow through with. ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + ThreadUtil.getUiThreadHandler().removeCallbacks(setShouldMarkCallsReadTrue); CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); callLogFramework.detachUi(); + + if (shouldMarkCallsRead) { + Futures.addCallback( + CallLogComponent.get(getContext()).getClearMissedCalls().clearAll(), + new DefaultFutureCallback<>(), + MoreExecutors.directExecutor()); + } } @Override @@ -159,7 +185,8 @@ public final class NewCallLogFragment extends Fragment throw new RuntimeException(throwable); }); }; - ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS); + ThreadUtil.getUiThreadHandler() + .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS); } @Override diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java index 81c05135f..02724e628 100644 --- a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java +++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java @@ -17,10 +17,15 @@ package com.android.dialer.calllog.ui.menu; import android.content.Context; +import android.provider.CallLog.Calls; import android.view.View; +import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.common.concurrent.DefaultFutureCallback; import com.android.dialer.contactactions.ContactActionBottomSheet; import com.android.dialer.glidephotomanager.GlidePhotoManager; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; /** Handles configuration of the bottom sheet menus for call log entries. */ public final class NewCallLogMenu { @@ -28,11 +33,23 @@ public final class NewCallLogMenu { /** Creates and returns the OnClickListener which opens the menu for the provided row. */ public static View.OnClickListener createOnClickListener( Context context, CoalescedRow row, GlidePhotoManager glidePhotoManager) { - return (view) -> - ContactActionBottomSheet.show( - context, - PrimaryAction.fromRow(context, row), - Modules.fromRow(context, row), - glidePhotoManager); + return view -> { + ContactActionBottomSheet.show( + context, + PrimaryAction.fromRow(context, row), + Modules.fromRow(context, row), + glidePhotoManager); + + // If the user opens the bottom sheet for a new call, clear the notifications and make the row + // not bold immediately. To do this, mark all of the calls in group as not new. + if (row.isNew() && row.callType() == Calls.MISSED_TYPE) { + Futures.addCallback( + CallLogComponent.get(context) + .getClearMissedCalls() + .clearBySystemCallLogId(row.coalescedIds().getCoalescedIdList()), + new DefaultFutureCallback<>(), + MoreExecutors.directExecutor()); + } + }; } } diff --git a/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java b/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java new file mode 100644 index 000000000..93ad0faf0 --- /dev/null +++ b/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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.common.concurrent; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.MoreExecutors; + +/** + * Returns a {@link FutureCallback} which does nothing on success and crashes the application on + * failure. + * + *

You generally shouldn't use this for futures which should be tied to UI, for those use {@link + * UiListener}. + * + *

Can be safely used with {@link MoreExecutors#directExecutor()} + */ +public final class DefaultFutureCallback implements FutureCallback { + + @Override + public void onSuccess(T unused) {} + + @Override + public void onFailure(Throwable throwable) { + ThreadUtil.getUiThreadHandler() + .post( + () -> { + throw new RuntimeException(throwable); + }); + } +} diff --git a/java/com/android/dialer/common/concurrent/UiThreadExecutor.java b/java/com/android/dialer/common/concurrent/UiThreadExecutor.java index ec51ed334..8378d69ce 100644 --- a/java/com/android/dialer/common/concurrent/UiThreadExecutor.java +++ b/java/com/android/dialer/common/concurrent/UiThreadExecutor.java @@ -31,7 +31,7 @@ import javax.inject.Inject; public class UiThreadExecutor extends AbstractListeningExecutorService { @Inject - UiThreadExecutor() {} + public UiThreadExecutor() {} @Override public void shutdown() { diff --git a/java/com/android/dialer/main/impl/NewMainActivityPeer.java b/java/com/android/dialer/main/impl/NewMainActivityPeer.java index 789648928..0a85667a1 100644 --- a/java/com/android/dialer/main/impl/NewMainActivityPeer.java +++ b/java/com/android/dialer/main/impl/NewMainActivityPeer.java @@ -16,16 +16,22 @@ package com.android.dialer.main.impl; +import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; +import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.ui.NewCallLogFragment; +import com.android.dialer.common.concurrent.DefaultFutureCallback; import com.android.dialer.main.MainActivityPeer; import com.android.dialer.main.impl.bottomnav.BottomNavBar; import com.android.dialer.main.impl.bottomnav.BottomNavBar.OnBottomNavTabSelectedListener; import com.android.dialer.main.impl.bottomnav.BottomNavBar.TabIndex; import com.android.dialer.voicemail.listui.NewVoicemailFragment; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; /** MainActivityPeer that implements the new fragments. */ public class NewMainActivityPeer implements MainActivityPeer { @@ -40,7 +46,8 @@ public class NewMainActivityPeer implements MainActivityPeer { public void onActivityCreate(Bundle saveInstanceState) { mainActivity.setContentView(R.layout.main_activity); MainBottomNavBarBottomNavTabListener bottomNavBarBottomNavTabListener = - new MainBottomNavBarBottomNavTabListener(mainActivity.getSupportFragmentManager()); + new MainBottomNavBarBottomNavTabListener( + mainActivity.getSupportFragmentManager(), mainActivity.getApplicationContext()); BottomNavBar bottomNav = mainActivity.findViewById(R.id.bottom_nav_bar); bottomNav.addOnTabSelectedListener(bottomNavBarBottomNavTabListener); bottomNav.selectTab(TabIndex.SPEED_DIAL); @@ -77,9 +84,12 @@ public class NewMainActivityPeer implements MainActivityPeer { private static final String VOICEMAIL_TAG = "voicemail"; private final FragmentManager supportFragmentManager; + private final Context appContext; - private MainBottomNavBarBottomNavTabListener(FragmentManager supportFragmentManager) { + private MainBottomNavBarBottomNavTabListener( + FragmentManager supportFragmentManager, Context appContext) { this.supportFragmentManager = supportFragmentManager; + this.appContext = appContext; } @Override @@ -126,8 +136,18 @@ public class NewMainActivityPeer implements MainActivityPeer { private void hideAllFragments() { FragmentTransaction supportTransaction = supportFragmentManager.beginTransaction(); - if (supportFragmentManager.findFragmentByTag(CALL_LOG_TAG) != null) { - supportTransaction.hide(supportFragmentManager.findFragmentByTag(CALL_LOG_TAG)); + Fragment callLogFragment = supportFragmentManager.findFragmentByTag(CALL_LOG_TAG); + if (callLogFragment != null) { + if (callLogFragment.isVisible()) { + // If the user taps any bottom nav button and the call log is showing, immediately cancel + // missed calls (unbold them and clear their notifications). + Futures.addCallback( + // TODO(zachh): Use dagger to create Peer and MainBottomNavBarBottomNavTabListener. + CallLogComponent.get(appContext).getClearMissedCalls().clearAll(), + new DefaultFutureCallback<>(), + MoreExecutors.directExecutor()); + } + supportTransaction.hide(callLogFragment); } if (supportFragmentManager.findFragmentByTag(VOICEMAIL_TAG) != null) { supportTransaction.hide(supportFragmentManager.findFragmentByTag(VOICEMAIL_TAG)); diff --git a/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java b/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java index a580aee68..2945e39a9 100644 --- a/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java +++ b/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java @@ -162,6 +162,7 @@ public final class BottomNavBar extends LinearLayout { } } + @TabIndex public int getSelectedTab() { return selectedTab; } diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java b/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java new file mode 100644 index 000000000..8553ea58e --- /dev/null +++ b/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.notification.missedcalls; + +/** Constants related to missed call notifications. */ +public final class MissedCallConstants { + + /** Prefix used to generate a unique tag for each missed call notification. */ + public static final String NOTIFICATION_TAG_PREFIX = "MissedCall_"; + + /** Common ID for all missed call notifications. */ + public static final int NOTIFICATION_ID = 1; + + /** Tag for the group summary notification. */ + public 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. + */ + public static final String GROUP_KEY = "MissedCallGroup"; +} diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java new file mode 100644 index 000000000..8798c2127 --- /dev/null +++ b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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.notification.missedcalls; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.DialerNotificationManager; +import com.android.dialer.notification.NotificationManagerUtils; + +/** Cancels missed calls notifications. */ +public final class MissedCallNotificationCanceller { + + /** Cancels all missed call notifications. */ + public static void cancelAll(@NonNull Context context) { + NotificationManagerUtils.cancelAllInGroup(context, MissedCallConstants.GROUP_KEY); + } + + /** Cancels a missed call notification for a single call. */ + public static void cancelSingle(@NonNull Context context, @Nullable Uri callUri) { + if (callUri == null) { + LogUtil.e( + "MissedCallNotificationCanceller.cancelSingle", + "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, + MissedCallNotificationTags.getNotificationTagForCallUri(callUri), + MissedCallConstants.NOTIFICATION_ID); + } +} diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java new file mode 100644 index 000000000..64f28eeec --- /dev/null +++ b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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.notification.missedcalls; + +import android.net.Uri; +import android.support.annotation.NonNull; + +/** Static methods related to missed call notification tags. */ +public final class MissedCallNotificationTags { + + /** Gets the notification tag for a single call. */ + public static String getNotificationTagForCallUri(@NonNull Uri callUri) { + return MissedCallConstants.NOTIFICATION_TAG_PREFIX + callUri; + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java index 00899fd69..f28393c0c 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java +++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java @@ -29,6 +29,7 @@ import android.telecom.TelecomManager; import android.telephony.TelephonyManager; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.strictmode.StrictModeUtils; import java.util.Arrays; import java.util.List; import java.util.Random; @@ -53,17 +54,23 @@ public class SimulatorSimCallManager { static void register(@NonNull Context context) { LogUtil.enterBlock("SimulatorSimCallManager.register"); Assert.isNotNull(context); - TelecomManager telecomManager = context.getSystemService(TelecomManager.class); - telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context)); - telecomManager.registerPhoneAccount(buildVideoProviderAccount(context)); + StrictModeUtils.bypass( + () -> { + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context)); + telecomManager.registerPhoneAccount(buildVideoProviderAccount(context)); + }); } static void unregister(@NonNull Context context) { LogUtil.enterBlock("SimulatorSimCallManager.unregister"); Assert.isNotNull(context); - TelecomManager telecomManager = context.getSystemService(TelecomManager.class); - telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context)); - telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context)); + StrictModeUtils.bypass( + () -> { + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context)); + telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context)); + }); } @NonNull -- cgit v1.2.3