diff options
author | Eric Erfanian <erfanian@google.com> | 2018-05-07 14:58:35 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2018-05-07 14:58:35 +0000 |
commit | 84b862acd2c53f665386f1dfd23b57c2bdcf5baf (patch) | |
tree | 78bf30ec2633948a69facf84ebfa84a979d814f3 /java | |
parent | 95c2775dd2b77f9fce454d655d2b23ed700d7f22 (diff) | |
parent | 763ce4404ae8e4199da719359a1b01389884cec4 (diff) |
Merge changes Ibab0bc7e,I1cb26187,I64225b78,I4806ab65,I7d431e74, ...
* changes:
Remove TODOs for showing SIM info in the bottom sheet & call details.
Use Telecom Bluetooth API instead of system Bluetooth API.
Update audio route after user select different audio route.
Clear NewCallLogViewHolder.onClickListener if row is not callable.
Filter out unnecessary bottom sheet options for a call to a voicemail box.
Aosp fix for v28-support-prelease bottom sheet.
Mark photo info as voicemails in bottom sheet
Simplify how we build bottom sheet options (a.k.a. modules).
Add test to verify no crash on multiple DialerCall#onRemovedFromCallList.
Add spam status tests for CallList#onCallAdded
More refactoring
Diffstat (limited to 'java')
28 files changed, 817 insertions, 847 deletions
diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java index bb0d3c625..ca8ed29ec 100644 --- a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java +++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java @@ -23,11 +23,6 @@ import java.lang.reflect.Field; /** Compatibility class for {@link android.telecom.TelecomManager}. */ public class TelecomManagerCompat { - // TODO(mdooley): remove once this is available in android.telecom.Call - // a bug - public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = - "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; - // Constants from http://cs/android/frameworks/base/telecomm/java/android/telecom/Call.java. public static final String EVENT_REQUEST_HANDOVER = "android.telecom.event.REQUEST_HANDOVER"; public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE = diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 5fed683f6..cad2eb7e0 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -41,7 +41,6 @@ import com.android.dialer.spam.SpamComponent; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; -import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; import com.android.incallui.calllocation.CallLocationComponent; import com.android.incallui.maps.MapsComponent; import com.android.incallui.speakeasy.SpeakEasyComponent; @@ -53,7 +52,6 @@ import com.android.voicemail.VoicemailComponent; */ public interface BaseDialerRootComponent extends ActiveCallsComponent.HasComponent, - BluetoothDeviceProviderComponent.HasComponent, BubbleComponent.HasComponent, CallLocationComponent.HasComponent, CallLogComponent.HasComponent, diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java index cb84a28c2..cd1752d74 100644 --- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java +++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java @@ -192,8 +192,6 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder nameView.setText(headerInfo.getPrimaryText()); numberView.setText(headerInfo.getSecondaryText()); - // TODO(a bug): show SIM info in the TextView returned by getNetworkView(). - setCallbackAction(callbackAction); } diff --git a/java/com/android/dialer/calldetails/proto/call_details_header_info.proto b/java/com/android/dialer/calldetails/proto/call_details_header_info.proto index ea7ba1e72..e2532d504 100644 --- a/java/com/android/dialer/calldetails/proto/call_details_header_info.proto +++ b/java/com/android/dialer/calldetails/proto/call_details_header_info.proto @@ -31,6 +31,4 @@ message CallDetailsHeaderInfo { // "Blocked • Mobile • 555-1234", and // "Spam • Mobile • 555-1234". optional string secondary_text = 4; - - // TODO(a bug): Add SIM info. }
\ No newline at end of file diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java index 44a08c75e..c02d80ede 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java @@ -268,6 +268,7 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { private void setOnClickListenerForRow(CoalescedRow row) { if (!PhoneNumberHelper.canPlaceCallsTo( row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) { + itemView.setOnClickListener(null); return; } itemView.setOnClickListener(view -> CallLogRowActions.startCallForRow(activity, row)); diff --git a/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java b/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java index d87888d34..4e25cedf4 100644 --- a/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java +++ b/java/com/android/dialer/calllog/ui/menu/BottomSheetHeader.java @@ -34,6 +34,7 @@ final class BottomSheetHeader { NumberAttributesConverter.toPhotoInfoBuilder(row.getNumberAttributes()) .setFormattedNumber(row.getFormattedNumber()) .setIsVideo((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) + .setIsVoicemail(row.getIsVoicemailCall()) .setIsRtt( BuildCompat.isAtLeastP() && (row.getFeatures() & Calls.FEATURES_RTT) == Calls.FEATURES_RTT) diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java index a56d6d5e2..b06e0fb1a 100644 --- a/java/com/android/dialer/calllog/ui/menu/Modules.java +++ b/java/com/android/dialer/calllog/ui/menu/Modules.java @@ -20,152 +20,53 @@ import android.content.Context; import android.provider.CallLog.Calls; import android.support.v4.os.BuildCompat; import android.text.TextUtils; -import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; import com.android.dialer.calldetails.CallDetailsActivity; import com.android.dialer.calldetails.CallDetailsHeaderInfo; -import com.android.dialer.callintent.CallInitiationType; -import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.calllog.model.CoalescedRow; import com.android.dialer.calllogutils.CallLogEntryText; import com.android.dialer.calllogutils.NumberAttributesConverter; -import com.android.dialer.duo.Duo; -import com.android.dialer.duo.DuoComponent; import com.android.dialer.glidephotomanager.PhotoInfo; -import com.android.dialer.historyitemactions.DividerModule; -import com.android.dialer.historyitemactions.DuoCallModule; import com.android.dialer.historyitemactions.HistoryItemActionModule; +import com.android.dialer.historyitemactions.HistoryItemActionModuleInfo; +import com.android.dialer.historyitemactions.HistoryItemActionModulesBuilder; import com.android.dialer.historyitemactions.IntentModule; -import com.android.dialer.historyitemactions.SharedModules; -import com.android.dialer.logging.ReportingLocation; import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import com.android.dialer.util.CallUtil; -import com.google.common.base.Optional; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** - * Configures the modules for the bottom sheet; these are the rows below the top row (primary - * action) in the bottom sheet. + * Configures the modules for the bottom sheet; these are the rows below the top row (contact info) + * in the bottom sheet. */ -@SuppressWarnings("Guava") final class Modules { + /** + * Returns a list of {@link HistoryItemActionModule HistoryItemActionModules}, which are items in + * the bottom sheet. + */ static List<HistoryItemActionModule> fromRow(Context context, CoalescedRow row) { - // Conditionally add each module, which are items in the bottom sheet's menu. - List<HistoryItemActionModule> modules = new ArrayList<>(); - - String normalizedNumber = row.getNumber().getNormalizedNumber(); - boolean canPlaceCalls = - PhoneNumberHelper.canPlaceCallsTo(normalizedNumber, row.getNumberPresentation()); - - if (canPlaceCalls) { - modules.addAll(createModulesForCalls(context, row, normalizedNumber)); - Optional<HistoryItemActionModule> moduleForSendingTextMessage = - SharedModules.createModuleForSendingTextMessage( - context, normalizedNumber, row.getNumberAttributes().getIsBlocked()); - if (moduleForSendingTextMessage.isPresent()) { - modules.add(moduleForSendingTextMessage.get()); - } - } - - if (!modules.isEmpty()) { - modules.add(new DividerModule()); - } + HistoryItemActionModulesBuilder modulesBuilder = + new HistoryItemActionModulesBuilder(context, buildModuleInfo(row)); // TODO(zachh): Module for CallComposer. - if (canPlaceCalls) { - Optional<HistoryItemActionModule> moduleForAddingToContacts = - SharedModules.createModuleForAddingToContacts( - context, - row.getNumber(), - row.getNumberAttributes().getName(), - row.getNumberAttributes().getLookupUri(), - row.getNumberAttributes().getIsBlocked(), - row.getNumberAttributes().getIsSpam()); - if (moduleForAddingToContacts.isPresent()) { - modules.add(moduleForAddingToContacts.get()); - } - - BlockReportSpamDialogInfo blockReportSpamDialogInfo = - BlockReportSpamDialogInfo.newBuilder() - .setNormalizedNumber(row.getNumber().getNormalizedNumber()) - .setCountryIso(row.getNumber().getCountryIso()) - .setCallType(row.getCallType()) - .setReportingLocation(ReportingLocation.Type.CALL_LOG_HISTORY) - .setContactSource(row.getNumberAttributes().getContactSource()) - .build(); - modules.addAll( - SharedModules.createModulesHandlingBlockedOrSpamNumber( - context, - blockReportSpamDialogInfo, - row.getNumberAttributes().getIsBlocked(), - row.getNumberAttributes().getIsSpam())); - - Optional<HistoryItemActionModule> moduleForCopyingNumber = - SharedModules.createModuleForCopyingNumber(context, normalizedNumber); - if (moduleForCopyingNumber.isPresent()) { - modules.add(moduleForCopyingNumber.get()); - } + if (PhoneNumberHelper.canPlaceCallsTo( + row.getNumber().getNormalizedNumber(), row.getNumberPresentation())) { + modulesBuilder + .addModuleForVoiceCall() + .addModuleForVideoCall() + .addModuleForSendingTextMessage() + .addModuleForDivider() + .addModuleForAddingToContacts() + .addModuleForBlockedOrSpamNumber() + .addModuleForCopyingNumber(); } - modules.add(createModuleForAccessingCallDetails(context, row)); + List<HistoryItemActionModule> modules = modulesBuilder.build(); + // Add modules only available in the call log. + modules.add(createModuleForAccessingCallDetails(context, row)); modules.add(new DeleteCallLogItemModule(context, row.getCoalescedIds())); - - return modules; - } - - private static List<HistoryItemActionModule> createModulesForCalls( - Context context, CoalescedRow row, String normalizedNumber) { - // Don't add call options if a number is blocked. - if (row.getNumberAttributes().getIsBlocked()) { - return Collections.emptyList(); - } - - boolean isDuoCall = - DuoComponent.get(context).getDuo().isDuoAccount(row.getPhoneAccountComponentName()); - - List<HistoryItemActionModule> modules = new ArrayList<>(); - - // Add an audio call item - // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber. - CallIntentBuilder callIntentBuilder = - new CallIntentBuilder(normalizedNumber, CallInitiationType.Type.CALL_LOG) - .setAllowAssistedDial(canSupportAssistedDialing(row)); - // Leave PhoneAccountHandle blank so regular PreCall logic will be used. The account the call - // was made/received in should be ignored for audio and carrier video calls. - // TODO(a bug): figure out the correct video call behavior - modules.add(IntentModule.newCallModule(context, callIntentBuilder)); - - // If the call log entry is for a spam call, nothing more to be done. - if (row.getNumberAttributes().getIsSpam()) { - return modules; - } - - // If the call log entry is for a video call, add the corresponding video call options. - // Note that if the entry is for a Duo video call but Duo is not available, we will fall back to - // a carrier video call. - if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { - modules.add( - isDuoCall && canPlaceDuoCall(context, normalizedNumber) - ? new DuoCallModule(context, normalizedNumber) - : IntentModule.newCallModule(context, callIntentBuilder.setIsVideoCall(true))); - return modules; - } - - // At this point, the call log entry is for an audio call. We will also show a video call option - // if the video capability is present. - // - // The carrier video call option takes precedence over Duo. - if (canPlaceCarrierVideoCall(context, row)) { - modules.add(IntentModule.newCallModule(context, callIntentBuilder.setIsVideoCall(true))); - } else if (canPlaceDuoCall(context, normalizedNumber)) { - modules.add(new DuoCallModule(context, normalizedNumber)); - } - return modules; } @@ -208,30 +109,27 @@ final class Modules { .build(); } - private static boolean canPlaceDuoCall(Context context, String phoneNumber) { - Duo duo = DuoComponent.get(context).getDuo(); - - return duo.isInstalled(context) - && duo.isEnabled(context) - && duo.isActivated(context) - && duo.isReachable(context, phoneNumber); - } - - private static boolean canPlaceCarrierVideoCall(Context context, CoalescedRow row) { - int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context); - boolean isCarrierVideoCallingEnabled = - ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) - == CallUtil.VIDEO_CALLING_ENABLED); - boolean canRelyOnCarrierVideoPresence = - ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) - == CallUtil.VIDEO_CALLING_PRESENCE); - - return isCarrierVideoCallingEnabled - && canRelyOnCarrierVideoPresence - && row.getNumberAttributes().getCanSupportCarrierVideoCall(); - } - private static boolean canSupportAssistedDialing(CoalescedRow row) { return !TextUtils.isEmpty(row.getNumberAttributes().getLookupUri()); } + + private static HistoryItemActionModuleInfo buildModuleInfo(CoalescedRow row) { + return HistoryItemActionModuleInfo.newBuilder() + .setNormalizedNumber(row.getNumber().getNormalizedNumber()) + .setCountryIso(row.getNumber().getCountryIso()) + .setName(row.getNumberAttributes().getName()) + .setCallType(row.getCallType()) + .setFeatures(row.getFeatures()) + .setLookupUri(row.getNumberAttributes().getLookupUri()) + .setPhoneAccountComponentName(row.getPhoneAccountComponentName()) + .setCanReportAsInvalidNumber(row.getNumberAttributes().getCanReportAsInvalidNumber()) + .setCanSupportAssistedDialing(canSupportAssistedDialing(row)) + .setCanSupportCarrierVideoCall(row.getNumberAttributes().getCanSupportCarrierVideoCall()) + .setIsBlocked(row.getNumberAttributes().getIsBlocked()) + .setIsSpam(row.getNumberAttributes().getIsSpam()) + .setIsVoicemailCall(row.getIsVoicemailCall()) + .setContactSource(row.getNumberAttributes().getContactSource()) + .setHost(HistoryItemActionModuleInfo.Host.CALL_LOG) + .build(); + } } diff --git a/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java b/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java index ce95c5700..54e9c9aff 100644 --- a/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java +++ b/java/com/android/dialer/configprovider/SharedPrefConfigProvider.java @@ -98,6 +98,10 @@ public class SharedPrefConfigProvider implements ConfigProvider { sharedPreferences.edit().putBoolean(PREF_PREFIX + key, value).apply(); } + public void putLong(String key, long value) { + sharedPreferences.edit().putLong(PREF_PREFIX + key, value).apply(); + } + @Override public String getString(String key, String defaultValue) { // Reading shared prefs on the main thread is generally safe since a single instance is cached. diff --git a/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java b/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java new file mode 100644 index 000000000..396c03376 --- /dev/null +++ b/java/com/android/dialer/historyitemactions/BlockReportSpamModules.java @@ -0,0 +1,120 @@ +/* + * 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.historyitemactions; + +import android.content.Context; +import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; +import com.android.dialer.blockreportspam.ShowBlockReportSpamDialogNotifier; + +/** Modules for blocking/unblocking a number and/or reporting it as spam/not spam. */ +final class BlockReportSpamModules { + + private BlockReportSpamModules() {} + + static HistoryItemActionModule moduleForMarkingNumberAsNotSpam( + Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { + + return new HistoryItemActionModule() { + @Override + public int getStringId() { + return R.string.not_spam; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_report_off_vd_theme_24; + } + + @Override + public boolean onClick() { + ShowBlockReportSpamDialogNotifier.notifyShowDialogToReportNotSpam( + context, blockReportSpamDialogInfo); + return true; // Close the bottom sheet. + } + }; + } + + static HistoryItemActionModule moduleForBlockingNumber( + Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { + + return new HistoryItemActionModule() { + @Override + public int getStringId() { + return R.string.block_number; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_block_vd_theme_24; + } + + @Override + public boolean onClick() { + ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumber( + context, blockReportSpamDialogInfo); + return true; // Close the bottom sheet. + } + }; + } + + static HistoryItemActionModule moduleForUnblockingNumber( + Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { + + return new HistoryItemActionModule() { + @Override + public int getStringId() { + return R.string.unblock_number; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_unblock_vd_theme_24; + } + + @Override + public boolean onClick() { + ShowBlockReportSpamDialogNotifier.notifyShowDialogToUnblockNumber( + context, blockReportSpamDialogInfo); + + return true; // Close the bottom sheet. + } + }; + } + + static HistoryItemActionModule moduleForBlockingNumberAndOptionallyReportingSpam( + Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { + + return new HistoryItemActionModule() { + @Override + public int getStringId() { + return R.string.block_and_optionally_report_spam; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_block_vd_theme_24; + } + + @Override + public boolean onClick() { + ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumberAndOptionallyReportSpam( + context, blockReportSpamDialogInfo); + return true; // Close the bottom sheet. + } + }; + } +} diff --git a/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java new file mode 100644 index 000000000..9af08be50 --- /dev/null +++ b/java/com/android/dialer/historyitemactions/HistoryItemActionModulesBuilder.java @@ -0,0 +1,422 @@ +/* + * 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.historyitemactions; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract; +import android.text.TextUtils; +import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; +import com.android.dialer.callintent.CallInitiationType; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.clipboard.ClipboardUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.duo.Duo; +import com.android.dialer.duo.DuoComponent; +import com.android.dialer.logging.ReportingLocation; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.UriUtils; +import java.util.ArrayList; +import java.util.List; + +/** + * Builds a list of {@link HistoryItemActionModule HistoryItemActionModules}. + * + * <p>Example usage: + * + * <pre><code> + * // Create a HistoryItemActionModuleInfo proto with the information you have. + * // You can simply skip a field if there is no information for it. + * HistoryItemActionModuleInfo moduleInfo = + * HistoryItemActionModuleInfo.newBuilder() + * .setNormalizedNumber("+16502530000") + * .setCountryIso("US") + * .setName("Google") + * .build(); + * + * // Initialize the builder using the module info above. + * // Note that some modules require an activity context to work so it is preferred to pass one + * // instead of an application context to the builder. + * HistoryItemActionModulesBuilder modulesBuilder = + * new HistoryItemActionModulesBuilder(activityContext, moduleInfo); + * + * // Add all modules you want in the order you like. + * // If a module shouldn't be added according to the module info, it won't be. + * // For example, if the module info is not for a video call and doesn't indicate the presence + * // of video calling capabilities, calling addModuleForVideoCall() is a no-op. + * modulesBuilder + * .addModuleForVoiceCall() + * .addModuleForVideoCall() + * .addModuleForSendingTextMessage() + * .addModuleForDivider() + * .addModuleForAddingToContacts() + * .addModuleForBlockedOrSpamNumber() + * .addModuleForCopyingNumber(); + * + * List<HistoryItemActionModule> modules = modulesBuilder.build(); + * </code></pre> + */ +public final class HistoryItemActionModulesBuilder { + + private final Context context; + private final HistoryItemActionModuleInfo moduleInfo; + private final List<HistoryItemActionModule> modules; + + public HistoryItemActionModulesBuilder(Context context, HistoryItemActionModuleInfo moduleInfo) { + Assert.checkArgument( + moduleInfo.getHost() != HistoryItemActionModuleInfo.Host.UNKNOWN, + "A host must be specified."); + + this.context = context; + this.moduleInfo = moduleInfo; + this.modules = new ArrayList<>(); + } + + public List<HistoryItemActionModule> build() { + return new ArrayList<>(modules); + } + + /** + * Adds a module for placing a voice call. + * + * <p>The method is a no-op if the number is blocked. + */ + public HistoryItemActionModulesBuilder addModuleForVoiceCall() { + if (moduleInfo.getIsBlocked()) { + return this; + } + + // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber. + // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to + // place or receive the call should be ignored for voice calls. + CallIntentBuilder callIntentBuilder = + new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType()) + .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing()); + modules.add(IntentModule.newCallModule(context, callIntentBuilder)); + return this; + } + + /** + * Adds a module for a carrier video call *or* a Duo video call. + * + * <p>This method is a no-op if + * + * <ul> + * <li>the call is one made to a voicemail box, + * <li>the number is blocked, or + * <li>the number is marked as spam. + * </ul> + * + * <p>If the provided module info is for a Duo video call and Duo is available, add a Duo video + * call module. + * + * <p>If the provided module info is for a Duo video call but Duo is unavailable, add a carrier + * video call module. + * + * <p>If the provided module info is for a carrier video call, add a carrier video call module. + * + * <p>If the provided module info is for a voice call and the device has carrier video call + * capability, add a carrier video call module. + * + * <p>If the provided module info is for a voice call, the device doesn't have carrier video call + * capability, and Duo is available, add a Duo video call module. + */ + public HistoryItemActionModulesBuilder addModuleForVideoCall() { + if (moduleInfo.getIsVoicemailCall() || moduleInfo.getIsBlocked() || moduleInfo.getIsSpam()) { + return this; + } + + // Do not set PhoneAccountHandle so that regular PreCall logic will be used. The account used to + // place or receive the call should be ignored for carrier video calls. + // TODO(a bug): figure out the correct video call behavior + HistoryItemActionModule carrierVideoCallModule = + IntentModule.newCallModule( + context, + new CallIntentBuilder(moduleInfo.getNormalizedNumber(), getCallInitiationType()) + .setAllowAssistedDial(moduleInfo.getCanSupportAssistedDialing()) + .setIsVideoCall(true)); + HistoryItemActionModule duoVideoCallModule = + new DuoCallModule(context, moduleInfo.getNormalizedNumber()); + + // If the module info is for a video call, add an appropriate video call module. + if ((moduleInfo.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { + modules.add(isDuoCall() && canPlaceDuoCall() ? duoVideoCallModule : carrierVideoCallModule); + return this; + } + + // At this point, the module info is for an audio call. We will also add a video call module if + // the video capability is present. + // + // The carrier video call module takes precedence over the Duo module. + if (canPlaceCarrierVideoCall()) { + modules.add(carrierVideoCallModule); + } else if (canPlaceDuoCall()) { + modules.add(duoVideoCallModule); + } + return this; + } + + /** + * Adds a module for sending text messages. + * + * <p>The method is a no-op if + * + * <ul> + * <li>the call is one made to a voicemail box, + * <li>the number is blocked, or + * <li>the number is empty. + * </ul> + */ + public HistoryItemActionModulesBuilder addModuleForSendingTextMessage() { + // TODO(zachh): There are other conditions where this module should not be shown + // (e.g., business numbers). + if (moduleInfo.getIsVoicemailCall() + || moduleInfo.getIsBlocked() + || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { + return this; + } + + modules.add( + IntentModule.newModuleForSendingTextMessage(context, moduleInfo.getNormalizedNumber())); + return this; + } + + /** + * Adds a module for a divider. + * + * <p>The method is a no-op if the divider module will be the first module. + */ + public HistoryItemActionModulesBuilder addModuleForDivider() { + if (modules.isEmpty()) { + return this; + } + + modules.add(new DividerModule()); + return this; + } + + /** + * Adds a module for adding a number to Contacts. + * + * <p>The method is a no-op if + * + * <ul> + * <li>the call is one made to a voicemail box, + * <li>the number is blocked, + * <li>the number is marked as spam, + * <li>the number is empty, or + * <li>the number belongs to an existing contact. + * </ul> + */ + public HistoryItemActionModulesBuilder addModuleForAddingToContacts() { + if (moduleInfo.getIsVoicemailCall() + || moduleInfo.getIsBlocked() + || moduleInfo.getIsSpam() + || isExistingContact() + || TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { + return this; + } + + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(ContactsContract.Intents.Insert.PHONE, moduleInfo.getNormalizedNumber()); + + if (!TextUtils.isEmpty(moduleInfo.getName())) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, moduleInfo.getName()); + } + + modules.add( + new IntentModule( + context, + intent, + R.string.add_to_contacts, + R.drawable.quantum_ic_person_add_vd_theme_24)); + return this; + } + + /** + * Add modules for blocking/unblocking a number and/or marking it as spam/not spam. + * + * <p>The method is a no-op if the call is one made to a voicemail box. + * + * <p>If a number is marked as spam, add two modules: + * + * <ul> + * <li>"Not spam" and "Block", or + * <li>"Not spam" and "Unblock". + * </ul> + * + * <p>If a number is blocked but not marked as spam, add the "Unblock" module. + * + * <p>If a number is not blocked or marked as spam, add the "Block/Report spam" module. + */ + public HistoryItemActionModulesBuilder addModuleForBlockedOrSpamNumber() { + if (moduleInfo.getIsVoicemailCall()) { + return this; + } + + BlockReportSpamDialogInfo blockReportSpamDialogInfo = + BlockReportSpamDialogInfo.newBuilder() + .setNormalizedNumber(moduleInfo.getNormalizedNumber()) + .setCountryIso(moduleInfo.getCountryIso()) + .setCallType(moduleInfo.getCallType()) + .setReportingLocation(getReportingLocation()) + .setContactSource(moduleInfo.getContactSource()) + .build(); + + // For a spam number, add two modules: + // (1) "Not spam" and "Block", or + // (2) "Not spam" and "Unblock". + if (moduleInfo.getIsSpam()) { + modules.add( + BlockReportSpamModules.moduleForMarkingNumberAsNotSpam( + context, blockReportSpamDialogInfo)); + modules.add( + moduleInfo.getIsBlocked() + ? BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo) + : BlockReportSpamModules.moduleForBlockingNumber(context, blockReportSpamDialogInfo)); + return this; + } + + // For a blocked non-spam number, add the "Unblock" module. + if (moduleInfo.getIsBlocked()) { + modules.add( + BlockReportSpamModules.moduleForUnblockingNumber(context, blockReportSpamDialogInfo)); + return this; + } + + // For a number that is neither a spam number nor blocked, add the "Block/Report spam" module. + modules.add( + BlockReportSpamModules.moduleForBlockingNumberAndOptionallyReportingSpam( + context, blockReportSpamDialogInfo)); + return this; + } + + /** + * Adds a module for copying a number. + * + * <p>The method is a no-op if the number is empty. + */ + public HistoryItemActionModulesBuilder addModuleForCopyingNumber() { + if (TextUtils.isEmpty(moduleInfo.getNormalizedNumber())) { + return this; + } + + modules.add( + new HistoryItemActionModule() { + @Override + public int getStringId() { + return R.string.copy_number; + } + + @Override + public int getDrawableId() { + return R.drawable.quantum_ic_content_copy_vd_theme_24; + } + + @Override + public boolean onClick() { + ClipboardUtils.copyText( + context, + /* label = */ null, + moduleInfo.getNormalizedNumber(), + /* showToast = */ true); + return false; + } + }); + return this; + } + + private boolean canPlaceCarrierVideoCall() { + int carrierVideoAvailability = CallUtil.getVideoCallingAvailability(context); + boolean isCarrierVideoCallingEnabled = + ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) + == CallUtil.VIDEO_CALLING_ENABLED); + boolean canRelyOnCarrierVideoPresence = + ((carrierVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) + == CallUtil.VIDEO_CALLING_PRESENCE); + + return isCarrierVideoCallingEnabled + && canRelyOnCarrierVideoPresence + && moduleInfo.getCanSupportCarrierVideoCall(); + } + + private boolean isDuoCall() { + return DuoComponent.get(context) + .getDuo() + .isDuoAccount(moduleInfo.getPhoneAccountComponentName()); + } + + private boolean canPlaceDuoCall() { + Duo duo = DuoComponent.get(context).getDuo(); + + return duo.isInstalled(context) + && duo.isEnabled(context) + && duo.isActivated(context) + && duo.isReachable(context, moduleInfo.getNormalizedNumber()); + } + + /** + * Lookup URIs are currently fetched from the cached column of the system call log. This URI + * contains encoded information for non-contacts for the purposes of populating contact cards. + * + * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or + * not. + * + * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the + * cached column in the system database, in case we decide not to overload the column. + */ + private boolean isExistingContact() { + return !TextUtils.isEmpty(moduleInfo.getLookupUri()) + && !UriUtils.isEncodedContactUri(Uri.parse(moduleInfo.getLookupUri())); + } + + /** + * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link + * CallInitiationType.Type}, which is required by {@link CallIntentBuilder} to build a call + * intent. + */ + private CallInitiationType.Type getCallInitiationType() { + switch (moduleInfo.getHost()) { + case CALL_LOG: + return CallInitiationType.Type.CALL_LOG; + case VOICEMAIL: + return CallInitiationType.Type.VOICEMAIL_LOG; + default: + throw Assert.createUnsupportedOperationFailException( + String.format("Unsupported host: %s", moduleInfo.getHost())); + } + } + + /** + * Maps the value of {@link HistoryItemActionModuleInfo#getHost()} to {@link + * ReportingLocation.Type}, which is for logging where a spam number is reported. + */ + private ReportingLocation.Type getReportingLocation() { + switch (moduleInfo.getHost()) { + case CALL_LOG: + return ReportingLocation.Type.CALL_LOG_HISTORY; + case VOICEMAIL: + return ReportingLocation.Type.VOICEMAIL_HISTORY; + default: + throw Assert.createUnsupportedOperationFailException( + String.format("Unsupported host: %s", moduleInfo.getHost())); + } + } +} diff --git a/java/com/android/dialer/historyitemactions/IntentModule.java b/java/com/android/dialer/historyitemactions/IntentModule.java index f73d4c951..dc53064af 100644 --- a/java/com/android/dialer/historyitemactions/IntentModule.java +++ b/java/com/android/dialer/historyitemactions/IntentModule.java @@ -23,6 +23,7 @@ import android.support.annotation.StringRes; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.precall.PreCall; import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; /** * {@link HistoryItemActionModule} useful for making easy to build modules based on starting an @@ -73,4 +74,12 @@ public class IntentModule implements HistoryItemActionModule { return new IntentModule(context, PreCall.getIntent(context, callIntentBuilder), text, image); } + + public static IntentModule newModuleForSendingTextMessage(Context context, String number) { + return new IntentModule( + context, + IntentUtil.getSendSmsIntent(number), + R.string.send_a_message, + R.drawable.quantum_ic_message_vd_theme_24); + } } diff --git a/java/com/android/dialer/historyitemactions/SharedModules.java b/java/com/android/dialer/historyitemactions/SharedModules.java deleted file mode 100644 index 8604bed1d..000000000 --- a/java/com/android/dialer/historyitemactions/SharedModules.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.dialer.historyitemactions; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import com.android.dialer.DialerPhoneNumber; -import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; -import com.android.dialer.blockreportspam.ShowBlockReportSpamDialogNotifier; -import com.android.dialer.clipboard.ClipboardUtils; -import com.android.dialer.util.IntentUtil; -import com.android.dialer.util.UriUtils; -import com.google.common.base.Optional; -import java.util.ArrayList; -import java.util.List; - -/** - * Modules for the bottom sheet that are shared between NewVoicemailFragment and NewCallLogFragment - */ -@SuppressWarnings("Guava") -public class SharedModules { - - public static Optional<HistoryItemActionModule> createModuleForAddingToContacts( - Context context, - DialerPhoneNumber dialerPhoneNumber, - String name, - String lookupUri, - boolean isBlocked, - boolean isSpam) { - // Skip showing the menu item for a spam/blocked number. - if (isBlocked || isSpam) { - return Optional.absent(); - } - - // Skip showing the menu item for existing contacts. - if (isExistingContact(lookupUri)) { - return Optional.absent(); - } - - // Skip showing the menu item if there is no number. - String normalizedNumber = dialerPhoneNumber.getNormalizedNumber(); - if (TextUtils.isEmpty(normalizedNumber)) { - return Optional.absent(); - } - - Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); - intent.putExtra(ContactsContract.Intents.Insert.PHONE, normalizedNumber); - - if (!TextUtils.isEmpty(name)) { - intent.putExtra(ContactsContract.Intents.Insert.NAME, name); - } - - return Optional.of( - new IntentModule( - context, - intent, - R.string.add_to_contacts, - R.drawable.quantum_ic_person_add_vd_theme_24)); - } - - /** - * Lookup URIs are currently fetched from the cached column of the system call log. This URI - * contains encoded information for non-contacts for the purposes of populating contact cards. - * - * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or - * not. - * - * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the - * cached column in the system database, in case we decide not to overload the column. - */ - private static boolean isExistingContact(@Nullable String lookupUri) { - return !TextUtils.isEmpty(lookupUri) && !UriUtils.isEncodedContactUri(Uri.parse(lookupUri)); - } - - public static Optional<HistoryItemActionModule> createModuleForSendingTextMessage( - Context context, String normalizedNumber, boolean isBlocked) { - // Don't show the option to send a text message if the number is blocked. - if (isBlocked) { - return Optional.absent(); - } - - // TODO(zachh): There are some conditions where this module should not be shown; consider - // voicemail, business numbers, etc. - - return !TextUtils.isEmpty(normalizedNumber) - ? Optional.of( - new IntentModule( - context, - IntentUtil.getSendSmsIntent(normalizedNumber), - R.string.send_a_message, - R.drawable.quantum_ic_message_vd_theme_24)) - : Optional.absent(); - } - - /** - * Create modules related to blocking/unblocking a number and/or reporting it as spam/not spam. - */ - public static List<HistoryItemActionModule> createModulesHandlingBlockedOrSpamNumber( - Context context, - BlockReportSpamDialogInfo blockReportSpamDialogInfo, - boolean isBlocked, - boolean isSpam) { - List<HistoryItemActionModule> modules = new ArrayList<>(); - - // For a spam number, add two options: - // (1) "Not spam" and "Block", or - // (2) "Not spam" and "Unblock". - if (isSpam) { - modules.add(createModuleForMarkingNumberAsNonSpam(context, blockReportSpamDialogInfo)); - modules.add( - createModuleForBlockingOrUnblockingNumber(context, blockReportSpamDialogInfo, isBlocked)); - return modules; - } - - // For a blocked non-spam number, add "Unblock" option. - if (isBlocked) { - modules.add( - createModuleForBlockingOrUnblockingNumber(context, blockReportSpamDialogInfo, isBlocked)); - return modules; - } - - // For a number that is neither a spam number nor blocked, add "Block/Report spam" option. - modules.add( - createModuleForBlockingNumberAndOptionallyReportingSpam( - context, blockReportSpamDialogInfo)); - return modules; - } - - /** Create "Not spam" module. */ - private static HistoryItemActionModule createModuleForMarkingNumberAsNonSpam( - Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { - return new HistoryItemActionModule() { - @Override - public int getStringId() { - return R.string.not_spam; - } - - @Override - public int getDrawableId() { - return R.drawable.quantum_ic_report_off_vd_theme_24; - } - - @Override - public boolean onClick() { - ShowBlockReportSpamDialogNotifier.notifyShowDialogToReportNotSpam( - context, blockReportSpamDialogInfo); - return true; // Close the bottom sheet. - } - }; - } - - private static HistoryItemActionModule createModuleForBlockingOrUnblockingNumber( - Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo, boolean isBlocked) { - return new HistoryItemActionModule() { - @Override - public int getStringId() { - return isBlocked ? R.string.unblock_number : R.string.block_number; - } - - @Override - public int getDrawableId() { - return isBlocked - ? R.drawable.quantum_ic_unblock_vd_theme_24 - : R.drawable.quantum_ic_block_vd_theme_24; - } - - @Override - public boolean onClick() { - if (isBlocked) { - ShowBlockReportSpamDialogNotifier.notifyShowDialogToUnblockNumber( - context, blockReportSpamDialogInfo); - } else { - ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumber( - context, blockReportSpamDialogInfo); - } - return true; // Close the bottom sheet. - } - }; - } - - /** Create "Block/Report spam" module */ - private static HistoryItemActionModule createModuleForBlockingNumberAndOptionallyReportingSpam( - Context context, BlockReportSpamDialogInfo blockReportSpamDialogInfo) { - return new HistoryItemActionModule() { - @Override - public int getStringId() { - return R.string.block_and_optionally_report_spam; - } - - @Override - public int getDrawableId() { - return R.drawable.quantum_ic_block_vd_theme_24; - } - - @Override - public boolean onClick() { - ShowBlockReportSpamDialogNotifier.notifyShowDialogToBlockNumberAndOptionallyReportSpam( - context, blockReportSpamDialogInfo); - return true; // Close the bottom sheet. - } - }; - } - - public static Optional<HistoryItemActionModule> createModuleForCopyingNumber( - Context context, String normalizedNumber) { - if (TextUtils.isEmpty(normalizedNumber)) { - return Optional.absent(); - } - return Optional.of( - new HistoryItemActionModule() { - @Override - public int getStringId() { - return R.string.copy_number; - } - - @Override - public int getDrawableId() { - return R.drawable.quantum_ic_content_copy_vd_theme_24; - } - - @Override - public boolean onClick() { - ClipboardUtils.copyText(context, null, normalizedNumber, true); - return false; - } - }); - } -} diff --git a/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto b/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto new file mode 100644 index 000000000..99071a7cd --- /dev/null +++ b/java/com/android/dialer/historyitemactions/history_item_action_module_info.proto @@ -0,0 +1,69 @@ +syntax = "proto2"; + +option java_package = "com.android.dialer.historyitemactions"; +option java_multiple_files = true; +option optimize_for = LITE_RUNTIME; + + +package com.android.dialer.historyitemactions; + +import "java/com/android/dialer/logging/contact_source.proto"; + +// Contains information needed to construct items (modules) in a bottom sheet. +// Next ID: 16 +message HistoryItemActionModuleInfo { + // The dialer-normalized version of a phone number. + // See DialerPhoneNumber.normalized_number. + optional string normalized_number = 1; + + // The ISO 3166-1 two letters country code of the number. + optional string country_iso = 2; + + // The name associated with the number. + optional string name = 3; + + // The type of the call. + // See android.provider.CallLog.Calls.TYPE. + optional int32 call_type = 4; + + // Bit-mask describing features of the call. + // See android.provider.CallLog.Calls.FEATURES. + optional int32 features = 5; + + // The Contacts Provider lookup URI for the contact associated with the + // number. + optional string lookup_uri = 6; + + // The component name of the account used to place or receive the call. + // See android.provider.CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME. + optional string phone_account_component_name = 7; + + // Whether the number can be reported as invalid through People API + optional bool can_report_as_invalid_number = 8; + + // Whether assisted dialing is supported. + optional bool can_support_assisted_dialing = 9; + + // Whether carrier video call is supported. + optional bool can_support_carrier_video_call = 10; + + // Whether the number is blocked. + optional bool is_blocked = 11; + + // Whether the number is spam. + optional bool is_spam = 12; + + // Whether the call is to the voicemail inbox. + optional bool is_voicemail_call = 13; + + // The source of the contact if there is one associated with the number. + optional com.android.dialer.logging.ContactSource.Type contact_source = 14; + + // Places that can host items (modules) in a bottom sheet + enum Host { + UNKNOWN = 0; + CALL_LOG = 1; + VOICEMAIL = 2; + } + optional Host host = 15; +} diff --git a/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto b/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto index ef71ecd7e..04d9f2259 100644 --- a/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto +++ b/java/com/android/dialer/historyitemactions/history_item_bottom_sheet_header_info.proto @@ -36,6 +36,4 @@ message HistoryItemBottomSheetHeaderInfo { // "Blocked • Mobile • 555-1234", and // "Spam • Mobile • 555-1234". optional string secondary_text = 4; - - // TODO(a bug): Add SIM info. } diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index db4c0245e..aa306d214 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -29,6 +29,7 @@ import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -46,7 +47,6 @@ import com.android.dialer.historyitemactions.HistoryItemActionBottomSheet; import com.android.dialer.historyitemactions.HistoryItemActionModule; import com.android.dialer.historyitemactions.HistoryItemBottomSheetHeaderInfo; import com.android.dialer.historyitemactions.IntentModule; -import com.android.dialer.historyitemactions.SharedModules; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.precall.PreCall; @@ -60,7 +60,6 @@ import com.android.dialer.speeddial.draghelper.SpeedDialLayoutManager; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.util.IntentUtil; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import java.util.ArrayList; @@ -312,11 +311,9 @@ public class SpeedDialFragment extends Fragment { } // Add sms module - Optional<HistoryItemActionModule> smsModule = - SharedModules.createModuleForSendingTextMessage( - getContext(), defaultChannel.number(), false); - if (smsModule.isPresent()) { - modules.add(smsModule.get()); + if (!TextUtils.isEmpty(defaultChannel.number())) { + modules.add( + IntentModule.newModuleForSendingTextMessage(getContext(), defaultChannel.number())); } modules.add(new DividerModule()); diff --git a/java/com/android/dialer/voicemail/listui/menu/Modules.java b/java/com/android/dialer/voicemail/listui/menu/Modules.java index 226063c1b..dcd9116e9 100644 --- a/java/com/android/dialer/voicemail/listui/menu/Modules.java +++ b/java/com/android/dialer/voicemail/listui/menu/Modules.java @@ -17,77 +17,53 @@ package com.android.dialer.voicemail.listui.menu; import android.content.Context; -import com.android.dialer.blockreportspam.BlockReportSpamDialogInfo; -import com.android.dialer.historyitemactions.DividerModule; +import android.text.TextUtils; import com.android.dialer.historyitemactions.HistoryItemActionModule; -import com.android.dialer.historyitemactions.SharedModules; -import com.android.dialer.logging.ReportingLocation; +import com.android.dialer.historyitemactions.HistoryItemActionModuleInfo; +import com.android.dialer.historyitemactions.HistoryItemActionModulesBuilder; import com.android.dialer.voicemail.model.VoicemailEntry; -import com.google.common.base.Optional; -import java.util.ArrayList; import java.util.List; /** * Configures the modules for the voicemail bottom sheet; these are the rows below the top row - * (primary action) in the bottom sheet. + * (contact info) in the bottom sheet. */ -@SuppressWarnings("Guava") final class Modules { static List<HistoryItemActionModule> fromVoicemailEntry( Context context, VoicemailEntry voicemailEntry) { - // Conditionally add each module, which are items in the bottom sheet's menu. - List<HistoryItemActionModule> modules = new ArrayList<>(); - - // TODO(uabdullah): Handle maybeAddModuleForVideoOrAudioCall(context, modules, row); - Optional<HistoryItemActionModule> moduleForAddingContacts = - SharedModules.createModuleForAddingToContacts( - context, - voicemailEntry.getNumber(), - voicemailEntry.getNumberAttributes().getName(), - voicemailEntry.getNumberAttributes().getLookupUri(), - voicemailEntry.getNumberAttributes().getIsBlocked(), - voicemailEntry.getNumberAttributes().getIsSpam()); - if (moduleForAddingContacts.isPresent()) { - modules.add(moduleForAddingContacts.get()); - } - - Optional<HistoryItemActionModule> moduleForSendingTextMessage = - SharedModules.createModuleForSendingTextMessage( - context, - voicemailEntry.getNumber().getNormalizedNumber(), - voicemailEntry.getNumberAttributes().getIsBlocked()); - if (moduleForSendingTextMessage.isPresent()) { - modules.add(moduleForSendingTextMessage.get()); - } - - if (!modules.isEmpty()) { - modules.add(new DividerModule()); - } - - BlockReportSpamDialogInfo blockReportSpamDialogInfo = - BlockReportSpamDialogInfo.newBuilder() - .setNormalizedNumber(voicemailEntry.getNumber().getNormalizedNumber()) - .setCountryIso(voicemailEntry.getNumber().getCountryIso()) - .setCallType(voicemailEntry.getCallType()) - .setReportingLocation(ReportingLocation.Type.VOICEMAIL_HISTORY) - .setContactSource(voicemailEntry.getNumberAttributes().getContactSource()) - .build(); - modules.addAll( - SharedModules.createModulesHandlingBlockedOrSpamNumber( - context, - blockReportSpamDialogInfo, - voicemailEntry.getNumberAttributes().getIsBlocked(), - voicemailEntry.getNumberAttributes().getIsSpam())); - - // TODO(zachh): Module for CallComposer. - Optional<HistoryItemActionModule> moduleForCopyingNumber = - SharedModules.createModuleForCopyingNumber( - context, voicemailEntry.getNumber().getNormalizedNumber()); - if (moduleForCopyingNumber.isPresent()) { - modules.add(moduleForCopyingNumber.get()); - } + return new HistoryItemActionModulesBuilder(context, buildModuleInfo(voicemailEntry)) + // TODO(uabdullah): add module for calls. + .addModuleForAddingToContacts() + .addModuleForSendingTextMessage() + .addModuleForDivider() + .addModuleForBlockedOrSpamNumber() + .addModuleForCopyingNumber() + // TODO(zachh): Module for CallComposer. + .build(); + } - return modules; + private static HistoryItemActionModuleInfo buildModuleInfo(VoicemailEntry voicemailEntry) { + return HistoryItemActionModuleInfo.newBuilder() + .setNormalizedNumber(voicemailEntry.getNumber().getNormalizedNumber()) + .setCountryIso(voicemailEntry.getNumber().getCountryIso()) + .setName(voicemailEntry.getNumberAttributes().getName()) + .setCallType(voicemailEntry.getCallType()) + .setLookupUri(voicemailEntry.getNumberAttributes().getLookupUri()) + .setPhoneAccountComponentName(voicemailEntry.getPhoneAccountComponentName()) + .setCanReportAsInvalidNumber( + voicemailEntry.getNumberAttributes().getCanReportAsInvalidNumber()) + .setCanSupportAssistedDialing( + !TextUtils.isEmpty(voicemailEntry.getNumberAttributes().getLookupUri())) + .setCanSupportCarrierVideoCall( + voicemailEntry.getNumberAttributes().getCanSupportCarrierVideoCall()) + .setIsBlocked(voicemailEntry.getNumberAttributes().getIsBlocked()) + .setIsSpam(voicemailEntry.getNumberAttributes().getIsSpam()) + // A voicemail call is an outgoing call to the voicemail box. + // Voicemail entries are not voicemail calls. + .setIsVoicemailCall(false) + .setContactSource(voicemailEntry.getNumberAttributes().getContactSource()) + .setHost(HistoryItemActionModuleInfo.Host.VOICEMAIL) + .build(); } } diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java index 5ac6b5029..98f001925 100644 --- a/java/com/android/incallui/InCallActivity.java +++ b/java/com/android/incallui/InCallActivity.java @@ -1504,7 +1504,8 @@ public class InCallActivity extends TransactionSafeFragmentActivity call.getVideoTech().isSelfManagedCamera(), shouldAllowAnswerAndRelease(call), CallList.getInstance().getBackgroundCall() != null, - call.isSpeakEasyEligible()); + getSpeakEasyCallManager().isAvailable(getApplicationContext()) + && call.isSpeakEasyEligible()); transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), Tags.ANSWER_SCREEN); Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this); diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java index d803956e6..b9d0eccba 100644 --- a/java/com/android/incallui/InCallServiceImpl.java +++ b/java/com/android/incallui/InCallServiceImpl.java @@ -26,7 +26,6 @@ import android.telecom.InCallService; import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; import com.android.dialer.feedback.FeedbackComponent; import com.android.incallui.audiomode.AudioModeProvider; -import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; import com.android.incallui.call.CallList; import com.android.incallui.call.ExternalCallList; import com.android.incallui.call.TelecomAdapter; @@ -98,7 +97,6 @@ public class InCallServiceImpl extends InCallService { final Context context = getApplicationContext(); final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context); AudioModeProvider.getInstance().initializeAudioState(this); - BluetoothDeviceProviderComponent.get(context).bluetoothDeviceProvider().setUp(); InCallPresenter.getInstance() .setUp( context, @@ -142,7 +140,6 @@ public class InCallServiceImpl extends InCallService { // Tear down the InCall system InCallPresenter.getInstance().tearDown(); TelecomAdapter.getInstance().clearInCallService(); - BluetoothDeviceProviderComponent.get(this).bluetoothDeviceProvider().tearDown(); if (returnToCallController != null) { returnToCallController.tearDown(); returnToCallController = null; diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java deleted file mode 100644 index 1aa1c20a8..000000000 --- a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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.incallui.audiomode; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothProfile; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.util.ArraySet; -import com.android.dialer.common.LogUtil; -import com.android.dialer.inject.ApplicationContext; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** Proxy class for getting and setting connected/active Bluetooth devices. */ -@Singleton -public final class BluetoothDeviceProvider extends BroadcastReceiver { - - // TODO(yueg): use BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED when possible - private static final String ACTION_ACTIVE_DEVICE_CHANGED = - "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED"; - - private final Context appContext; - private final BluetoothProfileServiceListener bluetoothProfileServiceListener = - new BluetoothProfileServiceListener(); - - private final Set<BluetoothDevice> connectedBluetoothDeviceSet = new ArraySet<>(); - - private BluetoothDevice activeBluetoothDevice; - private BluetoothHeadset bluetoothHeadset; - private boolean isSetUp; - - @Inject - public BluetoothDeviceProvider(@ApplicationContext Context appContext) { - this.appContext = appContext; - } - - public void setUp() { - if (BluetoothAdapter.getDefaultAdapter() == null) { - // Bluetooth is not supported on this hardware platform - return; - } - // Get Bluetooth service including the initial connected device list (should only contain one - // device) - BluetoothAdapter.getDefaultAdapter() - .getProfileProxy(appContext, bluetoothProfileServiceListener, BluetoothProfile.HEADSET); - // Get notified of Bluetooth device update - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(ACTION_ACTIVE_DEVICE_CHANGED); - appContext.registerReceiver(this, filter); - - isSetUp = true; - } - - public void tearDown() { - if (!isSetUp) { - return; - } - appContext.unregisterReceiver(this); - if (bluetoothHeadset != null) { - BluetoothAdapter.getDefaultAdapter() - .closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); - } - } - - public Set<BluetoothDevice> getConnectedBluetoothDeviceSet() { - return connectedBluetoothDeviceSet; - } - - public BluetoothDevice getActiveBluetoothDevice() { - return activeBluetoothDevice; - } - - @SuppressLint("PrivateApi") - public void setActiveBluetoothDevice(BluetoothDevice bluetoothDevice) { - if (!connectedBluetoothDeviceSet.contains(bluetoothDevice)) { - LogUtil.e("BluetoothProfileServiceListener.setActiveBluetoothDevice", "device is not in set"); - return; - } - // TODO(yueg): use BluetoothHeadset.setActiveDevice() when possible - try { - Method getActiveDeviceMethod = - bluetoothHeadset.getClass().getDeclaredMethod("setActiveDevice", BluetoothDevice.class); - getActiveDeviceMethod.setAccessible(true); - getActiveDeviceMethod.invoke(bluetoothHeadset, bluetoothDevice); - } catch (Exception e) { - LogUtil.e( - "BluetoothProfileServiceListener.setActiveBluetoothDevice", - "failed to call setActiveDevice", - e); - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { - handleActionConnectionStateChanged(intent); - } else if (ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) { - handleActionActiveDeviceChanged(intent); - } - } - - private void handleActionConnectionStateChanged(Intent intent) { - if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) { - LogUtil.i( - "BluetoothDeviceProvider.handleActionConnectionStateChanged", - "extra BluetoothDevice.EXTRA_DEVICE not found"); - return; - } - BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (bluetoothDevice == null) { - return; - } - - int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - if (state == BluetoothProfile.STATE_DISCONNECTED) { - connectedBluetoothDeviceSet.remove(bluetoothDevice); - LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device removed"); - } else if (state == BluetoothProfile.STATE_CONNECTED) { - connectedBluetoothDeviceSet.add(bluetoothDevice); - LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device added"); - } - } - - private void handleActionActiveDeviceChanged(Intent intent) { - if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) { - LogUtil.i( - "BluetoothDeviceProvider.handleActionActiveDeviceChanged", - "extra BluetoothDevice.EXTRA_DEVICE not found"); - return; - } - activeBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - LogUtil.i( - "BluetoothDeviceProvider.handleActionActiveDeviceChanged", - (activeBluetoothDevice == null ? "null" : "")); - } - - private final class BluetoothProfileServiceListener implements BluetoothProfile.ServiceListener { - @Override - @SuppressLint("PrivateApi") - public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) { - if (profile != BluetoothProfile.HEADSET) { - return; - } - // Get initial connected device list - bluetoothHeadset = (BluetoothHeadset) bluetoothProfile; - List<BluetoothDevice> devices = bluetoothProfile.getConnectedDevices(); - for (BluetoothDevice device : devices) { - connectedBluetoothDeviceSet.add(device); - LogUtil.i( - "BluetoothProfileServiceListener.onServiceConnected", "get initial connected device"); - } - - // Get initial active device - // TODO(yueg): use BluetoothHeadset.getActiveDevice() when possible - try { - Method getActiveDeviceMethod = - bluetoothHeadset.getClass().getDeclaredMethod("getActiveDevice"); - getActiveDeviceMethod.setAccessible(true); - activeBluetoothDevice = (BluetoothDevice) getActiveDeviceMethod.invoke(bluetoothHeadset); - LogUtil.i( - "BluetoothProfileServiceListener.onServiceConnected", - "get initial active device" + ((activeBluetoothDevice == null) ? " null" : "")); - } catch (Exception e) { - LogUtil.e( - "BluetoothProfileServiceListener.onServiceConnected", - "failed to call getAcitveDevice", - e); - } - } - - @Override - public void onServiceDisconnected(int profile) { - LogUtil.enterBlock("BluetoothProfileServiceListener.onServiceDisconnected"); - if (profile == BluetoothProfile.HEADSET) { - bluetoothHeadset = null; - } - } - } -} diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java deleted file mode 100644 index 9cd926835..000000000 --- a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.audiomode; - -import android.content.Context; -import com.android.dialer.inject.HasRootComponent; -import dagger.Subcomponent; - -/** Dagger component for the Bluetooth device provider. */ -@Subcomponent -public abstract class BluetoothDeviceProviderComponent { - - public abstract BluetoothDeviceProvider bluetoothDeviceProvider(); - - public static BluetoothDeviceProviderComponent get(Context context) { - return ((BluetoothDeviceProviderComponent.HasComponent) - ((HasRootComponent) context.getApplicationContext()).component()) - .bluetoothDeviceProviderComponent(); - } - - /** Used to refer to the root application component. */ - public interface HasComponent { - BluetoothDeviceProviderComponent bluetoothDeviceProviderComponent(); - } -} diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java index d6946d8de..a561b5ee5 100644 --- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java +++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java @@ -39,12 +39,12 @@ import com.android.dialer.common.FragmentUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; -import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; import com.android.incallui.call.CallList; import com.android.incallui.call.DialerCall; +import com.android.incallui.call.TelecomAdapter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Set; +import java.util.Collection; /** Shows picker for audio routes */ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment { @@ -91,24 +91,33 @@ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment @Nullable @Override + @SuppressLint("NewApi") public View onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false); CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE); - Set<BluetoothDevice> bluetoothDeviceSet = - BluetoothDeviceProviderComponent.get(getContext()) - .bluetoothDeviceProvider() - .getConnectedBluetoothDeviceSet(); - for (BluetoothDevice device : bluetoothDeviceSet) { - boolean selected = - (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) - && (bluetoothDeviceSet.size() == 1 - || device.equals( - BluetoothDeviceProviderComponent.get(getContext()) - .bluetoothDeviceProvider() - .getActiveBluetoothDevice())); - TextView textView = createBluetoothItem(device, selected); + if (BuildCompat.isAtLeastP()) { + // Create items for all connected Bluetooth devices + Collection<BluetoothDevice> bluetoothDeviceSet = audioState.getSupportedBluetoothDevices(); + for (BluetoothDevice device : bluetoothDeviceSet) { + boolean selected = + (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) + && (bluetoothDeviceSet.size() == 1 + || device.equals(audioState.getActiveBluetoothDevice())); + TextView textView = createBluetoothItem(device, selected); + ((LinearLayout) view).addView(textView, 0); + } + } else { + // Only create Bluetooth audio route + TextView textView = + (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false); + textView.setText(getString(R.string.audioroute_bluetooth)); + initItem( + textView, + CallAudioState.ROUTE_BLUETOOTH, + audioState, + DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_BLUETOOTH); ((LinearLayout) view).addView(textView, 0); } @@ -183,9 +192,7 @@ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class) .onAudioRouteSelected(CallAudioState.ROUTE_BLUETOOTH); // Set active Bluetooth device - BluetoothDeviceProviderComponent.get(getContext()) - .bluetoothDeviceProvider() - .setActiveBluetoothDevice(bluetoothDevice); + TelecomAdapter.getInstance().requestBluetoothAudio(bluetoothDevice); dismiss(); }); diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java index 1a0de1960..77e2ea30b 100644 --- a/java/com/android/incallui/call/DialerCall.java +++ b/java/com/android/incallui/call/DialerCall.java @@ -50,7 +50,6 @@ import android.telecom.VideoProfile; import android.text.TextUtils; import android.widget.Toast; import com.android.contacts.common.compat.CallCompat; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; import com.android.dialer.assisteddialing.ConcreteCreator; import com.android.dialer.assisteddialing.TransformationInfo; import com.android.dialer.callintent.CallInitiationType; @@ -86,7 +85,6 @@ import com.android.incallui.audiomode.AudioModeProvider; import com.android.incallui.call.state.DialerCallState; import com.android.incallui.latencyreport.LatencyReport; import com.android.incallui.rtt.protocol.RttChatMessage; -import com.android.incallui.speakeasy.runtime.Constraints; import com.android.incallui.videotech.VideoTech; import com.android.incallui.videotech.VideoTech.VideoTechListener; import com.android.incallui.videotech.duo.DuoVideoTech; @@ -118,8 +116,11 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa public static final int PROPERTY_CODEC_KNOWN = 0x04000000; private static final String ID_PREFIX = "DialerCall_"; - private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS = + + @VisibleForTesting + public static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS = "emergency_callback_window_millis"; + private static int idCounter = 0; /** @@ -822,10 +823,9 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa // We want to treat any incoming call that arrives a short time after an outgoing emergency call // as a potential emergency callback. if (getExtras() != null - && getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0) - > 0) { + && getExtras().getLong(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0) > 0) { long lastEmergencyCallMillis = - getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0); + getExtras().getLong(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0); if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) { return true; } @@ -1058,6 +1058,7 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa } @TargetApi(28) + @Nullable public RttCall getRttCall() { if (!isActiveRttCall()) { return null; @@ -1111,16 +1112,18 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa if (!BuildCompat.isAtLeastP()) { return; } - // Save any remaining text in the buffer that's not shown by UI yet. - // This may happen when the call is switched to background before disconnect. - try { - String messageLeft = getRttCall().readImmediately(); - if (!TextUtils.isEmpty(messageLeft)) { - rttTranscript = - RttChatMessage.getRttTranscriptWithNewRemoteMessage(rttTranscript, messageLeft); + if (getRttCall() != null) { + // Save any remaining text in the buffer that's not shown by UI yet. + // This may happen when the call is switched to background before disconnect. + try { + String messageLeft = getRttCall().readImmediately(); + if (!TextUtils.isEmpty(messageLeft)) { + rttTranscript = + RttChatMessage.getRttTranscriptWithNewRemoteMessage(rttTranscript, messageLeft); + } + } catch (IOException e) { + LogUtil.e("DialerCall.saveRttTranscript", "error when reading remaining message", e); } - } catch (IOException e) { - LogUtil.e("DialerCall.saveRttTranscript", "error when reading remaining message", e); } // Don't save transcript if it's empty. if (rttTranscript.getMessagesCount() == 0) { @@ -1662,7 +1665,6 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa if (videoTechManager != null) { videoTechManager.dispatchRemovedFromCallList(); } - // TODO(a bug): Add tests for it to make sure no crash on subsequent call to this method. // TODO(wangqi): Consider moving this to a DialerCallListener. if (rttTranscript != null && !isCallRemoved) { saveRttTranscript(); @@ -1697,10 +1699,6 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa /** Indicates the call is eligible for SpeakEasy */ public boolean isSpeakEasyEligible() { - if (!Constraints.isAvailable(context)) { - return false; - } - return !isPotentialEmergencyCallback() && !isEmergencyCall() && !isActiveRttCall() diff --git a/java/com/android/incallui/call/TelecomAdapter.java b/java/com/android/incallui/call/TelecomAdapter.java index a7e10d37c..4ae1bc1db 100644 --- a/java/com/android/incallui/call/TelecomAdapter.java +++ b/java/com/android/incallui/call/TelecomAdapter.java @@ -16,7 +16,9 @@ package com.android.incallui.call; +import android.annotation.TargetApi; import android.app.Notification; +import android.bluetooth.BluetoothDevice; import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Looper; @@ -193,4 +195,13 @@ public class TelecomAdapter implements InCallServiceListener { "no inCallService available for stopping foreground notification"); } } + + @TargetApi(28) + public void requestBluetoothAudio(BluetoothDevice bluetoothDevice) { + if (inCallService != null) { + inCallService.requestBluetoothAudio(bluetoothDevice); + } else { + LogUtil.e("TelecomAdapter.requestBluetoothAudio", "inCallService is null"); + } + } } diff --git a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java index 01c3950e9..1c83637ea 100644 --- a/java/com/android/incallui/rtt/impl/AudioSelectMenu.java +++ b/java/com/android/incallui/rtt/impl/AudioSelectMenu.java @@ -17,8 +17,6 @@ package com.android.incallui.rtt.impl; import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.PorterDuff.Mode; import android.telecom.CallAudioState; import android.view.View; import android.widget.PopupWindow; @@ -28,8 +26,11 @@ import com.android.incallui.incall.protocol.InCallButtonUiDelegate; public class AudioSelectMenu extends PopupWindow { private final InCallButtonUiDelegate inCallButtonUiDelegate; - private final Context context; private final OnButtonClickListener onButtonClickListener; + private final RttCheckableButton bluetoothButton; + private final RttCheckableButton speakerButton; + private final RttCheckableButton headsetButton; + private final RttCheckableButton earpieceButton; interface OnButtonClickListener { void onBackPressed(); @@ -40,7 +41,6 @@ public class AudioSelectMenu extends PopupWindow { InCallButtonUiDelegate inCallButtonUiDelegate, OnButtonClickListener onButtonClickListener) { super(context, null, 0, R.style.OverflowMenu); - this.context = context; this.inCallButtonUiDelegate = inCallButtonUiDelegate; this.onButtonClickListener = onButtonClickListener; View view = View.inflate(context, R.layout.audio_route, null); @@ -55,28 +55,32 @@ public class AudioSelectMenu extends PopupWindow { this.onButtonClickListener.onBackPressed(); }); CallAudioState audioState = inCallButtonUiDelegate.getCurrentAudioState(); - initItem( - view.findViewById(R.id.audioroute_bluetooth), CallAudioState.ROUTE_BLUETOOTH, audioState); - initItem(view.findViewById(R.id.audioroute_speaker), CallAudioState.ROUTE_SPEAKER, audioState); - initItem( - view.findViewById(R.id.audioroute_headset), CallAudioState.ROUTE_WIRED_HEADSET, audioState); - initItem( - view.findViewById(R.id.audioroute_earpiece), CallAudioState.ROUTE_EARPIECE, audioState); + bluetoothButton = view.findViewById(R.id.audioroute_bluetooth); + speakerButton = view.findViewById(R.id.audioroute_speaker); + headsetButton = view.findViewById(R.id.audioroute_headset); + earpieceButton = view.findViewById(R.id.audioroute_earpiece); + initItem(bluetoothButton, CallAudioState.ROUTE_BLUETOOTH, audioState); + initItem(speakerButton, CallAudioState.ROUTE_SPEAKER, audioState); + initItem(headsetButton, CallAudioState.ROUTE_WIRED_HEADSET, audioState); + initItem(earpieceButton, CallAudioState.ROUTE_EARPIECE, audioState); } private void initItem(RttCheckableButton item, final int itemRoute, CallAudioState audioState) { - int selectedColor = - context.getColor(com.android.incallui.audioroute.R.color.dialer_theme_color); if ((audioState.getSupportedRouteMask() & itemRoute) == 0) { item.setVisibility(View.GONE); } else if (audioState.getRoute() == itemRoute) { - item.setTextColor(selectedColor); - item.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor)); - item.setCompoundDrawableTintMode(Mode.SRC_ATOP); + item.setChecked(true); } item.setOnClickListener( (v) -> { inCallButtonUiDelegate.setAudioRoute(itemRoute); }); } + + void setAudioState(CallAudioState audioState) { + bluetoothButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH); + speakerButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_SPEAKER); + headsetButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET); + earpieceButton.setChecked(audioState.getRoute() == CallAudioState.ROUTE_EARPIECE); + } } diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java index e56715981..c393393f8 100644 --- a/java/com/android/incallui/rtt/impl/RttChatFragment.java +++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java @@ -107,6 +107,7 @@ public class RttChatFragment extends Fragment private PrimaryCallState primaryCallState = PrimaryCallState.empty(); private boolean isUserScrolling; private boolean shouldAutoScrolling; + private AudioSelectMenu audioSelectMenu; /** * Create a new instance of RttChatFragment. @@ -558,6 +559,9 @@ public class RttChatFragment extends Fragment LogUtil.i("RttChatFragment.setAudioState", "audioState: " + audioState); overflowMenu.setMuteButtonChecked(audioState.isMuted()); overflowMenu.setAudioState(audioState); + if (audioSelectMenu != null) { + audioSelectMenu.setAudioState(audioState); + } } @Override @@ -573,7 +577,7 @@ public class RttChatFragment extends Fragment @Override public void showAudioRouteSelector() { - AudioSelectMenu audioSelectMenu = + audioSelectMenu = new AudioSelectMenu( getContext(), inCallButtonUiDelegate, diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java index f2721da7d..8a815d385 100644 --- a/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java +++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManager.java @@ -16,6 +16,7 @@ package com.android.incallui.speakeasy; +import android.content.Context; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import com.android.incallui.call.DialerCall; @@ -37,4 +38,15 @@ public interface SpeakEasyCallManager { * @param call The call which has been removed. */ void onCallRemoved(@NonNull DialerCall call); + + /** + * Indicates the feature is available. + * + * @param context The application context. + */ + boolean isAvailable(@NonNull Context context); + + /** Returns the config provider flag associated with the feature. */ + @NonNull + String getConfigProviderFlag(); } diff --git a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java index 9e58ce18f..a0409737b 100644 --- a/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java +++ b/java/com/android/incallui/speakeasy/SpeakEasyCallManagerStub.java @@ -16,6 +16,8 @@ package com.android.incallui.speakeasy; +import android.content.Context; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import com.android.incallui.call.DialerCall; @@ -38,4 +40,17 @@ public class SpeakEasyCallManagerStub implements SpeakEasyCallManager { /** Always inert in the stub. */ @Override public void onCallRemoved(DialerCall call) {} + + /** Always returns false. */ + @Override + public boolean isAvailable(@NonNull Context unused) { + return false; + } + + /** Always returns a stub string. */ + @NonNull + @Override + public String getConfigProviderFlag() { + return "not_yet_implmented"; + } } diff --git a/java/com/android/incallui/speakeasy/runtime/Constraints.java b/java/com/android/incallui/speakeasy/runtime/Constraints.java deleted file mode 100644 index 1206d599c..000000000 --- a/java/com/android/incallui/speakeasy/runtime/Constraints.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.incallui.speakeasy.runtime; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build.VERSION_CODES; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.support.v4.os.BuildCompat; -import android.support.v4.os.UserManagerCompat; -import com.android.dialer.common.Assert; -import com.android.dialer.common.LogUtil; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.util.PermissionsUtil; - -/** Preconditions for the use of SpeakEasyModule */ -public final class Constraints { - - @VisibleForTesting public static final String SPEAK_EASY_ENABLED = "speak_easy_enabled"; - private static final String[] REQUIRED_PERMISSIONS = { - - }; - - // Non-instantiatable. - private Constraints() {} - - public static boolean isAvailable(@NonNull Context context) { - Assert.isNotNull(context); - - return isServerConfigEnabled(context) - && isUserUnlocked(context) - && meetsPlatformSdkFloor() - && hasNecessaryPermissions(context); - } - - private static boolean isServerConfigEnabled(@NonNull Context context) { - return ConfigProviderBindings.get(context).getBoolean(SPEAK_EASY_ENABLED, false); - } - - private static boolean isUserUnlocked(@NonNull Context context) { - return UserManagerCompat.isUserUnlocked(context); - } - - private static boolean meetsPlatformSdkFloor() { - return BuildCompat.isAtLeastP(); - } - - @SuppressWarnings("AndroidApiChecker") // Use of Java 8 APIs. - @TargetApi(VERSION_CODES.N) - private static boolean hasNecessaryPermissions(@NonNull Context context) { - for (String permission : REQUIRED_PERMISSIONS) { - if (!PermissionsUtil.hasPermission(context, permission)) { - LogUtil.i("Constraints.hasNecessaryPermissions", "missing permission: %s ", permission); - return false; - } - } - return true; - } -} |