diff options
74 files changed, 1117 insertions, 328 deletions
diff --git a/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml index 3a85b7dd3..12db78d99 100644 --- a/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml +++ b/assets/quantum/res/drawable/quantum_ic_arrow_back_vd_theme_24.xml @@ -18,7 +18,8 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?attr/colorControlNormal"> + android:tint="?attr/colorControlNormal" + android:autoMirrored="true"> <path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/> diff --git a/java/com/android/bubble/Bubble.java b/java/com/android/bubble/Bubble.java index e192e06f4..1b853cf49 100644 --- a/java/com/android/bubble/Bubble.java +++ b/java/com/android/bubble/Bubble.java @@ -39,6 +39,9 @@ public interface Bubble { /** Returns whether the bubble is currently visible */ boolean isVisible(); + /** Returns whether the bubble is currently dismissed */ + boolean isDismissed(); + /** * Set the info for this Bubble to display * diff --git a/java/com/android/bubble/stub/BubbleStub.java b/java/com/android/bubble/stub/BubbleStub.java index 267f33f31..2aa55a337 100644 --- a/java/com/android/bubble/stub/BubbleStub.java +++ b/java/com/android/bubble/stub/BubbleStub.java @@ -40,6 +40,11 @@ public class BubbleStub implements Bubble { } @Override + public boolean isDismissed() { + return false; + } + + @Override public void setBubbleInfo(@NonNull BubbleInfo bubbleInfo) {} @Override diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index 9057cd9f9..c819fecba 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -1130,7 +1130,7 @@ public class DialtactsActivity extends TransactionSafeActivity NewSearchFragment fragment = (NewSearchFragment) getFragmentManager().findFragmentByTag(tag); if (fragment == null) { - fragment = NewSearchFragment.newInstance(!isDialpadShown()); + fragment = NewSearchFragment.newInstance(); transaction.add(R.id.dialtacts_frame, fragment, tag); } else { transaction.show(fragment); diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java index 10e30ff72..d84bd425b 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -150,12 +150,12 @@ public class CallLogNotificationsService extends IntentService { LogUtil.i("CallLogNotificationsService.onHandleIntent", "action: " + action); switch (action) { case ACTION_MARK_ALL_NEW_VOICEMAILS_AS_OLD: - VoicemailQueryHandler.markAllNewVoicemailsAsRead(this); + VoicemailQueryHandler.markAllNewVoicemailsAsOld(this); VisualVoicemailNotifier.cancelAllVoicemailNotifications(this); break; case ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD: Uri voicemailUri = intent.getData(); - VoicemailQueryHandler.markSingleNewVoicemailAsRead(this, voicemailUri); + VoicemailQueryHandler.markSingleNewVoicemailAsOld(this, voicemailUri); VisualVoicemailNotifier.cancelSingleVoicemailNotification(this, voicemailUri); break; case ACTION_LEGACY_VOICEMAIL_DISMISSED: diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java index 096488a39..680424a78 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -356,6 +356,9 @@ public class PhoneCallDetailsHelper new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int button) { + VoicemailComponent.get(context) + .getVoicemailClient() + .setVoicemailDonationEnabled(context, details.accountHandle, false); dialog.cancel(); recordPromoShown(context); ratingView.setVisibility(View.GONE); diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java index b353b3abc..bae30fa7b 100644 --- a/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java +++ b/java/com/android/dialer/app/calllog/VisualVoicemailUpdateTask.java @@ -212,6 +212,10 @@ class VisualVoicemailUpdateTask implements Worker<VisualVoicemailUpdateTask.Inpu "found voicemail from spam number, suppressing notification"); Logger.get(context) .logImpression(DialerImpression.Type.INCOMING_VOICEMAIL_AUTO_BLOCKED_AS_SPAM); + if (newCall.voicemailUri != null) { + // Mark auto blocked voicemail as old so that we don't process it again. + VoicemailQueryHandler.markSingleNewVoicemailAsOld(context, newCall.voicemailUri); + } } else { result.add(newCall); } diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java index 169d0fd35..5d8144ca9 100644 --- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java +++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java @@ -42,7 +42,7 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { } @WorkerThread - public static void markAllNewVoicemailsAsRead(final @NonNull Context context) { + public static void markAllNewVoicemailsAsOld(final @NonNull Context context) { ThreadUtil.postOnUiThread( () -> { new VoicemailQueryHandler(context.getContentResolver()) @@ -51,10 +51,10 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { } @WorkerThread - public static void markSingleNewVoicemailAsRead( + public static void markSingleNewVoicemailAsOld( final @NonNull Context context, final Uri voicemailUri) { if (voicemailUri == null) { - LogUtil.e("VoicemailQueryHandler.markSingleNewVoicemailAsRead", "voicemail URI is null"); + LogUtil.e("VoicemailQueryHandler.markSingleNewVoicemailAsOld", "voicemail URI is null"); return; } ThreadUtil.postOnUiThread( diff --git a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java index 621287f6c..de974cbec 100644 --- a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java +++ b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java @@ -41,6 +41,7 @@ import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar * providing undo functionality. */ +@Deprecated public class BlockNumberDialogFragment extends DialogFragment { private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog"; diff --git a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java index 6e9fe1315..8a57f29e7 100644 --- a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java +++ b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java @@ -33,6 +33,7 @@ import com.android.dialer.common.concurrent.DialerExecutorFactory; * android.provider.BlockedNumberContract} blocking. In order for this to happen, the user cannot * have any numbers that are blocked in the Dialer solution. */ +@Deprecated public class BlockedNumbersAutoMigrator { static final String HAS_CHECKED_AUTO_MIGRATE_KEY = "checkedAutoMigrate"; diff --git a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java index 61ebf2f56..101a04b2f 100644 --- a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java +++ b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java @@ -36,6 +36,7 @@ import java.util.Objects; * {@link android.provider.BlockedNumberContract} blocking. */ @TargetApi(VERSION_CODES.N) +@Deprecated public class BlockedNumbersMigrator { private final Context context; diff --git a/java/com/android/dialer/blocking/Blocking.java b/java/com/android/dialer/blocking/Blocking.java new file mode 100644 index 000000000..e86d0a6ac --- /dev/null +++ b/java/com/android/dialer/blocking/Blocking.java @@ -0,0 +1,117 @@ +/* + * 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.blocking; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.BlockedNumberContract.BlockedNumbers; +import android.support.annotation.Nullable; +import android.telephony.PhoneNumberUtils; +import com.android.dialer.common.database.Selection; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; + +/** Blocks and unblocks number. */ +public final class Blocking { + + private Blocking() {} + + /** + * Thrown when blocking cannot be performed because dialer is not the default dialer, or the + * current user is not a primary user. + * + * <p>Blocking is only allowed on the primary user (the first user added). Primary user cannot be + * easily checked because {@link + * android.provider.BlockedNumberContract#canCurrentUserBlockNumbers(Context)} is a slow IPC, and + * UserManager.isPrimaryUser() is a system API. Since secondary users are rare cases this class + * choose to ignore the check and let callers handle the failure later. + */ + public static final class BlockingFailedException extends Exception { + BlockingFailedException(Throwable cause) { + super(cause); + } + } + + /** + * Block a number. + * + * @param countryIso the current location used to guess the country code of the number if not + * available. If {@code null} and {@code number} does not have a country code, only the + * original number will be blocked. + * @throws BlockingFailedException in the returned future if the operation failed. + */ + public static ListenableFuture<Void> block( + Context context, + ListeningExecutorService executorService, + String number, + @Nullable String countryIso) { + return executorService.submit( + () -> { + ContentValues values = new ContentValues(); + values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number); + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + if (e164Number != null) { + values.put(BlockedNumbers.COLUMN_E164_NUMBER, e164Number); + } + try { + context.getContentResolver().insert(BlockedNumbers.CONTENT_URI, values); + } catch (SecurityException e) { + throw new BlockingFailedException(e); + } + return null; + }); + } + + /** + * Unblock a number. + * + * @param countryIso the current location used to guess the country code of the number if not + * available. If {@code null} and {@code number} does not have a country code, only the + * original number will be unblocked. + * @throws BlockingFailedException in the returned future if the operation failed. + */ + public static ListenableFuture<Void> unblock( + Context context, + ListeningExecutorService executorService, + String number, + @Nullable String countryIso) { + return executorService.submit( + () -> { + Selection selection = + Selection.column(BlockedNumbers.COLUMN_ORIGINAL_NUMBER).is("=", number); + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + if (e164Number != null) { + selection = + selection + .buildUpon() + .or(Selection.column(BlockedNumbers.COLUMN_E164_NUMBER).is("=", e164Number)) + .build(); + } + try { + context + .getContentResolver() + .delete( + BlockedNumbers.CONTENT_URI, + selection.getSelection(), + selection.getSelectionArgs()); + } catch (SecurityException e) { + throw new BlockingFailedException(e); + } + return null; + }); + } +} diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java index 8be479c99..b41759259 100644 --- a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java +++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** TODO(calderwoodra): documentation */ +@Deprecated public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { public static final int INVALID_ID = -1; diff --git a/java/com/android/dialer/blocking/FilteredNumberCompat.java b/java/com/android/dialer/blocking/FilteredNumberCompat.java index b0af45c97..d263d212c 100644 --- a/java/com/android/dialer/blocking/FilteredNumberCompat.java +++ b/java/com/android/dialer/blocking/FilteredNumberCompat.java @@ -48,6 +48,7 @@ import java.util.Objects; * referencing columns from either contract class in situations where both blocking solutions may be * used. */ +@Deprecated public class FilteredNumberCompat { private static Boolean canAttemptBlockOperationsForTest; diff --git a/java/com/android/dialer/blocking/FilteredNumberProvider.java b/java/com/android/dialer/blocking/FilteredNumberProvider.java index 3fad4e24f..547892b41 100644 --- a/java/com/android/dialer/blocking/FilteredNumberProvider.java +++ b/java/com/android/dialer/blocking/FilteredNumberProvider.java @@ -34,6 +34,7 @@ import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; import com.android.dialer.location.GeoUtil; /** Filtered number content provider. */ +@Deprecated public class FilteredNumberProvider extends ContentProvider { private static final int FILTERED_NUMBERS_TABLE = 1; diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java index 6433355fd..d839ef5da 100644 --- a/java/com/android/dialer/blocking/FilteredNumbersUtil.java +++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java @@ -42,6 +42,7 @@ import com.android.dialer.util.PermissionsUtil; import java.util.concurrent.TimeUnit; /** Utility to help with tasks related to filtered numbers. */ +@Deprecated public class FilteredNumbersUtil { public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking"; diff --git a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java index 9b416ff5e..9a3b647d2 100644 --- a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java +++ b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java @@ -30,6 +30,7 @@ import java.util.Objects; * Dialog fragment shown to users when they need to migrate to use {@link * android.provider.BlockedNumberContract} for blocking. */ +@Deprecated public class MigrateBlockedNumbersDialogFragment extends DialogFragment { private BlockedNumbersMigrator blockedNumbersMigrator; diff --git a/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java b/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java index cc307b6b9..fd26ab537 100644 --- a/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java +++ b/java/com/android/dialer/blockreportspam/ShowBlockReportSpamDialogReceiver.java @@ -21,8 +21,9 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.support.annotation.Nullable; -import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import android.widget.Toast; +import com.android.dialer.blocking.Blocking; +import com.android.dialer.blocking.Blocking.BlockingFailedException; import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForBlockingNumber; import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForBlockingNumberAndOptionallyReportingAsSpam; import com.android.dialer.blockreportspam.BlockReportSpamDialogs.DialogFragmentForReportingNotSpam; @@ -31,15 +32,16 @@ import com.android.dialer.blockreportspam.BlockReportSpamDialogs.OnConfirmListen import com.android.dialer.blockreportspam.BlockReportSpamDialogs.OnSpamDialogClickListener; 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.logging.DialerImpression; +import com.android.dialer.logging.DialerImpression.Type; import com.android.dialer.logging.Logger; import com.android.dialer.protos.ProtoParsers; import com.android.dialer.spam.Spam; import com.android.dialer.spam.SpamComponent; import com.android.dialer.spam.SpamSettings; -import com.google.auto.value.AutoValue; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; /** * A {@link BroadcastReceiver} that shows an appropriate dialog upon receiving notifications from @@ -106,8 +108,6 @@ public final class ShowBlockReportSpamDialogReceiver extends BroadcastReceiver { Spam spam = SpamComponent.get(context).spam(); SpamSettings spamSettings = SpamComponent.get(context).spamSettings(); - FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler = - new FilteredNumberAsyncQueryHandler(context); // Set up the positive listener for the dialog. OnSpamDialogClickListener onSpamDialogClickListener = @@ -132,12 +132,7 @@ public final class ShowBlockReportSpamDialogReceiver extends BroadcastReceiver { dialogInfo.getContactSource()); } - filteredNumberAsyncQueryHandler.blockNumber( - unused -> - Logger.get(context) - .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER), - dialogInfo.getNormalizedNumber(), - dialogInfo.getCountryIso()); + blockNumber(context, dialogInfo); }; // Create and show the dialog. @@ -157,19 +152,11 @@ public final class ShowBlockReportSpamDialogReceiver extends BroadcastReceiver { ProtoParsers.getTrusted( intent, EXTRA_DIALOG_INFO, BlockReportSpamDialogInfo.getDefaultInstance()); - FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler = - new FilteredNumberAsyncQueryHandler(context); - // Set up the positive listener for the dialog. OnConfirmListener onConfirmListener = () -> { LogUtil.i("ShowBlockReportSpamDialogReceiver.showDialogToBlockNumber", "block number"); - filteredNumberAsyncQueryHandler.blockNumber( - unused -> - Logger.get(context) - .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER), - dialogInfo.getNormalizedNumber(), - dialogInfo.getCountryIso()); + blockNumber(context, dialogInfo); }; // Create and show the dialog. @@ -219,46 +206,12 @@ public final class ShowBlockReportSpamDialogReceiver extends BroadcastReceiver { ProtoParsers.getTrusted( intent, EXTRA_DIALOG_INFO, BlockReportSpamDialogInfo.getDefaultInstance()); - FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler = - new FilteredNumberAsyncQueryHandler(context); - // Set up the positive listener for the dialog. OnConfirmListener onConfirmListener = () -> { LogUtil.i("ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber", "confirmed"); - DialerExecutorComponent.get(context) - .dialerExecutorFactory() - .createNonUiTaskBuilder( - new GetIdForBlockedNumberWorker(filteredNumberAsyncQueryHandler)) - .onSuccess( - idForBlockedNumber -> { - LogUtil.i( - "ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber", - "ID for the blocked number retrieved"); - if (idForBlockedNumber == null) { - throw new IllegalStateException("ID for a blocked number is null."); - } - - LogUtil.i( - "ShowBlockReportSpamDialogReceiver.showDialogToUnblockNumber", - "unblocking number"); - filteredNumberAsyncQueryHandler.unblock( - (rows, values) -> - Logger.get(context) - .logImpression(DialerImpression.Type.USER_ACTION_UNBLOCKED_NUMBER), - idForBlockedNumber); - }) - .onFailure( - throwable -> { - throw new RuntimeException(throwable); - }) - .build() - .executeSerial( - NumberInfo.newBuilder() - .setNormalizedNumber(dialogInfo.getNormalizedNumber()) - .setCountryIso(dialogInfo.getCountryIso()) - .build()); + unblockNumber(context, dialogInfo); }; // Create & show the dialog. @@ -267,46 +220,58 @@ public final class ShowBlockReportSpamDialogReceiver extends BroadcastReceiver { .show(fragmentManager, BlockReportSpamDialogs.UNBLOCK_DIALOG_TAG); } - /** A {@link Worker} that retrieves the ID of a blocked number from the database. */ - private static final class GetIdForBlockedNumberWorker implements Worker<NumberInfo, Integer> { - - private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler; - - GetIdForBlockedNumberWorker(FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { - this.filteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; - } - - @Nullable - @Override - public Integer doInBackground(NumberInfo input) throws Throwable { - LogUtil.enterBlock("GetIdForBlockedNumberWorker.doInBackground"); + private static void blockNumber(Context context, BlockReportSpamDialogInfo dialogInfo) { + Logger.get(context).logImpression(Type.USER_ACTION_BLOCKED_NUMBER); + Futures.addCallback( + Blocking.block( + context, + DialerExecutorComponent.get(context).backgroundExecutor(), + dialogInfo.getNormalizedNumber(), + dialogInfo.getCountryIso()), + new FutureCallback<Void>() { + @Override + public void onSuccess(Void unused) { + // Do nothing + } - return filteredNumberAsyncQueryHandler.getBlockedIdSynchronous( - input.getNormalizedNumber(), input.getCountryIso()); - } + @Override + public void onFailure(Throwable throwable) { + if (throwable instanceof BlockingFailedException) { + Logger.get(context).logImpression(Type.USER_ACTION_BLOCK_NUMBER_FAILED); + Toast.makeText(context, R.string.block_number_failed_toast, Toast.LENGTH_LONG).show(); + } else { + throw new RuntimeException(throwable); + } + } + }, + DialerExecutorComponent.get(context).uiExecutor()); } - /** - * Contains information about a number and serves as the input to {@link - * GetIdForBlockedNumberWorker}. - */ - @AutoValue - abstract static class NumberInfo { - static Builder newBuilder() { - return new AutoValue_ShowBlockReportSpamDialogReceiver_NumberInfo.Builder(); - } - - abstract String getNormalizedNumber(); - - abstract String getCountryIso(); - - @AutoValue.Builder - abstract static class Builder { - abstract Builder setNormalizedNumber(String normalizedNumber); - - abstract Builder setCountryIso(String countryIso); + private static void unblockNumber(Context context, BlockReportSpamDialogInfo dialogInfo) { + Logger.get(context).logImpression(Type.USER_ACTION_UNBLOCKED_NUMBER); + Futures.addCallback( + Blocking.unblock( + context, + DialerExecutorComponent.get(context).backgroundExecutor(), + dialogInfo.getNormalizedNumber(), + dialogInfo.getCountryIso()), + new FutureCallback<Void>() { + @Override + public void onSuccess(Void unused) { + // Do nothing + } - abstract NumberInfo build(); - } + @Override + public void onFailure(Throwable throwable) { + if (throwable instanceof BlockingFailedException) { + Logger.get(context).logImpression(Type.USER_ACTION_UNBLOCK_NUMBER_FAILED); + Toast.makeText(context, R.string.unblock_number_failed_toast, Toast.LENGTH_LONG) + .show(); + } else { + throw new RuntimeException(throwable); + } + } + }, + DialerExecutorComponent.get(context).uiExecutor()); } } diff --git a/java/com/android/dialer/blockreportspam/res/values/strings.xml b/java/com/android/dialer/blockreportspam/res/values/strings.xml index aface9268..1537c2353 100644 --- a/java/com/android/dialer/blockreportspam/res/values/strings.xml +++ b/java/com/android/dialer/blockreportspam/res/values/strings.xml @@ -60,4 +60,9 @@ <!-- Label for checkbox in the Alert dialog to allow the user to report the number as spam as well. [CHAR LIMIT=30] --> <string name="checkbox_report_as_spam_action">Report call as spam</string> + <!-- Toast if the user clicked the block button but it failed. [CHAR LIMIT=100] --> + <string name="block_number_failed_toast">Problem blocking this number</string> + + <!-- Toast if the user clicked the unblock button but it failed. [CHAR LIMIT=100] --> + <string name="unblock_number_failed_toast">Problem unblocking this number</string> </resources> diff --git a/java/com/android/dialer/calllog/ClearMissedCalls.java b/java/com/android/dialer/calllog/ClearMissedCalls.java index d216e7b88..78eb80294 100644 --- a/java/com/android/dialer/calllog/ClearMissedCalls.java +++ b/java/com/android/dialer/calllog/ClearMissedCalls.java @@ -38,8 +38,8 @@ 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. + * Clears missed calls. This includes cancelling notifications and updating the "IS_READ" status in + * the system call log. */ public final class ClearMissedCalls { @@ -58,11 +58,11 @@ public final class ClearMissedCalls { } /** - * Cancels all missed call notifications and marks all "new" missed calls in the system call log - * as "not new". + * Cancels all missed call notifications and marks all "unread" missed calls in the system call + * log as "read". */ public ListenableFuture<Void> clearAll() { - ListenableFuture<Void> markNewFuture = markNotNew(ImmutableSet.of()); + ListenableFuture<Void> markReadFuture = markRead(ImmutableSet.of()); ListenableFuture<Void> cancelNotificationsFuture = uiThreadExecutor.submit( () -> { @@ -73,11 +73,11 @@ public final class ClearMissedCalls { // 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) + return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture) .call( () -> { // Calling get() is necessary to propagate failures. - markNewFuture.get(); + markReadFuture.get(); cancelNotificationsFuture.get(); return null; }, @@ -86,12 +86,12 @@ public final class ClearMissedCalls { /** * For the provided set of IDs from the system call log, cancels their missed call notifications - * and marks them "not new". + * and marks them "read". * * @param ids IDs from the system call log (see {@link Calls#_ID}}. */ public ListenableFuture<Void> clearBySystemCallLogId(Collection<Long> ids) { - ListenableFuture<Void> markNewFuture = markNotNew(ids); + ListenableFuture<Void> markReadFuture = markRead(ids); ListenableFuture<Void> cancelNotificationsFuture = uiThreadExecutor.submit( () -> { @@ -105,11 +105,11 @@ public final class ClearMissedCalls { // 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) + return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture) .call( () -> { // Calling get() is necessary to propagate failures. - markNewFuture.get(); + markReadFuture.get(); cancelNotificationsFuture.get(); return null; }, @@ -117,28 +117,28 @@ public final class ClearMissedCalls { } /** - * Marks all provided system call log IDs as not new, or if the provided collection is empty, - * marks all calls as not new. + * Marks all provided system call log IDs as read, or if the provided collection is empty, marks + * all calls as read. */ @SuppressLint("MissingPermission") - private ListenableFuture<Void> markNotNew(Collection<Long> ids) { + private ListenableFuture<Void> markRead(Collection<Long> ids) { return backgroundExecutor.submit( () -> { if (!UserManagerCompat.isUserUnlocked(appContext)) { - LogUtil.e("ClearMissedCalls.markNotNew", "locked"); + LogUtil.e("ClearMissedCalls.markRead", "locked"); return null; } if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) { - LogUtil.e("ClearMissedCalls.markNotNew", "no permission"); + LogUtil.e("ClearMissedCalls.markRead", "no permission"); return null; } ContentValues values = new ContentValues(); - values.put(Calls.NEW, 0); + values.put(Calls.IS_READ, 1); Selection.Builder selectionBuilder = Selection.builder() - .and(Selection.column(Calls.NEW).is("=", 1)) + .and(Selection.column(Calls.IS_READ).is("=", 0)) .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE)); if (!ids.isEmpty()) { selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids))); diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index 8362a81ac..aa4260cba 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -225,6 +225,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { return new RowCombiner(individualRowsSortedByTimestampDesc) .useMostRecentLong(AnnotatedCallLog.TIMESTAMP) .useMostRecentLong(AnnotatedCallLog.NEW) + .useMostRecentLong(AnnotatedCallLog.IS_READ) // Two different DialerPhoneNumbers could be combined if they are different but considered // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most // recent one. diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java index 05a339978..839ba332f 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java @@ -25,6 +25,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.calllogutils.CallLogDates; import com.android.dialer.common.Assert; +import com.android.dialer.logging.Logger; import com.android.dialer.time.Clock; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -53,6 +54,7 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { private final Clock clock; private final RealtimeRowProcessor realtimeRowProcessor; + private final PopCounts popCounts = new PopCounts(); private Cursor cursor; @@ -76,6 +78,7 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { void updateCursor(Cursor updatedCursor) { this.cursor = updatedCursor; this.realtimeRowProcessor.clearCache(); + this.popCounts.reset(); setHeaderPositions(); notifyDataSetChanged(); @@ -85,6 +88,10 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { this.realtimeRowProcessor.clearCache(); } + void logMetrics(Context context) { + Logger.get(context).logAnnotatedCallLogMetrics(popCounts.popped, popCounts.didNotPop); + } + private void setHeaderPositions() { // If there are no rows to display, set all header positions to null. if (!cursor.moveToFirst()) { @@ -138,7 +145,8 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.new_call_log_entry, viewGroup, false), clock, - realtimeRowProcessor); + realtimeRowProcessor, + popCounts); default: throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType); } @@ -207,4 +215,14 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { } return cursor.getCount() + numberOfHeaders; } + + static class PopCounts { + int popped; + int didNotPop; + + private void reset() { + popped = 0; + didNotPop = 0; + } + } } diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index bb1a7303e..0f1c2510a 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -89,6 +89,15 @@ public final class NewCallLogFragment extends Fragment implements LoaderCallback } @Override + public void onStop() { + super.onStop(); + + if (recyclerView.getAdapter() != null) { + ((NewCallLogAdapter) recyclerView.getAdapter()).logMetrics(getContext()); + } + } + + @Override public void onPause() { super.onPause(); LogUtil.enterBlock("NewCallLogFragment.onPause"); diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java index f322b562b..217208d17 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java @@ -27,6 +27,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.calllog.ui.NewCallLogAdapter.PopCounts; import com.android.dialer.calllog.ui.menu.NewCallLogMenu; import com.android.dialer.calllogutils.CallLogEntryText; import com.android.dialer.calllogutils.CallLogIntents; @@ -60,10 +61,12 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { private final Clock clock; private final RealtimeRowProcessor realtimeRowProcessor; private final ExecutorService uiExecutorService; + private final PopCounts popCounts; private long currentRowId; - NewCallLogViewHolder(View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor) { + NewCallLogViewHolder( + View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor, PopCounts popCounts) { super(view); this.context = view.getContext(); contactPhotoView = view.findViewById(R.id.contact_photo_view); @@ -79,6 +82,7 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { this.clock = clock; this.realtimeRowProcessor = realtimeRowProcessor; + this.popCounts = popCounts; uiExecutorService = DialerExecutorComponent.get(context).uiExecutor(); } @@ -105,11 +109,11 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row)); secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row)); - if (isNewMissedCall(row)) { - primaryTextView.setTextAppearance(R.style.primary_textview_new_call); - callCountTextView.setTextAppearance(R.style.primary_textview_new_call); - secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call); - phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_new_call); + if (isUnreadMissedCall(row)) { + primaryTextView.setTextAppearance(R.style.primary_textview_unread_call); + callCountTextView.setTextAppearance(R.style.primary_textview_unread_call); + secondaryTextView.setTextAppearance(R.style.secondary_textview_unread_call); + phoneAccountView.setTextAppearance(R.style.phoneaccount_textview_unread_call); } else { primaryTextView.setTextAppearance(R.style.primary_textview); callCountTextView.setTextAppearance(R.style.primary_textview); @@ -136,10 +140,11 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { } } - private boolean isNewMissedCall(CoalescedRow row) { + private boolean isUnreadMissedCall(CoalescedRow row) { // Show missed call styling if the most recent call in the group was missed and it is still - // marked as NEW. It is not clear what IS_READ should be used for and it is currently not used. - return row.getCallType() == Calls.MISSED_TYPE && row.getIsNew(); + // marked as not read. The "NEW" column is presumably used for notifications and voicemails + // only. + return row.getCallType() == Calls.MISSED_TYPE && !row.getIsRead(); } private void setPhoto(CoalescedRow row) { @@ -155,7 +160,7 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { ColorStateList colorStateList = ColorStateList.valueOf( context.getColor( - isNewMissedCall(row) + isUnreadMissedCall(row) ? R.color.feature_icon_unread_color : R.color.feature_icon_read_color)); @@ -213,7 +218,7 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { } callTypeIcon.setImageResource(resId); - if (isNewMissedCall(row)) { + if (isUnreadMissedCall(row)) { callTypeIcon.setImageTintList( ColorStateList.valueOf(context.getColor(R.color.call_type_icon_unread_color))); } else { @@ -258,13 +263,17 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { // If the user scrolled then this ViewHolder may not correspond to the completed task and // there's nothing to do. if (originalRow.getId() != currentRowId) { + popCounts.didNotPop++; return; } // Only update the UI if the updated row differs from the original row (which has already // been displayed). if (!updatedRow.equals(originalRow)) { displayRow(updatedRow); + popCounts.popped++; + return; } + popCounts.didNotPop++; } @Override diff --git a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java index b955e029b..c5148d93e 100644 --- a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java +++ b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java @@ -198,8 +198,14 @@ public final class RealtimeRowProcessor { private CoalescedRow applyPhoneLookupInfoToRow( PhoneLookupInfo phoneLookupInfo, CoalescedRow row) { + // Force the "cp2_info_incomplete" value to the original value so that it is not used when + // comparing the original row to the updated row. + // TODO(linyuh): Improve the comparison instead. return row.toBuilder() - .setNumberAttributes(NumberAttributesConverter.fromPhoneLookupInfo(phoneLookupInfo).build()) + .setNumberAttributes( + NumberAttributesConverter.fromPhoneLookupInfo(phoneLookupInfo) + .setIsCp2InfoIncomplete(row.getNumberAttributes().getIsCp2InfoIncomplete()) + .build()) .build(); } } diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java index dabb9bbe4..3869e78c3 100644 --- a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java +++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java @@ -35,9 +35,9 @@ public final class NewCallLogMenu { HistoryItemActionBottomSheet.show( context, BottomSheetHeader.fromRow(context, row), Modules.fromRow(context, row)); - // 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.getIsNew() && row.getCallType() == Calls.MISSED_TYPE) { + // If the user opens the bottom sheet for an unread call, clear the notifications and make the + // row not bold immediately. To do this, mark all of the calls in group as read. + if (!row.getIsRead() && row.getCallType() == Calls.MISSED_TYPE) { Futures.addCallback( CallLogComponent.get(context) .getClearMissedCalls() diff --git a/java/com/android/dialer/calllog/ui/res/values/styles.xml b/java/com/android/dialer/calllog/ui/res/values/styles.xml index d521feed4..047f1dace 100644 --- a/java/com/android/dialer/calllog/ui/res/values/styles.xml +++ b/java/com/android/dialer/calllog/ui/res/values/styles.xml @@ -21,7 +21,7 @@ <item name="android:fontFamily">sans-serif</item> </style> - <style name="primary_textview_new_call"> + <style name="primary_textview_unread_call"> <item name="android:textColor">@color/primary_text_color</item> <item name="android:fontFamily">sans-serif-medium</item> </style> @@ -35,12 +35,12 @@ <item name="android:fontFamily">sans-serif</item> </style> - <style name="secondary_textview_new_call"> + <style name="secondary_textview_unread_call"> <item name="android:textColor">@color/missed_call</item> <item name="android:fontFamily">sans-serif-medium</item> </style> - <style name="phoneaccount_textview_new_call"> + <style name="phoneaccount_textview_unread_call"> <item name="android:fontFamily">sans-serif-medium</item> </style> diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java index c77869169..e346de011 100644 --- a/java/com/android/dialer/calllogutils/CallLogEntryText.java +++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java @@ -20,6 +20,7 @@ import android.content.Context; import android.provider.CallLog.Calls; import android.text.TextUtils; import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.duo.DuoConstants; import com.android.dialer.time.Clock; import com.google.common.base.Optional; import com.google.common.collect.Collections2; @@ -72,20 +73,22 @@ public final class CallLogEntryText { * <p>Rules: * * <ul> - * <li>For numbers that are not spam or blocked: (Duo video, )?$Label|$Location • Date - * <li>For blocked non-spam numbers: Blocked • (Duo video, )?$Label|$Location • Date - * <li>For spam but not blocked numbers: Spam • (Duo video, )?$Label • Date - * <li>For blocked spam numbers: Blocked • Spam • (Duo video, )?$Label • Date + * <li>For numbers that are not spam or blocked: $Label(, Duo video|Carrier video)?|$Location • + * Date + * <li>For blocked non-spam numbers: Blocked • $Label(, Duo video|Carrier video)?|$Location • + * Date + * <li>For spam but not blocked numbers: Spam • $Label(, Duo video|Carrier video)? • Date + * <li>For blocked spam numbers: Blocked • Spam • $Label(, Duo video|Carrier video)? • Date * </ul> * * <p>Examples: * * <ul> - * <li>Duo Video, Mobile • Now - * <li>Duo Video • 10 min ago + * <li>Mobile, Duo video • Now + * <li>Duo video • 10 min ago * <li>Mobile • 11:45 PM * <li>Mobile • Sun - * <li>Blocked • Duo Video, Mobile • Now + * <li>Blocked • Mobile, Duo video • Now * <li>Blocked • Brooklyn, NJ • 10 min ago * <li>Spam • Mobile • Now * <li>Spam • Now @@ -125,20 +128,20 @@ public final class CallLogEntryText { /* * Rules: * For numbers that are not spam or blocked: - * (Duo video, )?$Label|$Location [• NumberIfNoName]? + * $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]? * For blocked non-spam numbers: - * Blocked • (Duo video, )?$Label|$Location [• NumberIfNoName]? + * Blocked • $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]? * For spam but not blocked numbers: - * Spam • (Duo video, )?$Label [• NumberIfNoName]? + * Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]? * For blocked spam numbers: - * Blocked • Spam • (Duo video, )?$Label [• NumberIfNoName]? + * Blocked • Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]? * * The number is shown at the end if there is no name for the entry. (It is shown in primary * text otherwise.) * * Examples: - * Duo Video, Mobile • 555-1234 - * Duo Video • 555-1234 + * Mobile, Duo video • 555-1234 + * Duo video • 555-1234 * Mobile • 555-1234 * Blocked • Mobile • 555-1234 * Blocked • Brooklyn, NJ • 555-1234 @@ -178,7 +181,7 @@ public final class CallLogEntryText { } /** - * Returns a value such as "Duo Video, Mobile" without the time of the call or formatted number + * Returns a value such as "Mobile, Duo video" without the time of the call or formatted number * appended. * * <p>When the secondary text is shown in call log entry list, this prefix is suffixed with the @@ -187,18 +190,30 @@ public final class CallLogEntryText { */ private static CharSequence getNumberTypeLabel(Context context, CoalescedRow row) { StringBuilder secondaryText = new StringBuilder(); - if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { - // TODO(zachh): Add "Duo" prefix? - secondaryText.append(context.getText(R.string.new_call_log_video)); - } + + // The number type label comes first (e.g., "Mobile", "Work", "Home", etc). String numberTypeLabel = row.getNumberAttributes().getNumberTypeLabel(); - if (!TextUtils.isEmpty(numberTypeLabel)) { + secondaryText.append(numberTypeLabel); + + // Add video call info if applicable. + if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { if (secondaryText.length() > 0) { secondaryText.append(", "); } - secondaryText.append(numberTypeLabel); - } else if (!row.getNumberAttributes().getIsSpam()) { - // Don't show the location if there's a number type label or the number is spam. + + boolean isDuoCall = + DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME + .flattenToString() + .equals(row.getPhoneAccountComponentName()); + secondaryText.append( + context.getText( + isDuoCall ? R.string.new_call_log_duo_video : R.string.new_call_log_carrier_video)); + } + + // Show the location if + // (1) there is no number type label, and + // (2) the number is not spam. + if (TextUtils.isEmpty(numberTypeLabel) && !row.getNumberAttributes().getIsSpam()) { String location = row.getGeocodedLocation(); if (!TextUtils.isEmpty(location)) { if (secondaryText.length() > 0) { @@ -207,6 +222,7 @@ public final class CallLogEntryText { secondaryText.append(location); } } + return secondaryText; } diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml index f536ca66d..bc19ce22a 100644 --- a/java/com/android/dialer/calllogutils/res/values/strings.xml +++ b/java/com/android/dialer/calllogutils/res/values/strings.xml @@ -131,8 +131,11 @@ <!-- String to be displayed to indicate in the call log that a call just now occurred. --> <string name="just_now">Just now</string> - <!-- Text to show in call log for a video call. [CHAR LIMIT=16] --> - <string name="new_call_log_video">Video</string> + <!-- Text to show in call log for a carrier video call. [CHAR LIMIT=30] --> + <string name="new_call_log_carrier_video">Carrier video</string> + + <!-- Text to show in call log for a duo video call. [CHAR LIMIT=30] --> + <string name="new_call_log_duo_video">Duo video</string> <!-- String used to display calls from unknown numbers in the call log. [CHAR LIMIT=30] --> <string name="new_call_log_unknown">Unknown</string> diff --git a/java/com/android/dialer/commandline/CommandLineModule.java b/java/com/android/dialer/commandline/CommandLineModule.java index 612155662..915578722 100644 --- a/java/com/android/dialer/commandline/CommandLineModule.java +++ b/java/com/android/dialer/commandline/CommandLineModule.java @@ -16,7 +16,7 @@ package com.android.dialer.commandline; -import com.android.dialer.commandline.impl.Blocking; +import com.android.dialer.commandline.impl.BlockingCommand; import com.android.dialer.commandline.impl.CallCommand; import com.android.dialer.commandline.impl.Echo; import com.android.dialer.commandline.impl.Help; @@ -43,16 +43,20 @@ public abstract class CommandLineModule { private final Help help; private final Version version; private final Echo echo; - private final Blocking blocking; + private final BlockingCommand blockingCommand; private final CallCommand callCommand; @Inject AospCommandInjector( - Help help, Version version, Echo echo, Blocking blocking, CallCommand callCommand) { + Help help, + Version version, + Echo echo, + BlockingCommand blockingCommand, + CallCommand callCommand) { this.help = help; this.version = version; this.echo = echo; - this.blocking = blocking; + this.blockingCommand = blockingCommand; this.callCommand = callCommand; } @@ -60,7 +64,7 @@ public abstract class CommandLineModule { builder.addCommand("help", help); builder.addCommand("version", version); builder.addCommand("echo", echo); - builder.addCommand("blocking", blocking); + builder.addCommand("blocking", blockingCommand); builder.addCommand("call", callCommand); return builder; } diff --git a/java/com/android/dialer/commandline/impl/Blocking.java b/java/com/android/dialer/commandline/impl/BlockingCommand.java index 2afd16522..c8f893422 100644 --- a/java/com/android/dialer/commandline/impl/Blocking.java +++ b/java/com/android/dialer/commandline/impl/BlockingCommand.java @@ -18,17 +18,25 @@ package com.android.dialer.commandline.impl; import android.content.Context; import android.support.annotation.NonNull; -import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.blocking.Blocking; import com.android.dialer.commandline.Arguments; import com.android.dialer.commandline.Command; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.phonelookup.PhoneLookupComponent; +import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.consolidator.PhoneLookupInfoConsolidator; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; +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 com.google.i18n.phonenumbers.PhoneNumberUtil; import javax.inject.Inject; /** Block or unblock a number. */ -public class Blocking implements Command { +public class BlockingCommand implements Command { @NonNull @Override @@ -46,7 +54,7 @@ public class Blocking implements Command { private final ListeningExecutorService executorService; @Inject - Blocking( + BlockingCommand( @ApplicationContext Context context, @BackgroundExecutor ListeningExecutorService executorService) { this.appContext = context; @@ -55,42 +63,49 @@ public class Blocking implements Command { @Override public ListenableFuture<String> run(Arguments args) throws IllegalCommandLineArgumentException { - // AsyncQueryHandler must be created on a thread with looper. - // TODO(a bug): Use blocking version - FilteredNumberAsyncQueryHandler asyncQueryHandler = - new FilteredNumberAsyncQueryHandler(appContext); - return executorService.submit(() -> doInBackground(args, asyncQueryHandler)); - } - - private String doInBackground(Arguments args, FilteredNumberAsyncQueryHandler asyncQueryHandler) { if (args.getPositionals().isEmpty()) { - return getUsage(); + return Futures.immediateFuture(getUsage()); } String command = args.getPositionals().get(0); if ("block".equals(command)) { String number = args.getPositionals().get(1); - asyncQueryHandler.blockNumber((unused) -> {}, number, null); - return "blocked " + number; + return Futures.transform( + Blocking.block(appContext, executorService, number, null), + (unused) -> "blocked " + number, + MoreExecutors.directExecutor()); } if ("unblock".equals(command)) { String number = args.getPositionals().get(1); - Integer id = asyncQueryHandler.getBlockedIdSynchronous(number, null); - if (id == null) { - return number + " is not blocked"; - } - asyncQueryHandler.unblock((unusedRows, unusedValues) -> {}, id); - return "unblocked " + number; + return Futures.transform( + Blocking.unblock(appContext, executorService, number, null), + (unused) -> "unblocked " + number, + MoreExecutors.directExecutor()); } if ("isblocked".equals(command)) { String number = args.getPositionals().get(1); - Integer id = asyncQueryHandler.getBlockedIdSynchronous(number, null); - return id == null ? "false" : "true"; + ListenableFuture<DialerPhoneNumber> dialerPhoneNumberFuture = + executorService.submit( + () -> new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()).parse(number, null)); + + ListenableFuture<PhoneLookupInfo> lookupFuture = + Futures.transformAsync( + dialerPhoneNumberFuture, + (dialerPhoneNumber) -> + PhoneLookupComponent.get(appContext) + .compositePhoneLookup() + .lookup(dialerPhoneNumber), + executorService); + + return Futures.transform( + lookupFuture, + (info) -> new PhoneLookupInfoConsolidator(info).isBlocked() ? "true" : "false", + MoreExecutors.directExecutor()); } - return getUsage(); + return Futures.immediateFuture(getUsage()); } } diff --git a/java/com/android/dialer/logging/LoggingBindings.java b/java/com/android/dialer/logging/LoggingBindings.java index a6795ed1e..7c580cb77 100644 --- a/java/com/android/dialer/logging/LoggingBindings.java +++ b/java/com/android/dialer/logging/LoggingBindings.java @@ -90,4 +90,10 @@ public interface LoggingBindings { /** Logs a call auto-blocked in call screening. */ void logAutoBlockedCall(String phoneNumber); + + /** Logs annotated call log metrics. */ + void logAnnotatedCallLogMetrics(int invalidNumbersInCallLog); + + /** Logs annotated call log metrics. */ + void logAnnotatedCallLogMetrics(int numberRowsThatDidPop, int numberRowsThatDidNotPop); } diff --git a/java/com/android/dialer/logging/LoggingBindingsStub.java b/java/com/android/dialer/logging/LoggingBindingsStub.java index de08f4497..65ebd1a52 100644 --- a/java/com/android/dialer/logging/LoggingBindingsStub.java +++ b/java/com/android/dialer/logging/LoggingBindingsStub.java @@ -64,4 +64,10 @@ public class LoggingBindingsStub implements LoggingBindings { @Override public void logAutoBlockedCall(String phoneNumber) {} + + @Override + public void logAnnotatedCallLogMetrics(int invalidNumbersInCallLog) {} + + @Override + public void logAnnotatedCallLogMetrics(int numberRowsThatDidPop, int numberRowsThatDidNotPop) {} } diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto index 16efd137c..96a7bb6f6 100644 --- a/java/com/android/dialer/logging/dialer_impression.proto +++ b/java/com/android/dialer/logging/dialer_impression.proto @@ -12,7 +12,7 @@ message DialerImpression { // Event enums to be used for Impression Logging in Dialer. // It's perfectly acceptable for this enum to be large // Values should be from 1000 to 100000. - // Next Tag: 1369 + // Next Tag: 1371 enum Type { UNKNOWN_AOSP_EVENT_TYPE = 1000; @@ -78,9 +78,17 @@ message DialerImpression { ; + // User made it to the last step but blocking failed because user is + // secondary or dialer is not default + USER_ACTION_BLOCK_NUMBER_FAILED = 1369; + // User made it to the last step and actually unblocked the number USER_ACTION_UNBLOCKED_NUMBER = 1013; + // User made it to the last step but unblocking failed because user is + // secondary or dialer is not default + USER_ACTION_UNBLOCK_NUMBER_FAILED = 1370; + // User blocked a number, does not guarantee if the number was reported as // spam or not To compute the number of blocked numbers that were reported // as not spam and yet blocked Subtract this value from diff --git a/java/com/android/dialer/main/impl/MainSearchController.java b/java/com/android/dialer/main/impl/MainSearchController.java index c2ff0512a..7b4bc3569 100644 --- a/java/com/android/dialer/main/impl/MainSearchController.java +++ b/java/com/android/dialer/main/impl/MainSearchController.java @@ -149,25 +149,24 @@ public class MainSearchController implements SearchBarListener { // Show Search if (searchFragment == null) { - // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby - // places promo to be shown. - searchFragment = NewSearchFragment.newInstance(/* showZeroSuggest=*/ true); + searchFragment = NewSearchFragment.newInstance(); transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG); transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); } else if (!isSearchVisible()) { transaction.show(searchFragment); } - searchFragment.setQuery("", CallInitiationType.Type.DIALPAD); // Show Dialpad if (getDialpadFragment() == null) { DialpadFragment dialpadFragment = new DialpadFragment(); dialpadFragment.setStartedFromNewIntent(fromNewIntent); transaction.add(R.id.dialpad_fragment_container, dialpadFragment, DIALPAD_FRAGMENT_TAG); + searchFragment.setQuery("", CallInitiationType.Type.DIALPAD); } else { DialpadFragment dialpadFragment = getDialpadFragment(); dialpadFragment.setStartedFromNewIntent(fromNewIntent); transaction.show(dialpadFragment); + searchFragment.setQuery(dialpadFragment.getQuery(), CallInitiationType.Type.DIALPAD); } transaction.commit(); @@ -258,7 +257,7 @@ public class MainSearchController implements SearchBarListener { } else { Logger.get(activity) .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_HIDE_KEYBOARD); - toolbar.hideKeyboard(); + closeKeyboard(); } } } @@ -348,6 +347,14 @@ public class MainSearchController implements SearchBarListener { return isSearchVisible(); } + /** Closes the keyboard if necessary. */ + private void closeKeyboard() { + NewSearchFragment fragment = getSearchFragment(); + if (fragment != null && fragment.isAdded()) { + toolbar.hideKeyboard(); + } + } + /** * Opens search in regular/search bar search mode. * @@ -376,9 +383,7 @@ public class MainSearchController implements SearchBarListener { // Show Search if (searchFragment == null) { - // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby - // places promo to be shown. - searchFragment = NewSearchFragment.newInstance(true); + searchFragment = NewSearchFragment.newInstance(); transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG); transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); } else if (!isSearchVisible()) { @@ -446,6 +451,9 @@ public class MainSearchController implements SearchBarListener { @Override public void onActivityPause() { + LogUtil.enterBlock("MainSearchController.onActivityPause"); + closeKeyboard(); + if (closeSearchOnPause) { closeSearchOnPause = false; if (isInSearch()) { @@ -462,7 +470,7 @@ public class MainSearchController implements SearchBarListener { closeSearchOnPause = !requestingPermission; // Always hide the keyboard when the user leaves dialer (including permission requests) - toolbar.hideKeyboard(); + closeKeyboard(); } } @@ -473,6 +481,7 @@ public class MainSearchController implements SearchBarListener { @Override public void requestingPermission() { + LogUtil.enterBlock("MainSearchController.requestingPermission"); requestingPermission = true; } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java index 902a2fbe3..c5d4e53f2 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2DefaultDirectoryPhoneLookup.java @@ -33,7 +33,9 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; +import com.android.dialer.configprovider.ConfigProvider; import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.logging.Logger; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; @@ -64,15 +66,11 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info private static final String PREF_LAST_TIMESTAMP_PROCESSED = "cp2DefaultDirectoryPhoneLookupLastTimestampProcessed"; - // We cannot efficiently process invalid numbers because batch queries cannot be constructed which - // accomplish the necessary loose matching. We'll attempt to process a limited number of them, - // but if there are too many we fall back to querying CP2 at render time. - private static final int MAX_SUPPORTED_INVALID_NUMBERS = 5; - private final Context appContext; private final SharedPreferences sharedPreferences; private final ListeningExecutorService backgroundExecutorService; private final ListeningExecutorService lightweightExecutorService; + private final ConfigProvider configProvider; @Nullable private Long currentLastTimestampProcessed; @@ -81,11 +79,13 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info @ApplicationContext Context appContext, @Unencrypted SharedPreferences sharedPreferences, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, - @LightweightExecutor ListeningExecutorService lightweightExecutorService) { + @LightweightExecutor ListeningExecutorService lightweightExecutorService, + ConfigProvider configProvider) { this.appContext = appContext; this.sharedPreferences = sharedPreferences; this.backgroundExecutorService = backgroundExecutorService; this.lightweightExecutorService = lightweightExecutorService; + this.configProvider = configProvider; } @Override @@ -138,7 +138,7 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info @Override public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers); - if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { + if (partitionedNumbers.invalidNumbers().size() > getMaxSupportedInvalidNumbers()) { // If there are N invalid numbers, we can't determine determine dirtiness without running N // queries; since running this many queries is not feasible for the (lightweight) isDirty // check, simply return true. The expectation is that this should rarely be the case as the @@ -234,7 +234,8 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info // Then run a separate query for each invalid number. Separate queries are done to accomplish // loose matching which couldn't be accomplished with a batch query. - Assert.checkState(partitionedNumbers.invalidNumbers().size() <= MAX_SUPPORTED_INVALID_NUMBERS); + Assert.checkState( + partitionedNumbers.invalidNumbers().size() <= getMaxSupportedInvalidNumbers()); for (String invalidNumber : partitionedNumbers.invalidNumbers()) { queryFutures.add(queryPhoneLookupTableForContactIdsBasedOnRawNumber(invalidNumber)); } @@ -529,7 +530,11 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) { ArraySet<DialerPhoneNumber> unprocessableNumbers = new ArraySet<>(); PartitionedNumbers partitionedNumbers = new PartitionedNumbers(existingInfoMap.keySet()); - if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { + + int invalidNumberCount = partitionedNumbers.invalidNumbers().size(); + Logger.get(appContext).logAnnotatedCallLogMetrics(invalidNumberCount); + + if (invalidNumberCount > getMaxSupportedInvalidNumbers()) { for (String invalidNumber : partitionedNumbers.invalidNumbers()) { unprocessableNumbers.addAll(partitionedNumbers.dialerPhoneNumbersForInvalid(invalidNumber)); } @@ -928,4 +933,13 @@ public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info } return where.toString(); } + + /** + * We cannot efficiently process invalid numbers because batch queries cannot be constructed which + * accomplish the necessary loose matching. We'll attempt to process a limited number of them, but + * if there are too many we fall back to querying CP2 at render time. + */ + private long getMaxSupportedInvalidNumbers() { + return configProvider.getLong("cp2_phone_lookup_max_invalid_numbers", 5); + } } diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java index 505f1c6b1..51befe822 100644 --- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java +++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java @@ -67,7 +67,6 @@ import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Dir import com.android.dialer.searchfragment.directories.DirectoryContactsCursorLoader; import com.android.dialer.searchfragment.list.SearchActionViewHolder.Action; import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader; -import com.android.dialer.storage.StorageComponent; import com.android.dialer.util.CallUtil; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.PermissionsUtil; @@ -94,7 +93,6 @@ public final class NewSearchFragment extends Fragment // updates so they are bundled together private static final int ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY = 400; - private static final String KEY_SHOW_ZERO_SUGGEST = "use_zero_suggest"; private static final String KEY_LOCATION_PROMPT_DISMISSED = "search_location_prompt_dismissed"; @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; @@ -134,12 +132,8 @@ public final class NewSearchFragment extends Fragment private Runnable updatePositionRunnable; - public static NewSearchFragment newInstance(boolean showZeroSuggest) { - NewSearchFragment fragment = new NewSearchFragment(); - Bundle args = new Bundle(); - args.putBoolean(KEY_SHOW_ZERO_SUGGEST, showZeroSuggest); - fragment.setArguments(args); - return fragment; + public static NewSearchFragment newInstance() { + return new NewSearchFragment(); } @Nullable @@ -150,7 +144,7 @@ public final class NewSearchFragment extends Fragment adapter = new SearchAdapter(getContext(), new SearchCursorManager(), this); adapter.setQuery(query, rawNumber); adapter.setSearchActions(getActions()); - adapter.setZeroSuggestVisible(getArguments().getBoolean(KEY_SHOW_ZERO_SUGGEST)); + showLocationPermission(); emptyContentView = view.findViewById(R.id.empty_view); recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -265,13 +259,31 @@ public final class NewSearchFragment extends Fragment if (adapter != null) { adapter.setQuery(query, rawNumber); adapter.setSearchActions(getActions()); - adapter.setZeroSuggestVisible(isRegularSearch()); + showLocationPermission(); loadCp2ContactsCursor(); loadNearbyPlacesCursor(); loadDirectoryContactsCursors(); } } + /** Returns true if the location permission was shown. */ + private boolean showLocationPermission() { + if (adapter == null) { + return false; + } + + if (PermissionsUtil.hasLocationPermissions(getContext()) + || hasBeenDismissed() + || !isRegularSearch()) { + adapter.hideLocationPermissionRequest(); + return false; + } + + adapter.showLocationPermissionRequest( + v -> requestLocationPermission(), v -> dismissLocationPermission()); + return true; + } + /** Translate the search fragment and resize it to fit on the screen. */ public void animatePosition(int start, int end, int duration) { // Called before the view is ready, prepare a runnable to run in onCreateView @@ -382,16 +394,16 @@ public final class NewSearchFragment extends Fragment * <p>Should not be called before finishing loading info about all directories (local and remote). */ private void loadNearbyPlacesCursor() { - if (!PermissionsUtil.hasLocationPermissions(getContext()) - && !StorageComponent.get(getContext()) - .unencryptedSharedPrefs() - .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false)) { - if (adapter != null && isRegularSearch() && !hasBeenDismissed()) { - adapter.showLocationPermissionRequest( - v -> requestLocationPermission(), v -> dismissLocationPermission()); - } + // If we're requesting the location permission, don't load nearby places cursor. + if (showLocationPermission()) { return; } + + // If the user dismissed the prompt without granting us the permission, don't load the cursor. + if (!PermissionsUtil.hasLocationPermissions(getContext())) { + return; + } + // Cancel existing load if one exists. ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable); diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java index 805eaf524..74b60c603 100644 --- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java +++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java @@ -42,7 +42,6 @@ public final class SearchAdapter extends RecyclerView.Adapter<ViewHolder> { private final SearchCursorManager searchCursorManager; private final Context context; - private boolean showZeroSuggest; private String query; // Raw query number from dialpad, which may contain special character such as "+". This is used // for actions to add contact or send sms. @@ -138,21 +137,9 @@ public final class SearchAdapter extends RecyclerView.Adapter<ViewHolder> { @Override public int getItemCount() { - if (TextUtils.isEmpty(query) && !showZeroSuggest) { - return 0; - } return searchCursorManager.getCount(); } - /** - * @param visible If true and query is empty, the adapter won't show any list elements. - * @see #setQuery(String, String) - * @see #getItemCount() - */ - public void setZeroSuggestVisible(boolean visible) { - showZeroSuggest = visible; - } - public void setQuery(String query, @Nullable String rawNumber) { this.query = query; this.rawNumber = rawNumber; diff --git a/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml index 99d0fbf0c..8b366fe8a 100644 --- a/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml +++ b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml @@ -24,7 +24,7 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginStart="8dp" - android:layout_gravity="center_vertical" + android:layout_gravity="center_vertical|start" android:padding="12dp" android:tint="@color/dialer_theme_color"/> diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java new file mode 100644 index 000000000..aa90909f1 --- /dev/null +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java @@ -0,0 +1,116 @@ +/* + * 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.speeddial.database; + +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import com.google.auto.value.AutoValue; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** POJO representation of database rows returned by {@link SpeedDialEntryDao}. */ +@AutoValue +public abstract class SpeedDialEntry { + + /** Unique ID */ + public abstract long id(); + + /** @see {@link Contacts#_ID} */ + public abstract long contactId(); + + /** @see {@link Contacts#LOOKUP_KEY} */ + public abstract String lookupKey(); + + /** + * {@link Channel} that is associated with this entry. + * + * <p>Contacts with multiple channels do not have a default until specified by the user. Once the + * default channel is determined, all calls should be placed to this channel. + */ + @Nullable + public abstract Channel defaultChannel(); + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_SpeedDialEntry.Builder(); + } + + /** Builder class for speed dial entry. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setId(long id); + + public abstract Builder setContactId(long contactId); + + public abstract Builder setLookupKey(String lookupKey); + + public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel); + + public abstract SpeedDialEntry build(); + } + + /** POJO representation of a relevant phone number columns in {@link SpeedDialEntryDao}. */ + @AutoValue + public abstract static class Channel { + + public static final int UNKNOWN = 0; + public static final int VOICE = 1; + public static final int VIDEO = 2; + + /** Whether the Channel is for an audio or video call. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({UNKNOWN, VOICE, VIDEO}) + public @interface Technology {} + + /** + * Raw phone number as the user entered it. + * + * @see {@link Phone#NUMBER} + */ + public abstract String number(); + + /** + * Label that the user associated with this number like {@link Phone#TYPE_WORK}, {@link + * Phone#TYPE_HOME}, ect. + * + * @see {@link Phone#LABEL} + */ + public abstract String label(); + + public abstract @Technology int technology(); + + public static Builder builder() { + return new AutoValue_SpeedDialEntry_Channel.Builder(); + } + + /** Builder class for {@link Channel}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setNumber(String number); + + public abstract Builder setLabel(String label); + + public abstract Builder setTechnology(@Technology int technology); + + public abstract Channel build(); + } + } +} diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java new file mode 100644 index 000000000..39cb115c8 --- /dev/null +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java @@ -0,0 +1,57 @@ +/* + * 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.speeddial.database; + +import java.util.List; + +/** Interface that databases support speed dial entries should implement. */ +public interface SpeedDialEntryDao { + + /** Return all entries in the database */ + List<SpeedDialEntry> getAllEntries(); + + /** + * Insert new entries. + * + * <p>Fails if any of the {@link SpeedDialEntry#id()} already exist. + */ + void insert(List<SpeedDialEntry> entries); + + /** + * Insert a new entry. + * + * <p>Fails if the {@link SpeedDialEntry#id()} already exists. + */ + long insert(SpeedDialEntry entry); + + /** + * Updates existing entries based on {@link SpeedDialEntry#id}. + * + * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist. + */ + void update(List<SpeedDialEntry> entries); + + /** + * Delete the passed in entries based on {@link SpeedDialEntry#id}. + * + * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist. + */ + void delete(List<Long> entries); + + /** Delete all entries in the database. */ + void deleteAll(); +} diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java new file mode 100644 index 000000000..1812dbdc0 --- /dev/null +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java @@ -0,0 +1,221 @@ +/* + * 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.speeddial.database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.text.TextUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.database.Selection; +import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import java.util.ArrayList; +import java.util.List; + +/** {@link SpeedDialEntryDao} implemented as an SQLite database. */ +public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper + implements SpeedDialEntryDao { + + private static final int DATABASE_VERSION = 1; + private static final String DATABASE_NAME = "CPSpeedDialEntry"; + + // Column names + private static final String TABLE_NAME = "speed_dial_entries"; + private static final String ID = "id"; + private static final String CONTACT_ID = "contact_id"; + private static final String LOOKUP_KEY = "lookup_key"; + private static final String PHONE_NUMBER = "phone_number"; + private static final String PHONE_LABEL = "phone_label"; + private static final String PHONE_TYPE = "phone_type"; + + // Column positions + private static final int POSITION_ID = 0; + private static final int POSITION_CONTACT_ID = 1; + private static final int POSITION_LOOKUP_KEY = 2; + private static final int POSITION_PHONE_NUMBER = 3; + private static final int POSITION_PHONE_LABEL = 4; + private static final int POSITION_PHONE_TYPE = 5; + + // Create Table Query + private static final String CREATE_TABLE_SQL = + "create table if not exists " + + TABLE_NAME + + " (" + + (ID + " integer primary key, ") + + (CONTACT_ID + " integer, ") + + (LOOKUP_KEY + " text, ") + + (PHONE_NUMBER + " text, ") + + (PHONE_LABEL + " text, ") + + (PHONE_TYPE + " integer ") + + ");"; + + private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME; + + public SpeedDialEntryDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_SQL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO(calderwoodra): handle upgrades more elegantly + db.execSQL(DELETE_TABLE_SQL); + this.onCreate(db); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO(calderwoodra): handle upgrades more elegantly + this.onUpgrade(db, oldVersion, newVersion); + } + + @Override + public List<SpeedDialEntry> getAllEntries() { + List<SpeedDialEntry> entries = new ArrayList<>(); + + String query = "SELECT * FROM " + TABLE_NAME; + try (SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.rawQuery(query, null)) { + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + Channel channel = + Channel.builder() + .setNumber(cursor.getString(POSITION_PHONE_NUMBER)) + .setLabel(cursor.getString(POSITION_PHONE_LABEL)) + .setTechnology(cursor.getInt(POSITION_PHONE_TYPE)) + .build(); + if (TextUtils.isEmpty(channel.number())) { + channel = null; + } + SpeedDialEntry entry = + SpeedDialEntry.builder() + .setDefaultChannel(channel) + .setContactId(cursor.getLong(POSITION_CONTACT_ID)) + .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY)) + .setId(cursor.getInt(POSITION_ID)) + .build(); + entries.add(entry); + } + } + return entries; + } + + @Override + public void insert(List<SpeedDialEntry> entries) { + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + try { + for (SpeedDialEntry entry : entries) { + if (db.insert(TABLE_NAME, null, buildContentValues(entry)) == -1L) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to insert a row that already exists."); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + db.close(); + } + } + + @Override + public long insert(SpeedDialEntry entry) { + long updateRowId; + try (SQLiteDatabase db = getWritableDatabase()) { + updateRowId = db.insert(TABLE_NAME, null, buildContentValues(entry)); + } + if (updateRowId == -1) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to insert a row that already exists."); + } + return updateRowId; + } + + @Override + public void update(List<SpeedDialEntry> entries) { + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + try { + for (SpeedDialEntry entry : entries) { + int count = + db.update( + TABLE_NAME, + buildContentValues(entry), + ID + " = ?", + new String[] {Long.toString(entry.id())}); + if (count != 1) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to update an undetermined number of rows: " + count); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + db.close(); + } + } + + private ContentValues buildContentValues(SpeedDialEntry entry) { + ContentValues values = new ContentValues(); + values.put(ID, entry.id()); + values.put(CONTACT_ID, entry.contactId()); + values.put(LOOKUP_KEY, entry.lookupKey()); + if (entry.defaultChannel() != null) { + values.put(PHONE_NUMBER, entry.defaultChannel().number()); + values.put(PHONE_LABEL, entry.defaultChannel().label()); + values.put(PHONE_TYPE, entry.defaultChannel().technology()); + } + return values; + } + + @Override + public void delete(List<Long> ids) { + List<String> idStrings = new ArrayList<>(); + for (Long id : ids) { + idStrings.add(Long.toString(id)); + } + + Selection selection = Selection.builder().and(Selection.column(ID).in(idStrings)).build(); + try (SQLiteDatabase db = getWritableDatabase()) { + int count = db.delete(TABLE_NAME, selection.getSelection(), selection.getSelectionArgs()); + if (count != ids.size()) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to delete an undetermined number of rows: " + count); + } + } + } + + @Override + public void deleteAll() { + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + try { + // Passing null into where clause will delete all rows + db.delete(TABLE_NAME, /* whereClause=*/ null, null); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + db.close(); + } + } +} diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java index 2608cb2aa..f05ec202d 100644 --- a/java/com/android/dialer/telecom/TelecomUtil.java +++ b/java/com/android/dialer/telecom/TelecomUtil.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.provider.CallLog.Calls; +import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresPermission; @@ -299,6 +300,11 @@ public abstract class TelecomUtil { return instance.isDefaultDialer(context); } + public static boolean isRttEnabled(Context context) { + return Settings.System.getInt(context.getContentResolver(), Settings.System.RTT_CALLING_MODE, 0) + != 0; + } + /** @return the other SIM based PhoneAccountHandle that is not {@code currentAccount} */ @Nullable @RequiresPermission(permission.READ_PHONE_STATE) diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java index 385464252..38c8da898 100644 --- a/java/com/android/incallui/CallButtonPresenter.java +++ b/java/com/android/incallui/CallButtonPresenter.java @@ -304,6 +304,12 @@ public class CallButtonPresenter } @Override + public void changeToRttClicked() { + LogUtil.enterBlock("CallButtonPresenter.changeToRttClicked"); + call.sendRttUpgradeRequest(); + } + + @Override public void onEndCallClicked() { LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + call); if (call != null) { @@ -473,6 +479,8 @@ public class CallButtonPresenter // Most devices cannot make calls on 2 SIMs at the same time. && InCallPresenter.getInstance().getCallList().getAllCalls().size() == 1; + boolean showUpgradeToRtt = TelecomUtil.isRttEnabled(context) && call.canUpgradeToRttCall(); + inCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true); inCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap); inCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold); @@ -482,6 +490,7 @@ public class CallButtonPresenter inCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true); inCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall); inCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); + inCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_RTT, showUpgradeToRtt); inCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio); inCallButtonUi.showButton( InCallButtonIds.BUTTON_SWITCH_CAMERA, diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java index 9c5e0623e..2f88c8836 100644 --- a/java/com/android/incallui/CallCardPresenter.java +++ b/java/com/android/incallui/CallCardPresenter.java @@ -271,10 +271,10 @@ public class CallCardPresenter // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the // highest priority call to display as the secondary call. - secondary = getCallToDisplay(callList, null, true); + secondary = InCallPresenter.getCallToDisplay(callList, null, true); } else if (newState == InCallState.INCALL) { - primary = getCallToDisplay(callList, null, false); - secondary = getCallToDisplay(callList, primary, true); + primary = InCallPresenter.getCallToDisplay(callList, null, false); + secondary = InCallPresenter.getCallToDisplay(callList, primary, true); } LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary); @@ -302,7 +302,6 @@ public class CallCardPresenter this.primaryNumber = primaryNumber; if (this.primary != null) { - InCallPresenter.getInstance().onForegroundCallChanged(this.primary); inCallScreen.updateInCallScreenColors(); } @@ -636,54 +635,6 @@ public class CallCardPresenter } } - /** - * Get the highest priority call to display. Goes through the calls and chooses which to return - * based on priority of which type of call to display to the user. Callers can use the "ignore" - * feature to get the second best call by passing a previously found primary call as ignore. - * - * @param ignore A call to ignore if found. - */ - private DialerCall getCallToDisplay( - CallList callList, DialerCall ignore, boolean skipDisconnected) { - // Active calls come second. An active call always gets precedent. - DialerCall retval = callList.getActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Sometimes there is intemediate state that two calls are in active even one is about - // to be on hold. - retval = callList.getSecondActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Disconnected calls get primary position if there are no active calls - // to let user know quickly what call has disconnected. Disconnected - // calls are very short lived. - if (!skipDisconnected) { - retval = callList.getDisconnectingCall(); - if (retval != null && retval != ignore) { - return retval; - } - retval = callList.getDisconnectedCall(); - if (retval != null && retval != ignore) { - return retval; - } - } - - // Then we go to background call (calls on hold) - retval = callList.getBackgroundCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Lastly, we go to a second background call. - retval = callList.getSecondBackgroundCall(); - - return retval; - } - private void updatePrimaryDisplayInfo() { if (inCallScreen == null) { // TODO: May also occur if search result comes back after ui is destroyed. Look into diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java index 9d08dc4b6..1ba3683f0 100644 --- a/java/com/android/incallui/InCallActivity.java +++ b/java/com/android/incallui/InCallActivity.java @@ -1468,7 +1468,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity return new ShouldShowUiResult(false, null); } - if (call.isRttCall()) { + if (call.isActiveRttCall()) { LogUtil.i("InCallActivity.getShouldShowRttUi", "found rtt call"); return new ShouldShowUiResult(true, call); } @@ -1520,7 +1520,7 @@ public class InCallActivity extends TransactionSafeFragmentActivity AnswerScreen answerScreen = AnswerBindings.createAnswerScreen( call.getId(), - call.isRttCall(), + call.isActiveRttCall(), call.isVideoCall(), isVideoUpgradeRequest, call.getVideoTech().isSelfManagedCamera(), diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java index 2e98a969d..e11b376c1 100644 --- a/java/com/android/incallui/InCallPresenter.java +++ b/java/com/android/incallui/InCallPresenter.java @@ -771,6 +771,22 @@ public class InCallPresenter implements CallList.Listener, AudioModeProvider.Aud "Phone switching state: " + oldState + " -> " + newState); inCallState = newState; + // Foreground call changed + DialerCall primary = null; + if (newState == InCallState.INCOMING) { + primary = callList.getIncomingCall(); + } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { + primary = callList.getOutgoingCall(); + if (primary == null) { + primary = callList.getPendingOutgoingCall(); + } + } else if (newState == InCallState.INCALL) { + primary = getCallToDisplay(callList, null, false); + } + if (primary != null) { + onForegroundCallChanged(primary); + } + // notify listeners of new state for (InCallStateListener listener : listeners) { LogUtil.d( @@ -787,6 +803,54 @@ public class InCallPresenter implements CallList.Listener, AudioModeProvider.Aud Trace.endSection(); } + /** + * Get the highest priority call to display. Goes through the calls and chooses which to return + * based on priority of which type of call to display to the user. Callers can use the "ignore" + * feature to get the second best call by passing a previously found primary call as ignore. + * + * @param ignore A call to ignore if found. + */ + static DialerCall getCallToDisplay( + CallList callList, DialerCall ignore, boolean skipDisconnected) { + // Active calls come second. An active call always gets precedent. + DialerCall retval = callList.getActiveCall(); + if (retval != null && retval != ignore) { + return retval; + } + + // Sometimes there is intemediate state that two calls are in active even one is about + // to be on hold. + retval = callList.getSecondActiveCall(); + if (retval != null && retval != ignore) { + return retval; + } + + // Disconnected calls get primary position if there are no active calls + // to let user know quickly what call has disconnected. Disconnected + // calls are very short lived. + if (!skipDisconnected) { + retval = callList.getDisconnectingCall(); + if (retval != null && retval != ignore) { + return retval; + } + retval = callList.getDisconnectedCall(); + if (retval != null && retval != ignore) { + return retval; + } + } + + // Then we go to background call (calls on hold) + retval = callList.getBackgroundCall(); + if (retval != null && retval != ignore) { + return retval; + } + + // Lastly, we go to a second background call. + retval = callList.getSecondBackgroundCall(); + + return retval; + } + /** Called when there is a new incoming call. */ @Override public void onIncomingCall(DialerCall call) { diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java index 4b033441d..9719e5d3d 100644 --- a/java/com/android/incallui/ProximitySensor.java +++ b/java/com/android/incallui/ProximitySensor.java @@ -113,7 +113,7 @@ public class ProximitySensor DialerCall activeCall = callList.getActiveCall(); boolean isVideoCall = activeCall != null && activeCall.isVideoCall(); - boolean isRttCall = activeCall != null && activeCall.isRttCall(); + boolean isRttCall = activeCall != null && activeCall.isActiveRttCall(); if (isOffhook != isPhoneOffhook || this.isVideoCall != isVideoCall diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java index d5e6a1001..0850e913a 100644 --- a/java/com/android/incallui/ReturnToCallController.java +++ b/java/com/android/incallui/ReturnToCallController.java @@ -92,10 +92,10 @@ public class ReturnToCallController implements InCallUiListener, Listener, Audio endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL); fullScreen = createActionIntent(ReturnToCallActionReceiver.ACTION_RETURN_TO_CALL); - InCallPresenter.getInstance().addInCallUiListener(this); - CallList.getInstance().addListener(this); AudioModeProvider.getInstance().addListener(this); audioState = AudioModeProvider.getInstance().getAudioState(); + InCallPresenter.getInstance().addInCallUiListener(this); + CallList.getInstance().addListener(this); } public void tearDown() { @@ -186,7 +186,7 @@ public class ReturnToCallController implements InCallUiListener, Listener, Audio return; } - if ((bubble == null || !bubble.isVisible()) + if ((bubble == null || !(bubble.isVisible() || bubble.isDismissed())) && getCall() != null && !InCallPresenter.getInstance().isShowingInCallUi()) { LogUtil.i("ReturnToCallController.onCallListChange", "going to show bubble"); diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java index f639e5bdb..01f3b9d29 100644 --- a/java/com/android/incallui/call/CallList.java +++ b/java/com/android/incallui/call/CallList.java @@ -514,6 +514,15 @@ public class CallList implements DialerCallDelegate { return call != null && call != getDisconnectingCall() && call != getDisconnectedCall(); } + boolean hasActiveRttCall() { + for (DialerCall call : getAllCalls()) { + if (call.isActiveRttCall()) { + return true; + } + } + return false; + } + /** * Returns the first call found in the call map with the upgrade to video modification state. * diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java index 4815a6e41..e08c926d8 100644 --- a/java/com/android/incallui/call/DialerCall.java +++ b/java/com/android/incallui/call/DialerCall.java @@ -988,7 +988,7 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa } @TargetApi(28) - public boolean isRttCall() { + public boolean isActiveRttCall() { if (BuildCompat.isAtLeastP()) { return getTelecomCall().isRttActive(); } else { @@ -998,12 +998,41 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa @TargetApi(28) public RttCall getRttCall() { - if (!isRttCall()) { + if (!isActiveRttCall()) { return null; } return getTelecomCall().getRttCall(); } + @TargetApi(28) + public boolean canUpgradeToRttCall() { + PhoneAccount phoneAccount = getPhoneAccount(); + if (phoneAccount == null) { + return false; + } + if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) { + return false; + } + if (isActiveRttCall()) { + return false; + } + if (isVideoCall()) { + return false; + } + if (isConferenceCall()) { + return false; + } + if (CallList.getInstance().hasActiveRttCall()) { + return false; + } + return true; + } + + @TargetApi(28) + public void sendRttUpgradeRequest() { + getTelecomCall().sendRttRequest(); + } + public boolean hasReceivedVideoUpgradeRequest() { return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState()); } diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java index c7ce2b108..831ebbd52 100644 --- a/java/com/android/incallui/callpending/CallPendingActivity.java +++ b/java/com/android/incallui/callpending/CallPendingActivity.java @@ -255,6 +255,9 @@ public class CallPendingActivity extends FragmentActivity public void changeToVideoClicked() {} @Override + public void changeToRttClicked() {} + + @Override public void switchCameraClicked(boolean useFrontFacingCamera) {} @Override diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java index 2a0894047..757d81352 100644 --- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java +++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java @@ -57,6 +57,9 @@ class ButtonChooserFactory { mapping.put( InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE, MappingInfo.builder(4).setSlotOrder(0).build()); + // RTT call is only supported on IMS and WiFi. + mapping.put( + InCallButtonIds.BUTTON_UPGRADE_TO_RTT, MappingInfo.builder(3).setSlotOrder(0).build()); mapping.put( InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, MappingInfo.builder(4).setSlotOrder(10).build()); mapping.put( @@ -114,7 +117,7 @@ class ButtonChooserFactory { mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build()); mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build()); mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build()); - mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(0).build()); + mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(5).build()); mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build()); mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build()); return mapping; diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java index 98460c704..9106dab9d 100644 --- a/java/com/android/incallui/incall/impl/ButtonController.java +++ b/java/com/android/incallui/incall/impl/ButtonController.java @@ -519,6 +519,24 @@ interface ButtonController { } } + class UpgradeToRttButtonController extends SimpleNonCheckableButtonController { + + public UpgradeToRttButtonController(@NonNull InCallButtonUiDelegate delegate) { + super( + delegate, + InCallButtonIds.BUTTON_UPGRADE_TO_RTT, + 0, + R.string.incall_label_rttcall, + R.drawable.quantum_ic_rtt_vd_theme_24); + Assert.isNotNull(delegate); + } + + @Override + public void onClick(View view) { + delegate.changeToRttClicked(); + } + } + class ManageConferenceButtonController extends SimpleNonCheckableButtonController { private final InCallScreenDelegate inCallScreenDelegate; diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java index fb8c2c403..6f0ba60b8 100644 --- a/java/com/android/incallui/incall/impl/InCallFragment.java +++ b/java/com/android/incallui/incall/impl/InCallFragment.java @@ -54,6 +54,7 @@ import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRou import com.android.incallui.contactgrid.ContactGridManager; import com.android.incallui.hold.OnHoldFragment; import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController; +import com.android.incallui.incall.impl.ButtonController.UpgradeToRttButtonController; import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener; import com.android.incallui.incall.protocol.InCallButtonIds; import com.android.incallui.incall.protocol.InCallButtonIdsExtension; @@ -114,7 +115,8 @@ public class InCallFragment extends Fragment || id == InCallButtonIds.BUTTON_ADD_CALL || id == InCallButtonIds.BUTTON_MERGE || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE - || id == InCallButtonIds.BUTTON_SWAP_SIM; + || id == InCallButtonIds.BUTTON_SWAP_SIM + || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT; } @Override @@ -226,6 +228,7 @@ public class InCallFragment extends Fragment buttonControllers.add(new ButtonController.SwapSimButtonController(inCallButtonUiDelegate)); buttonControllers.add( new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate)); + buttonControllers.add(new UpgradeToRttButtonController(inCallButtonUiDelegate)); buttonControllers.add( new ButtonController.ManageConferenceButtonController(inCallScreenDelegate)); buttonControllers.add( diff --git a/java/com/android/incallui/incall/impl/res/values/strings.xml b/java/com/android/incallui/incall/impl/res/values/strings.xml index d0217566a..c4c40a15d 100644 --- a/java/com/android/incallui/incall/impl/res/values/strings.xml +++ b/java/com/android/incallui/incall/impl/res/values/strings.xml @@ -20,6 +20,10 @@ [CHAR LIMIT=12] --> <string name="incall_label_videocall">Video call</string> + <!-- Button shown during a phone call to upgrade to Real-time Text. + [CHAR LIMIT=12] --> + <string name="incall_label_rttcall">RTT</string> + <!-- Button shown during a phone call to put the call on hold. [CHAR LIMIT=12] --> <string name="incall_label_hold">Hold</string> diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java index 3de533519..80ea75e99 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java @@ -39,6 +39,7 @@ import java.lang.annotation.RetentionPolicy; InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, InCallButtonIds.BUTTON_SWAP_SIM, InCallButtonIds.BUTTON_COUNT, + InCallButtonIds.BUTTON_UPGRADE_TO_RTT }) public @interface InCallButtonIds { @@ -58,4 +59,5 @@ public @interface InCallButtonIds { int BUTTON_SWITCH_TO_SECONDARY = 13; int BUTTON_SWAP_SIM = 14; int BUTTON_COUNT = 15; + int BUTTON_UPGRADE_TO_RTT = 16; } diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java index db6e9009c..5239d9d34 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java @@ -56,6 +56,8 @@ public class InCallButtonIdsExtension { return "SWITCH_TO_SECONDARY"; } else if (id == InCallButtonIds.BUTTON_SWAP_SIM) { return "SWAP_SIM"; + } else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT) { + return "UPGRADE_TO_RTT"; } else { return "INVALID_BUTTON: " + id; } diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java index 9f9c5fb03..b0e23efcd 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java @@ -47,6 +47,8 @@ public interface InCallButtonUiDelegate { void changeToVideoClicked(); + void changeToRttClicked(); + void switchCameraClicked(boolean useFrontFacingCamera); void toggleCameraClicked(); diff --git a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java index 2d4ab3989..01c3950e9 100644 --- a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java +++ b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java @@ -39,7 +39,7 @@ public class AudioSelectMenu extends PopupWindow { Context context, InCallButtonUiDelegate inCallButtonUiDelegate, OnButtonClickListener onButtonClickListener) { - super(context); + super(context, null, 0, R.style.OverflowMenu); this.context = context; this.inCallButtonUiDelegate = inCallButtonUiDelegate; this.onButtonClickListener = onButtonClickListener; @@ -76,7 +76,6 @@ public class AudioSelectMenu extends PopupWindow { } item.setOnClickListener( (v) -> { - dismiss(); inCallButtonUiDelegate.setAudioRoute(itemRoute); }); } diff --git a/java/com/android/incallui/rtt/impl/RttChatAdapter.java b/java/com/android/incallui/rtt/impl/RttChatAdapter.java index 8d924c9f8..955fc9fec 100644 --- a/java/com/android/incallui/rtt/impl/RttChatAdapter.java +++ b/java/com/android/incallui/rtt/impl/RttChatAdapter.java @@ -33,7 +33,9 @@ import java.util.List; public class RttChatAdapter extends RecyclerView.Adapter<RttChatMessageViewHolder> { interface MessageListener { - void newMessageAdded(); + void onUpdateRemoteMessage(int position); + + void onUpdateLocalMessage(int position); } private static final String KEY_MESSAGE_DATA = "key_message_data"; @@ -114,7 +116,7 @@ public class RttChatAdapter extends RecyclerView.Adapter<RttChatMessageViewHolde void addLocalMessage(String message) { updateCurrentLocalMessage(message); if (messageListener != null) { - messageListener.newMessageAdded(); + messageListener.onUpdateLocalMessage(lastIndexOfLocalMessage); } } @@ -143,7 +145,7 @@ public class RttChatAdapter extends RecyclerView.Adapter<RttChatMessageViewHolde } updateCurrentRemoteMessage(message); if (messageListener != null) { - messageListener.newMessageAdded(); + messageListener.onUpdateRemoteMessage(RttChatMessage.getLastIndexRemoteMessage(rttMessages)); } } diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java index 56ac2429c..a181f88f0 100644 --- a/java/com/android/incallui/rtt/impl/RttChatFragment.java +++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java @@ -85,15 +85,6 @@ public class RttChatFragment extends Fragment private ImageButton submitButton; private boolean isClearingInput; - private final OnScrollListener onScrollListener = - new OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - if (dy < 0) { - UiUtil.hideKeyboardFrom(getContext(), editText); - } - } - }; private InCallScreenDelegate inCallScreenDelegate; private RttCallScreenDelegate rttCallScreenDelegate; private InCallButtonUiDelegate inCallButtonUiDelegate; @@ -105,6 +96,8 @@ public class RttChatFragment extends Fragment private SecondaryInfo savedSecondaryInfo; private TextView statusBanner; private PrimaryInfo primaryInfo; + private boolean isUserScrolling; + private boolean shouldAutoScrolling; /** * Create a new instance of RttChatFragment. @@ -193,7 +186,27 @@ public class RttChatFragment extends Fragment recyclerView.setHasFixedSize(false); adapter = new RttChatAdapter(getContext(), this, savedInstanceState); recyclerView.setAdapter(adapter); - recyclerView.addOnScrollListener(onScrollListener); + recyclerView.addOnScrollListener( + new OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int i) { + if (i == RecyclerView.SCROLL_STATE_DRAGGING) { + isUserScrolling = true; + } else if (i == RecyclerView.SCROLL_STATE_IDLE) { + isUserScrolling = false; + // Auto scrolling for new messages should be resumed if it's scrolled to bottom. + shouldAutoScrolling = !recyclerView.canScrollVertically(1); + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dy < 0 && isUserScrolling) { + UiUtil.hideKeyboardFrom(getContext(), editText); + } + } + }); + submitButton = view.findViewById(R.id.rtt_chat_submit_button); submitButton.setOnClickListener( v -> { @@ -202,6 +215,9 @@ public class RttChatFragment extends Fragment editText.setText(""); isClearingInput = false; rttCallScreenDelegate.onLocalMessage(Constants.BUBBLE_BREAKER); + // Auto scrolling for new messages should be resumed since user has submit current + // message. + shouldAutoScrolling = true; }); submitButton.setEnabled(false); endCallButton = view.findViewById(R.id.rtt_end_call_button); @@ -276,8 +292,21 @@ public class RttChatFragment extends Fragment } @Override - public void newMessageAdded() { - recyclerView.smoothScrollToPosition(adapter.getItemCount()); + public void onUpdateLocalMessage(int position) { + if (position < 0) { + return; + } + recyclerView.smoothScrollToPosition(position); + } + + @Override + public void onUpdateRemoteMessage(int position) { + if (position < 0) { + return; + } + if (shouldAutoScrolling) { + recyclerView.smoothScrollToPosition(position); + } } @Override diff --git a/java/com/android/incallui/rtt/impl/RttChatMessage.java b/java/com/android/incallui/rtt/impl/RttChatMessage.java index cbc53ef15..0060b1bd1 100644 --- a/java/com/android/incallui/rtt/impl/RttChatMessage.java +++ b/java/com/android/incallui/rtt/impl/RttChatMessage.java @@ -162,7 +162,7 @@ final class RttChatMessage implements Parcelable { return i; } - private static int getLastIndexRemoteMessage(List<RttChatMessage> messageList) { + static int getLastIndexRemoteMessage(List<RttChatMessage> messageList) { int i = messageList.size() - 1; while (i >= 0 && !messageList.get(i).isRemote) { i--; diff --git a/java/com/android/incallui/rtt/impl/RttOverflowMenu.java b/java/com/android/incallui/rtt/impl/RttOverflowMenu.java index 6a7aeba96..deee8ee15 100644 --- a/java/com/android/incallui/rtt/impl/RttOverflowMenu.java +++ b/java/com/android/incallui/rtt/impl/RttOverflowMenu.java @@ -42,7 +42,7 @@ public class RttOverflowMenu extends PopupWindow implements OnCheckedChangeListe Context context, InCallButtonUiDelegate inCallButtonUiDelegate, InCallScreenDelegate inCallScreenDelegate) { - super(context); + super(context, null, 0, R.style.OverflowMenu); this.inCallButtonUiDelegate = inCallButtonUiDelegate; this.inCallScreenDelegate = inCallScreenDelegate; View view = View.inflate(context, R.layout.overflow_menu, null); @@ -67,7 +67,6 @@ public class RttOverflowMenu extends PopupWindow implements OnCheckedChangeListe if (isSwitchToSecondaryButtonEnabled) { this.inCallScreenDelegate.onSecondaryInfoClicked(); } - dismiss(); }); } @@ -80,7 +79,6 @@ public class RttOverflowMenu extends PopupWindow implements OnCheckedChangeListe } else if (button == dialpadButton) { inCallButtonUiDelegate.showDialpadClicked(isChecked); } - dismiss(); } void setMuteButtonChecked(boolean isChecked) { diff --git a/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml b/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml index 614298679..2af14fd8e 100644 --- a/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml +++ b/java/com/android/incallui/rtt/impl/res/drawable/overflow_menu_background.xml @@ -18,4 +18,4 @@ android:shape="rectangle"> <solid android:color="@android:color/white"/> <corners android:radius="2dp"/> -</shape>
\ No newline at end of file +</shape> diff --git a/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml b/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml index 89b5c76f0..f098316a1 100644 --- a/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml +++ b/java/com/android/incallui/rtt/impl/res/layout/audio_route.xml @@ -20,7 +20,6 @@ android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" - android:background="@drawable/overflow_menu_background" android:orientation="vertical"> <com.android.incallui.rtt.impl.RttCheckableButton android:id="@+id/audioroute_back" diff --git a/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml b/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml index 34a99544a..ea7ff1095 100644 --- a/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml +++ b/java/com/android/incallui/rtt/impl/res/layout/frag_rtt_chat.xml @@ -23,6 +23,7 @@ android:id="@+id/rtt_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingTop="70dp" android:paddingBottom="70dp" android:clipToPadding="false"/> diff --git a/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml b/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml index eb7e38691..0ec36f33e 100644 --- a/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml +++ b/java/com/android/incallui/rtt/impl/res/layout/overflow_menu.xml @@ -20,7 +20,6 @@ android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" - android:background="@drawable/overflow_menu_background" android:orientation="vertical"> <com.android.incallui.rtt.impl.RttCheckableButton android:id="@+id/menu_mute" diff --git a/java/com/android/incallui/rtt/impl/res/values/dimens.xml b/java/com/android/incallui/rtt/impl/res/values/dimens.xml index 4c3fe02d2..a6418d70e 100644 --- a/java/com/android/incallui/rtt/impl/res/values/dimens.xml +++ b/java/com/android/incallui/rtt/impl/res/values/dimens.xml @@ -18,4 +18,5 @@ <dimen name="rtt_message_margin_top">16dp</dimen> <dimen name="rtt_same_group_message_margin_top">2dp</dimen> <dimen name="rtt_overflow_menu_width">180dp</dimen> + <dimen name="rtt_overflow_menu_elevation">8dp</dimen> </resources>
\ No newline at end of file diff --git a/java/com/android/incallui/rtt/impl/res/values/styles.xml b/java/com/android/incallui/rtt/impl/res/values/styles.xml index bbacde813..667cd1241 100644 --- a/java/com/android/incallui/rtt/impl/res/values/styles.xml +++ b/java/com/android/incallui/rtt/impl/res/values/styles.xml @@ -40,4 +40,10 @@ <item name="android:theme">@style/ButtonTheme</item> <item name="android:background">?attr/selectableItemBackground</item> </style> + + <style name="OverflowMenu"> + <item name="android:popupAnimationStyle">@android:style/Animation.Dialog</item> + <item name="android:popupBackground">@drawable/overflow_menu_background</item> + <item name="android:popupElevation">@dimen/rtt_overflow_menu_elevation</item> + </style> </resources>
\ No newline at end of file diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java index d9660e192..c4c177f39 100644 --- a/java/com/android/incallui/videotech/ims/ImsVideoTech.java +++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java @@ -203,6 +203,7 @@ public class ImsVideoTech implements VideoTech { LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest"); call.getVideoCall() .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState())); + setSessionModificationState(SessionModificationState.NO_REQUEST); logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_DECLINED); } diff --git a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java index cbf165753..5c5bae547 100644 --- a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java +++ b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java @@ -295,7 +295,7 @@ public class GetTranscriptReceiver extends BroadcastReceiver { transcriptionClientFactoryForTesting = factory; } - private static TranscriptionClientFactory getTranscriptionClientFactory(Context context) { + static TranscriptionClientFactory getTranscriptionClientFactory(Context context) { if (transcriptionClientFactoryForTesting != null) { return transcriptionClientFactoryForTesting; } diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java index f6035fd2c..034af6bfc 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java @@ -83,6 +83,16 @@ public class TranscriptionTaskAsync extends TranscriptionTask { } else if (uploadResponse == null) { VvmLog.i(TAG, "getTranscription, failed to upload voicemail."); return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); + } else if (uploadResponse.isStatusAlreadyExists()) { + VvmLog.i(TAG, "getTranscription, transcription already exists."); + GetTranscriptReceiver.beginPolling( + context, + voicemailUri, + uploadRequest.getTranscriptionId(), + 0, + configProvider, + phoneAccountHandle); + return new Pair<>(null, null); } else if (uploadResponse.getTranscriptionId() == null) { VvmLog.i(TAG, "getTranscription, upload error: " + uploadResponse.status); return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); @@ -116,7 +126,9 @@ public class TranscriptionTaskAsync extends TranscriptionTask { // Generate the transcript id locally if configured to do so, or if voicemail donation is // available (because rating donating voicemails requires locally generated voicemail ids). if (configProvider.useClientGeneratedVoicemailIds() - || configProvider.isVoicemailDonationAvailable()) { + || VoicemailComponent.get(context) + .getVoicemailClient() + .isVoicemailDonationAvailable(context, phoneAccountHandle)) { // The server currently can't handle repeated transcription id's so if we add the Uri to the // fingerprint (which contains the voicemail id) which is different each time a voicemail is // downloaded. If this becomes a problem then it should be possible to change the server diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java index ae4796dea..bd65abe84 100644 --- a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java +++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionResponse.java @@ -18,6 +18,7 @@ package com.android.voicemail.impl.transcribe.grpc; import android.support.annotation.Nullable; import com.android.dialer.common.Assert; import io.grpc.Status; +import io.grpc.Status.Code; /** * Base class for encapulating a voicemail transcription server response. This handles the Grpc @@ -43,6 +44,14 @@ public abstract class TranscriptionResponse { return false; } + public boolean isStatusAlreadyExists() { + if (status != null) { + return status.getCode() == Code.ALREADY_EXISTS; + } + + return false; + } + public boolean hasFatalError() { if (status != null) { return status.getCode() != Status.Code.OK && status.getCode() != Status.Code.UNAVAILABLE; |