1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.incallui; 18 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.graphics.drawable.Drawable; 23 import android.graphics.drawable.Icon; 24 import android.provider.Settings; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.VisibleForTesting; 27 import android.telecom.CallAudioState; 28 import android.text.TextUtils; 29 import com.android.bubble.Bubble; 30 import com.android.bubble.BubbleComponent; 31 import com.android.bubble.BubbleInfo; 32 import com.android.bubble.BubbleInfo.Action; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.configprovider.ConfigProviderComponent; 35 import com.android.dialer.contacts.ContactsComponent; 36 import com.android.dialer.lettertile.LetterTileDrawable; 37 import com.android.dialer.telecom.TelecomUtil; 38 import com.android.dialer.theme.base.ThemeComponent; 39 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 40 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 41 import com.android.incallui.InCallPresenter.InCallState; 42 import com.android.incallui.InCallPresenter.InCallUiListener; 43 import com.android.incallui.audiomode.AudioModeProvider; 44 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener; 45 import com.android.incallui.call.CallList; 46 import com.android.incallui.call.CallList.Listener; 47 import com.android.incallui.call.DialerCall; 48 import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo; 49 import java.lang.ref.WeakReference; 50 import java.util.ArrayList; 51 import java.util.List; 52 53 /** 54 * Listens for events relevant to the return-to-call bubble and updates the bubble's state as 55 * necessary. 56 * 57 * <p>Bubble shows when one of following happens: 1. a new outgoing/ongoing call appears 2. leave 58 * in-call UI with an outgoing/ongoing call 59 * 60 * <p>Bubble hides when one of following happens: 1. a call disconnect and there is no more 61 * outgoing/ongoing call 2. show in-call UI 62 */ 63 public class ReturnToCallController implements InCallUiListener, Listener, AudioModeListener { 64 65 private final Context context; 66 67 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 68 Bubble bubble; 69 70 private static Boolean canShowBubblesForTesting = null; 71 72 private CallAudioState audioState; 73 74 private final PendingIntent toggleSpeaker; 75 private final PendingIntent showSpeakerSelect; 76 private final PendingIntent toggleMute; 77 private final PendingIntent endCall; 78 private final PendingIntent fullScreen; 79 80 private final ContactInfoCache contactInfoCache; 81 82 private InCallState inCallState; 83 isEnabled(Context context)84 public static boolean isEnabled(Context context) { 85 return ConfigProviderComponent.get(context) 86 .getConfigProvider() 87 .getBoolean("enable_return_to_call_bubble_v2", false); 88 } 89 ReturnToCallController(Context context, ContactInfoCache contactInfoCache)90 public ReturnToCallController(Context context, ContactInfoCache contactInfoCache) { 91 this.context = context; 92 this.contactInfoCache = contactInfoCache; 93 94 toggleSpeaker = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_SPEAKER); 95 showSpeakerSelect = 96 createActionIntent(ReturnToCallActionReceiver.ACTION_SHOW_AUDIO_ROUTE_SELECTOR); 97 toggleMute = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_MUTE); 98 endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL); 99 fullScreen = createActionIntent(ReturnToCallActionReceiver.ACTION_RETURN_TO_CALL); 100 101 AudioModeProvider.getInstance().addListener(this); 102 audioState = AudioModeProvider.getInstance().getAudioState(); 103 InCallPresenter.getInstance().addInCallUiListener(this); 104 CallList.getInstance().addListener(this); 105 } 106 tearDown()107 public void tearDown() { 108 hide(); 109 InCallPresenter.getInstance().removeInCallUiListener(this); 110 CallList.getInstance().removeListener(this); 111 AudioModeProvider.getInstance().removeListener(this); 112 } 113 114 @Override onUiShowing(boolean showing)115 public void onUiShowing(boolean showing) { 116 if (!isEnabled(context)) { 117 hide(); 118 return; 119 } 120 121 LogUtil.i("ReturnToCallController.onUiShowing", "showing: " + showing); 122 if (showing) { 123 LogUtil.i("ReturnToCallController.onUiShowing", "going to hide"); 124 hide(); 125 } else { 126 if (getCall() != null) { 127 LogUtil.i("ReturnToCallController.onUiShowing", "going to show"); 128 show(); 129 } 130 } 131 } 132 hide()133 private void hide() { 134 if (bubble != null) { 135 bubble.hide(); 136 } else { 137 LogUtil.i("ReturnToCallController.hide", "hide() called without calling show()"); 138 } 139 } 140 show()141 private void show() { 142 if (bubble == null) { 143 bubble = startBubble(); 144 } else { 145 bubble.show(); 146 } 147 startContactInfoSearch(); 148 } 149 150 /** 151 * Determines whether bubbles can be shown based on permissions obtained. This should be checked 152 * before attempting to create a Bubble. 153 * 154 * @return true iff bubbles are able to be shown. 155 * @see Settings#canDrawOverlays(Context) 156 */ canShowBubbles(@onNull Context context)157 private static boolean canShowBubbles(@NonNull Context context) { 158 return canShowBubblesForTesting != null 159 ? canShowBubblesForTesting 160 : Settings.canDrawOverlays(context); 161 } 162 163 @VisibleForTesting(otherwise = VisibleForTesting.NONE) setCanShowBubblesForTesting(boolean canShowBubbles)164 static void setCanShowBubblesForTesting(boolean canShowBubbles) { 165 canShowBubblesForTesting = canShowBubbles; 166 } 167 startBubble()168 private Bubble startBubble() { 169 if (!canShowBubbles(context)) { 170 LogUtil.i("ReturnToCallController.startBubble", "can't show bubble, no permission"); 171 return null; 172 } 173 Bubble returnToCallBubble = BubbleComponent.get(context).getBubble(); 174 returnToCallBubble.setBubbleInfo(generateBubbleInfo()); 175 returnToCallBubble.show(); 176 return returnToCallBubble; 177 } 178 179 @Override onIncomingCall(DialerCall call)180 public void onIncomingCall(DialerCall call) {} 181 182 @Override onUpgradeToVideo(DialerCall call)183 public void onUpgradeToVideo(DialerCall call) {} 184 185 @Override onSessionModificationStateChange(DialerCall call)186 public void onSessionModificationStateChange(DialerCall call) {} 187 188 @Override onCallListChange(CallList callList)189 public void onCallListChange(CallList callList) { 190 if (!isEnabled(context)) { 191 hide(); 192 return; 193 } 194 195 boolean shouldStartInBubbleMode = InCallPresenter.getInstance().shouldStartInBubbleMode(); 196 InCallState newInCallState = 197 InCallPresenter.getInstance().getPotentialStateFromCallList(callList); 198 boolean isNewBackgroundCall = 199 newInCallState != inCallState 200 && newInCallState == InCallState.OUTGOING 201 && shouldStartInBubbleMode; 202 boolean bubbleNeverVisible = (bubble == null || !(bubble.isVisible() || bubble.isDismissed())); 203 if (bubble != null && isNewBackgroundCall) { 204 // If new outgoing call is in bubble mode, update bubble info. 205 // We don't update if new call is not in bubble mode even if the existing call is. 206 bubble.setBubbleInfo(generateBubbleInfoForBackgroundCalling()); 207 } 208 if (((bubbleNeverVisible && newInCallState != InCallState.OUTGOING) || isNewBackgroundCall) 209 && getCall() != null 210 && !InCallPresenter.getInstance().isShowingInCallUi()) { 211 LogUtil.i("ReturnToCallController.onCallListChange", "going to show bubble"); 212 show(); 213 } else { 214 // The call to display might be different for the existing bubble 215 startContactInfoSearch(); 216 } 217 inCallState = newInCallState; 218 } 219 220 @Override onDisconnect(DialerCall call)221 public void onDisconnect(DialerCall call) { 222 if (!isEnabled(context)) { 223 hide(); 224 return; 225 } 226 227 LogUtil.enterBlock("ReturnToCallController.onDisconnect"); 228 if (bubble != null && bubble.isVisible() && (getCall() == null)) { 229 // Show "Call ended" and hide bubble when there is no outgoing, active or background call 230 LogUtil.i("ReturnToCallController.onDisconnect", "show call ended and hide bubble"); 231 // Don't show text if it's Duo upgrade 232 // It doesn't work for Duo fallback upgrade since we're not considered in call 233 if (!TelecomUtil.isInCall(context) || CallList.getInstance().getIncomingCall() != null) { 234 bubble.showText(context.getText(R.string.incall_call_ended)); 235 } 236 hide(); 237 } else { 238 startContactInfoSearch(); 239 } 240 } 241 242 @Override onWiFiToLteHandover(DialerCall call)243 public void onWiFiToLteHandover(DialerCall call) {} 244 245 @Override onHandoverToWifiFailed(DialerCall call)246 public void onHandoverToWifiFailed(DialerCall call) {} 247 248 @Override onInternationalCallOnWifi(@onNull DialerCall call)249 public void onInternationalCallOnWifi(@NonNull DialerCall call) {} 250 251 @Override onAudioStateChanged(CallAudioState audioState)252 public void onAudioStateChanged(CallAudioState audioState) { 253 if (!isEnabled(context)) { 254 hide(); 255 return; 256 } 257 258 this.audioState = audioState; 259 if (bubble != null) { 260 bubble.updateActions(generateActions()); 261 } 262 } 263 startContactInfoSearch()264 private void startContactInfoSearch() { 265 DialerCall dialerCall = getCall(); 266 if (dialerCall != null) { 267 contactInfoCache.findInfo( 268 dialerCall, false /* isIncoming */, new ReturnToCallContactInfoCacheCallback(this)); 269 } 270 } 271 getCall()272 private DialerCall getCall() { 273 DialerCall dialerCall = CallList.getInstance().getOutgoingCall(); 274 if (dialerCall == null) { 275 dialerCall = CallList.getInstance().getActiveOrBackgroundCall(); 276 } 277 return dialerCall; 278 } 279 onPhotoAvatarReceived(@onNull Drawable photo)280 private void onPhotoAvatarReceived(@NonNull Drawable photo) { 281 if (bubble != null) { 282 bubble.updatePhotoAvatar(photo); 283 } 284 } 285 onLetterTileAvatarReceived(@onNull Drawable photo)286 private void onLetterTileAvatarReceived(@NonNull Drawable photo) { 287 if (bubble != null) { 288 bubble.updateAvatar(photo); 289 } 290 } 291 generateBubbleInfo()292 private BubbleInfo generateBubbleInfo() { 293 return BubbleInfo.builder() 294 .setPrimaryColor(ThemeComponent.get(context).theme().getColorPrimary()) 295 .setPrimaryIcon(Icon.createWithResource(context, R.drawable.on_going_call)) 296 .setStartingYPosition( 297 InCallPresenter.getInstance().shouldStartInBubbleMode() 298 ? context.getResources().getDisplayMetrics().heightPixels / 2 299 : context 300 .getResources() 301 .getDimensionPixelOffset(R.dimen.return_to_call_initial_offset_y)) 302 .setActions(generateActions()) 303 .build(); 304 } 305 generateBubbleInfoForBackgroundCalling()306 private BubbleInfo generateBubbleInfoForBackgroundCalling() { 307 return BubbleInfo.builder() 308 .setPrimaryColor(ThemeComponent.get(context).theme().getColorPrimary()) 309 .setPrimaryIcon(Icon.createWithResource(context, R.drawable.on_going_call)) 310 .setStartingYPosition(context.getResources().getDisplayMetrics().heightPixels / 2) 311 .setActions(generateActions()) 312 .build(); 313 } 314 315 @NonNull generateActions()316 private List<Action> generateActions() { 317 List<Action> actions = new ArrayList<>(); 318 SpeakerButtonInfo speakerButtonInfo = new SpeakerButtonInfo(audioState); 319 320 // Return to call 321 actions.add( 322 Action.builder() 323 .setIconDrawable( 324 context.getDrawable(R.drawable.quantum_ic_exit_to_app_flip_vd_theme_24)) 325 .setIntent(fullScreen) 326 .setName(context.getText(R.string.bubble_return_to_call)) 327 .setCheckable(false) 328 .build()); 329 // Mute/unmute 330 actions.add( 331 Action.builder() 332 .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_mic_off_vd_theme_24)) 333 .setChecked(audioState.isMuted()) 334 .setIntent(toggleMute) 335 .setName(context.getText(R.string.incall_label_mute)) 336 .build()); 337 // Speaker/audio selector 338 actions.add( 339 Action.builder() 340 .setIconDrawable(context.getDrawable(speakerButtonInfo.icon)) 341 .setSecondaryIconDrawable( 342 speakerButtonInfo.nonBluetoothMode 343 ? null 344 : context.getDrawable(R.drawable.quantum_ic_arrow_drop_down_vd_theme_24)) 345 .setName(context.getText(speakerButtonInfo.label)) 346 .setCheckable(speakerButtonInfo.nonBluetoothMode) 347 .setChecked(speakerButtonInfo.isChecked) 348 .setIntent(speakerButtonInfo.nonBluetoothMode ? toggleSpeaker : showSpeakerSelect) 349 .build()); 350 // End call 351 actions.add( 352 Action.builder() 353 .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_call_end_vd_theme_24)) 354 .setIntent(endCall) 355 .setName(context.getText(R.string.incall_label_end_call)) 356 .setCheckable(false) 357 .build()); 358 return actions; 359 } 360 361 @NonNull createActionIntent(String action)362 private PendingIntent createActionIntent(String action) { 363 Intent intent = new Intent(context, ReturnToCallActionReceiver.class); 364 intent.setAction(action); 365 return PendingIntent.getBroadcast(context, 0, intent, 0); 366 } 367 368 @NonNull createLettleTileDrawable( DialerCall dialerCall, ContactCacheEntry entry)369 private LetterTileDrawable createLettleTileDrawable( 370 DialerCall dialerCall, ContactCacheEntry entry) { 371 String preferredName = 372 ContactsComponent.get(context) 373 .contactDisplayPreferences() 374 .getDisplayName(entry.namePrimary, entry.nameAlternative); 375 if (TextUtils.isEmpty(preferredName)) { 376 preferredName = entry.number; 377 } 378 379 LetterTileDrawable letterTile = new LetterTileDrawable(context.getResources()); 380 letterTile.setCanonicalDialerLetterTileDetails( 381 dialerCall.updateNameIfRestricted(preferredName), 382 entry.lookupKey, 383 LetterTileDrawable.SHAPE_CIRCLE, 384 LetterTileDrawable.getContactTypeFromPrimitives( 385 dialerCall.isVoiceMailNumber(), 386 dialerCall.isSpam(), 387 entry.isBusiness, 388 dialerCall.getNumberPresentation(), 389 dialerCall.isConferenceCall())); 390 return letterTile; 391 } 392 393 private static class ReturnToCallContactInfoCacheCallback implements ContactInfoCacheCallback { 394 395 private final WeakReference<ReturnToCallController> returnToCallControllerWeakReference; 396 ReturnToCallContactInfoCacheCallback(ReturnToCallController returnToCallController)397 private ReturnToCallContactInfoCacheCallback(ReturnToCallController returnToCallController) { 398 returnToCallControllerWeakReference = new WeakReference<>(returnToCallController); 399 } 400 401 @Override onContactInfoComplete(String callId, ContactCacheEntry entry)402 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 403 ReturnToCallController returnToCallController = returnToCallControllerWeakReference.get(); 404 if (returnToCallController == null) { 405 return; 406 } 407 if (entry.photo != null) { 408 returnToCallController.onPhotoAvatarReceived(entry.photo); 409 } else { 410 DialerCall dialerCall = CallList.getInstance().getCallById(callId); 411 if (dialerCall != null) { 412 returnToCallController.onLetterTileAvatarReceived( 413 returnToCallController.createLettleTileDrawable(dialerCall, entry)); 414 } 415 } 416 } 417 418 @Override onImageLoadComplete(String callId, ContactCacheEntry entry)419 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 420 ReturnToCallController returnToCallController = returnToCallControllerWeakReference.get(); 421 if (returnToCallController == null) { 422 return; 423 } 424 if (entry.photo != null) { 425 returnToCallController.onPhotoAvatarReceived(entry.photo); 426 } else { 427 DialerCall dialerCall = CallList.getInstance().getCallById(callId); 428 if (dialerCall != null) { 429 returnToCallController.onLetterTileAvatarReceived( 430 returnToCallController.createLettleTileDrawable(dialerCall, entry)); 431 } 432 } 433 } 434 } 435 } 436