1 /* 2 * Copyright (C) 2016 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.dialer.callcomposer; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorSet; 22 import android.animation.ArgbEvaluator; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.res.Configuration; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.support.annotation.NonNull; 32 import android.support.annotation.VisibleForTesting; 33 import android.support.v4.content.ContextCompat; 34 import android.support.v4.content.FileProvider; 35 import android.support.v4.util.Pair; 36 import android.support.v4.view.ViewPager.OnPageChangeListener; 37 import android.support.v4.view.animation.FastOutSlowInInterpolator; 38 import android.support.v7.app.AppCompatActivity; 39 import android.text.TextUtils; 40 import android.util.Base64; 41 import android.view.Gravity; 42 import android.view.View; 43 import android.view.View.OnClickListener; 44 import android.view.ViewAnimationUtils; 45 import android.view.ViewGroup; 46 import android.widget.FrameLayout; 47 import android.widget.ImageView; 48 import android.widget.LinearLayout; 49 import android.widget.ProgressBar; 50 import android.widget.QuickContactBadge; 51 import android.widget.RelativeLayout; 52 import android.widget.TextView; 53 import android.widget.Toast; 54 import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener; 55 import com.android.dialer.callintent.CallInitiationType; 56 import com.android.dialer.callintent.CallIntentBuilder; 57 import com.android.dialer.common.Assert; 58 import com.android.dialer.common.LogUtil; 59 import com.android.dialer.common.UiUtil; 60 import com.android.dialer.common.concurrent.DialerExecutor; 61 import com.android.dialer.common.concurrent.DialerExecutorComponent; 62 import com.android.dialer.common.concurrent.ThreadUtil; 63 import com.android.dialer.configprovider.ConfigProviderComponent; 64 import com.android.dialer.constants.Constants; 65 import com.android.dialer.contactphoto.ContactPhotoManager; 66 import com.android.dialer.dialercontact.DialerContact; 67 import com.android.dialer.enrichedcall.EnrichedCallComponent; 68 import com.android.dialer.enrichedcall.EnrichedCallManager; 69 import com.android.dialer.enrichedcall.Session; 70 import com.android.dialer.enrichedcall.Session.State; 71 import com.android.dialer.enrichedcall.extensions.StateExtension; 72 import com.android.dialer.logging.DialerImpression; 73 import com.android.dialer.logging.Logger; 74 import com.android.dialer.multimedia.MultimediaData; 75 import com.android.dialer.precall.PreCall; 76 import com.android.dialer.protos.ProtoParsers; 77 import com.android.dialer.storage.StorageComponent; 78 import com.android.dialer.telecom.TelecomUtil; 79 import com.android.dialer.util.UriUtils; 80 import com.android.dialer.util.ViewUtil; 81 import com.android.dialer.widget.BidiTextView; 82 import com.android.dialer.widget.DialerToolbar; 83 import com.android.dialer.widget.LockableViewPager; 84 import com.android.incallui.callpending.CallPendingActivity; 85 import com.google.protobuf.InvalidProtocolBufferException; 86 import java.io.File; 87 88 /** 89 * Implements an activity which prompts for a call with additional media for an outgoing call. The 90 * activity includes a pop up with: 91 * 92 * <ul> 93 * <li>Contact galleryIcon 94 * <li>Name 95 * <li>Number 96 * <li>Media options to attach a gallery image, camera image or a message 97 * </ul> 98 */ 99 public class CallComposerActivity extends AppCompatActivity 100 implements OnClickListener, 101 OnPageChangeListener, 102 CallComposerListener, 103 EnrichedCallManager.StateChangedListener { 104 105 public static final String KEY_CONTACT_NAME = "contact_name"; 106 private static final String KEY_IS_FIRST_CALL_COMPOSE = "is_first_call_compose"; 107 108 private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500; 109 private static final int EXIT_ANIMATION_DURATION_MILLIS = 500; 110 111 private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT"; 112 private static final String ARG_CALL_COMPOSER_CONTACT_BASE64 = "CALL_COMPOSER_CONTACT_BASE64"; 113 114 private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key"; 115 private static final String SEND_AND_CALL_READY_KEY = "send_and_call_ready_key"; 116 private static final String CURRENT_INDEX_KEY = "current_index_key"; 117 private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key"; 118 private static final String SESSION_ID_KEY = "session_id_key"; 119 120 private final Handler timeoutHandler = ThreadUtil.getUiThreadHandler(); 121 private final Runnable sessionStartedTimedOut = 122 () -> { 123 LogUtil.i("CallComposerActivity.sessionStartedTimedOutRunnable", "session never started"); 124 setFailedResultAndFinish(); 125 }; 126 private final Runnable placeTelecomCallRunnable = 127 () -> { 128 LogUtil.i("CallComposerActivity.placeTelecomCallRunnable", "upload timed out."); 129 placeTelecomCall(); 130 }; 131 // Counter for the number of message sent updates received from EnrichedCallManager 132 private int messageSentCounter; 133 private boolean pendingCallStarted; 134 135 private DialerContact contact; 136 private Long sessionId = Session.NO_SESSION_ID; 137 138 private TextView nameView; 139 private BidiTextView numberView; 140 private QuickContactBadge contactPhoto; 141 private RelativeLayout contactContainer; 142 private DialerToolbar toolbar; 143 private View sendAndCall; 144 private TextView sendAndCallText; 145 146 private ProgressBar loading; 147 private ImageView cameraIcon; 148 private ImageView galleryIcon; 149 private ImageView messageIcon; 150 private LockableViewPager pager; 151 private CallComposerPagerAdapter adapter; 152 153 private FrameLayout background; 154 private LinearLayout windowContainer; 155 156 private DialerExecutor<Uri> copyAndResizeExecutor; 157 private FastOutSlowInInterpolator interpolator; 158 private boolean shouldAnimateEntrance = true; 159 private boolean inFullscreenMode; 160 private boolean isSendAndCallHidingOrHidden = true; 161 private boolean sendAndCallReady; 162 private boolean runningExitAnimation; 163 private int currentIndex; 164 newIntent(Context context, DialerContact contact)165 public static Intent newIntent(Context context, DialerContact contact) { 166 Intent intent = new Intent(context, CallComposerActivity.class); 167 ProtoParsers.put(intent, ARG_CALL_COMPOSER_CONTACT, contact); 168 return intent; 169 } 170 171 @Override onCreate(Bundle savedInstanceState)172 protected void onCreate(Bundle savedInstanceState) { 173 super.onCreate(savedInstanceState); 174 setContentView(R.layout.call_composer_activity); 175 176 nameView = findViewById(R.id.contact_name); 177 numberView = findViewById(R.id.phone_number); 178 contactPhoto = findViewById(R.id.contact_photo); 179 cameraIcon = findViewById(R.id.call_composer_camera); 180 galleryIcon = findViewById(R.id.call_composer_photo); 181 messageIcon = findViewById(R.id.call_composer_message); 182 contactContainer = findViewById(R.id.contact_bar); 183 pager = findViewById(R.id.call_composer_view_pager); 184 background = findViewById(R.id.background); 185 windowContainer = findViewById(R.id.call_composer_container); 186 toolbar = findViewById(R.id.toolbar); 187 sendAndCall = findViewById(R.id.send_and_call_button); 188 sendAndCallText = findViewById(R.id.send_and_call_text); 189 loading = findViewById(R.id.call_composer_loading); 190 191 interpolator = new FastOutSlowInInterpolator(); 192 adapter = 193 new CallComposerPagerAdapter( 194 getSupportFragmentManager(), 195 getResources().getInteger(R.integer.call_composer_message_limit)); 196 pager.setAdapter(adapter); 197 pager.addOnPageChangeListener(this); 198 199 cameraIcon.setOnClickListener(this); 200 galleryIcon.setOnClickListener(this); 201 messageIcon.setOnClickListener(this); 202 sendAndCall.setOnClickListener(this); 203 204 onHandleIntent(getIntent()); 205 206 if (savedInstanceState != null) { 207 shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY); 208 sendAndCallReady = savedInstanceState.getBoolean(SEND_AND_CALL_READY_KEY); 209 pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY)); 210 currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY); 211 sessionId = savedInstanceState.getLong(SESSION_ID_KEY, Session.NO_SESSION_ID); 212 onPageSelected(currentIndex); 213 } 214 215 // Since we can't animate the views until they are ready to be drawn, we use this listener to 216 // track that and animate the call compose UI as soon as it's ready. 217 ViewUtil.doOnPreDraw( 218 windowContainer, 219 false, 220 () -> { 221 showFullscreen(inFullscreenMode); 222 runEntranceAnimation(); 223 }); 224 225 setMediaIconSelected(currentIndex); 226 227 copyAndResizeExecutor = 228 DialerExecutorComponent.get(getApplicationContext()) 229 .dialerExecutorFactory() 230 .createUiTaskBuilder( 231 getFragmentManager(), 232 "copyAndResizeImageToSend", 233 new CopyAndResizeImageWorker(this.getApplicationContext())) 234 .onSuccess(this::onCopyAndResizeImageSuccess) 235 .onFailure(this::onCopyAndResizeImageFailure) 236 .build(); 237 } 238 onCopyAndResizeImageSuccess(Pair<File, String> output)239 private void onCopyAndResizeImageSuccess(Pair<File, String> output) { 240 Uri shareableUri = 241 FileProvider.getUriForFile( 242 CallComposerActivity.this, Constants.get().getFileProviderAuthority(), output.first); 243 244 placeRCSCall( 245 MultimediaData.builder().setImage(grantUriPermission(shareableUri), output.second)); 246 } 247 onCopyAndResizeImageFailure(Throwable throwable)248 private void onCopyAndResizeImageFailure(Throwable throwable) { 249 // TODO(a bug) - gracefully handle message failure 250 LogUtil.e("CallComposerActivity.onCopyAndResizeImageFailure", "copy Failed", throwable); 251 } 252 253 @Override onResume()254 protected void onResume() { 255 super.onResume(); 256 getEnrichedCallManager().registerStateChangedListener(this); 257 if (pendingCallStarted) { 258 // User went into incall ui and pressed disconnect before the image was done uploading. 259 // Kill the activity and cancel the telecom call. 260 timeoutHandler.removeCallbacks(placeTelecomCallRunnable); 261 setResult(RESULT_OK); 262 finish(); 263 } else if (sessionId == Session.NO_SESSION_ID) { 264 LogUtil.i("CallComposerActivity.onResume", "creating new session"); 265 sessionId = getEnrichedCallManager().startCallComposerSession(contact.getNumber()); 266 } else if (getEnrichedCallManager().getSession(sessionId) == null) { 267 LogUtil.i( 268 "CallComposerActivity.onResume", "session closed while activity paused, creating new"); 269 sessionId = getEnrichedCallManager().startCallComposerSession(contact.getNumber()); 270 } else { 271 LogUtil.i("CallComposerActivity.onResume", "session still open, using old"); 272 } 273 if (sessionId == Session.NO_SESSION_ID) { 274 LogUtil.w("CallComposerActivity.onResume", "failed to create call composer session"); 275 setFailedResultAndFinish(); 276 } 277 refreshUiForCallComposerState(); 278 } 279 280 @Override onDestroy()281 protected void onDestroy() { 282 super.onDestroy(); 283 getEnrichedCallManager().unregisterStateChangedListener(this); 284 timeoutHandler.removeCallbacksAndMessages(null); 285 } 286 287 /** 288 * This listener is registered in onResume and removed in onDestroy, meaning that calls to this 289 * method can come after onStop and updates to UI could cause crashes. 290 */ 291 @Override onEnrichedCallStateChanged()292 public void onEnrichedCallStateChanged() { 293 refreshUiForCallComposerState(); 294 } 295 refreshUiForCallComposerState()296 private void refreshUiForCallComposerState() { 297 Session session = getEnrichedCallManager().getSession(sessionId); 298 if (session == null) { 299 return; 300 } 301 302 @State int state = session.getState(); 303 LogUtil.i( 304 "CallComposerActivity.refreshUiForCallComposerState", 305 "state: %s", 306 StateExtension.toString(state)); 307 308 switch (state) { 309 case Session.STATE_STARTING: 310 timeoutHandler.postDelayed(sessionStartedTimedOut, getSessionStartedTimeoutMillis()); 311 if (sendAndCallReady) { 312 showLoadingUi(); 313 } 314 break; 315 case Session.STATE_STARTED: 316 timeoutHandler.removeCallbacks(sessionStartedTimedOut); 317 if (sendAndCallReady) { 318 sendAndCall(); 319 } 320 break; 321 case Session.STATE_START_FAILED: 322 case Session.STATE_CLOSED: 323 if (pendingCallStarted) { 324 placeTelecomCall(); 325 } else { 326 setFailedResultAndFinish(); 327 } 328 break; 329 case Session.STATE_MESSAGE_SENT: 330 if (++messageSentCounter == 3) { 331 // When we compose EC with images, there are 3 steps: 332 // 1. Message sent with no data 333 // 2. Image uploaded 334 // 3. url sent 335 // Once we receive 3 message sent updates, we know that we can proceed with the call. 336 timeoutHandler.removeCallbacks(placeTelecomCallRunnable); 337 placeTelecomCall(); 338 } 339 break; 340 case Session.STATE_MESSAGE_FAILED: 341 case Session.STATE_NONE: 342 default: 343 break; 344 } 345 } 346 347 @VisibleForTesting getSessionStartedTimeoutMillis()348 public long getSessionStartedTimeoutMillis() { 349 return ConfigProviderComponent.get(this) 350 .getConfigProvider() 351 .getLong("ec_session_started_timeout", 10_000); 352 } 353 354 @Override onNewIntent(Intent intent)355 protected void onNewIntent(Intent intent) { 356 super.onNewIntent(intent); 357 onHandleIntent(intent); 358 } 359 360 @Override onClick(View view)361 public void onClick(View view) { 362 LogUtil.enterBlock("CallComposerActivity.onClick"); 363 if (view == cameraIcon) { 364 pager.setCurrentItem(CallComposerPagerAdapter.INDEX_CAMERA, true /* animate */); 365 } else if (view == galleryIcon) { 366 pager.setCurrentItem(CallComposerPagerAdapter.INDEX_GALLERY, true /* animate */); 367 } else if (view == messageIcon) { 368 pager.setCurrentItem(CallComposerPagerAdapter.INDEX_MESSAGE, true /* animate */); 369 } else if (view == sendAndCall) { 370 sendAndCall(); 371 } else { 372 throw Assert.createIllegalStateFailException("View on click not implemented: " + view); 373 } 374 } 375 376 @Override sendAndCall()377 public void sendAndCall() { 378 if (!sessionReady()) { 379 sendAndCallReady = true; 380 showLoadingUi(); 381 LogUtil.i("CallComposerActivity.onClick", "sendAndCall pressed, but the session isn't ready"); 382 Logger.get(this) 383 .logImpression( 384 DialerImpression.Type 385 .CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY); 386 return; 387 } 388 sendAndCall.setEnabled(false); 389 CallComposerFragment fragment = 390 (CallComposerFragment) adapter.instantiateItem(pager, currentIndex); 391 MultimediaData.Builder builder = MultimediaData.builder(); 392 393 if (fragment instanceof MessageComposerFragment) { 394 MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment; 395 builder.setText(messageComposerFragment.getMessage()); 396 placeRCSCall(builder); 397 } 398 if (fragment instanceof GalleryComposerFragment) { 399 GalleryComposerFragment galleryComposerFragment = (GalleryComposerFragment) fragment; 400 // If the current data is not a copy, make one. 401 if (!galleryComposerFragment.selectedDataIsCopy()) { 402 copyAndResizeExecutor.executeParallel( 403 galleryComposerFragment.getGalleryData().getFileUri()); 404 } else { 405 Uri shareableUri = 406 FileProvider.getUriForFile( 407 this, 408 Constants.get().getFileProviderAuthority(), 409 new File(galleryComposerFragment.getGalleryData().getFilePath())); 410 411 builder.setImage( 412 grantUriPermission(shareableUri), 413 galleryComposerFragment.getGalleryData().getMimeType()); 414 415 placeRCSCall(builder); 416 } 417 } 418 if (fragment instanceof CameraComposerFragment) { 419 CameraComposerFragment cameraComposerFragment = (CameraComposerFragment) fragment; 420 cameraComposerFragment.getCameraUriWhenReady( 421 uri -> { 422 builder.setImage(grantUriPermission(uri), cameraComposerFragment.getMimeType()); 423 placeRCSCall(builder); 424 }); 425 } 426 } 427 showLoadingUi()428 private void showLoadingUi() { 429 loading.setVisibility(View.VISIBLE); 430 pager.setSwipingLocked(true); 431 } 432 sessionReady()433 private boolean sessionReady() { 434 Session session = getEnrichedCallManager().getSession(sessionId); 435 return session != null && session.getState() == Session.STATE_STARTED; 436 } 437 438 @VisibleForTesting placeRCSCall(MultimediaData.Builder builder)439 public void placeRCSCall(MultimediaData.Builder builder) { 440 MultimediaData data = builder.build(); 441 LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call, data: " + data); 442 Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL); 443 444 getEnrichedCallManager().sendCallComposerData(sessionId, data); 445 maybeShowPrivacyToast(data); 446 if (data.hasImageData() 447 && ConfigProviderComponent.get(this) 448 .getConfigProvider() 449 .getBoolean("enable_delayed_ec_images", true) 450 && !TelecomUtil.isInManagedCall(this)) { 451 timeoutHandler.postDelayed(placeTelecomCallRunnable, getRCSTimeoutMillis()); 452 startActivity( 453 CallPendingActivity.getIntent( 454 this, 455 contact.getNameOrNumber(), 456 contact.getDisplayNumber(), 457 contact.getNumberLabel(), 458 UriUtils.getLookupKeyFromUri(Uri.parse(contact.getContactUri())), 459 getString(R.string.call_composer_image_uploading), 460 Uri.parse(contact.getPhotoUri()), 461 sessionId)); 462 pendingCallStarted = true; 463 } else { 464 placeTelecomCall(); 465 } 466 } 467 maybeShowPrivacyToast(MultimediaData data)468 private void maybeShowPrivacyToast(MultimediaData data) { 469 SharedPreferences preferences = StorageComponent.get(this).unencryptedSharedPrefs(); 470 // Show a toast for privacy purposes if this is the first time a user uses call composer. 471 if (preferences.getBoolean(KEY_IS_FIRST_CALL_COMPOSE, true)) { 472 int privacyMessage = 473 data.hasImageData() ? R.string.image_sent_messages : R.string.message_sent_messages; 474 Toast toast = Toast.makeText(this, privacyMessage, Toast.LENGTH_LONG); 475 int yOffset = getResources().getDimensionPixelOffset(R.dimen.privacy_toast_y_offset); 476 toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, yOffset); 477 toast.show(); 478 preferences.edit().putBoolean(KEY_IS_FIRST_CALL_COMPOSE, false).apply(); 479 } 480 } 481 482 @VisibleForTesting getRCSTimeoutMillis()483 public long getRCSTimeoutMillis() { 484 return ConfigProviderComponent.get(this) 485 .getConfigProvider() 486 .getLong("ec_image_upload_timeout", 15_000); 487 } 488 placeTelecomCall()489 private void placeTelecomCall() { 490 PreCall.start( 491 this, 492 new CallIntentBuilder(contact.getNumber(), CallInitiationType.Type.CALL_COMPOSER) 493 // Call composer is only active if the number is associated with a known contact. 494 .setAllowAssistedDial(true)); 495 setResult(RESULT_OK); 496 finish(); 497 } 498 499 /** Give permission to Messenger to view our image for RCS purposes. */ grantUriPermission(Uri uri)500 private Uri grantUriPermission(Uri uri) { 501 // TODO(sail): Move this to the enriched call manager. 502 grantUriPermission( 503 "com.google.android.apps.messaging", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); 504 return uri; 505 } 506 507 /** Animates {@code contactContainer} to align with content inside viewpager. */ 508 @Override onPageSelected(int position)509 public void onPageSelected(int position) { 510 if (position == CallComposerPagerAdapter.INDEX_MESSAGE) { 511 sendAndCallText.setText(R.string.send_and_call); 512 } else { 513 sendAndCallText.setText(R.string.share_and_call); 514 } 515 if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) { 516 UiUtil.hideKeyboardFrom(this, windowContainer); 517 } 518 currentIndex = position; 519 CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position); 520 animateSendAndCall(fragment.shouldHide()); 521 setMediaIconSelected(position); 522 } 523 524 @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels)525 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} 526 527 @Override onPageScrollStateChanged(int state)528 public void onPageScrollStateChanged(int state) {} 529 530 @Override onSaveInstanceState(Bundle outState)531 protected void onSaveInstanceState(Bundle outState) { 532 super.onSaveInstanceState(outState); 533 outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState()); 534 outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance); 535 outState.putBoolean(SEND_AND_CALL_READY_KEY, sendAndCallReady); 536 outState.putInt(CURRENT_INDEX_KEY, currentIndex); 537 outState.putLong(SESSION_ID_KEY, sessionId); 538 } 539 540 @Override onBackPressed()541 public void onBackPressed() { 542 LogUtil.enterBlock("CallComposerActivity.onBackPressed"); 543 if (!isSendAndCallHidingOrHidden) { 544 ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer(); 545 } else if (!runningExitAnimation) { 546 // Unregister first to avoid receiving a callback when the session closes 547 getEnrichedCallManager().unregisterStateChangedListener(this); 548 549 // If the user presses the back button when the session fails, there's a race condition here 550 // since we clean up failed sessions. 551 if (getEnrichedCallManager().getSession(sessionId) != null) { 552 getEnrichedCallManager().endCallComposerSession(sessionId); 553 } 554 runExitAnimation(); 555 } 556 } 557 558 @Override composeCall(CallComposerFragment fragment)559 public void composeCall(CallComposerFragment fragment) { 560 // Since our ViewPager restores state to our fragments, it's possible that they could call 561 // #composeCall, so we have to check if the calling fragment is the current fragment. 562 if (adapter.instantiateItem(pager, currentIndex) != fragment) { 563 return; 564 } 565 animateSendAndCall(fragment.shouldHide()); 566 } 567 568 /** 569 * Reads arguments from the fragment arguments and populates the necessary instance variables. 570 * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}. 571 */ onHandleIntent(Intent intent)572 private void onHandleIntent(Intent intent) { 573 if (intent.getExtras().containsKey(ARG_CALL_COMPOSER_CONTACT_BASE64)) { 574 // Invoked from launch_call_composer.py. The proto is provided as a base64 encoded string. 575 byte[] bytes = 576 Base64.decode(intent.getStringExtra(ARG_CALL_COMPOSER_CONTACT_BASE64), Base64.DEFAULT); 577 try { 578 contact = DialerContact.parseFrom(bytes); 579 } catch (InvalidProtocolBufferException e) { 580 throw Assert.createAssertionFailException(e.toString()); 581 } 582 } else { 583 contact = 584 ProtoParsers.getTrusted( 585 intent, ARG_CALL_COMPOSER_CONTACT, DialerContact.getDefaultInstance()); 586 } 587 updateContactInfo(); 588 } 589 590 @Override isLandscapeLayout()591 public boolean isLandscapeLayout() { 592 return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 593 } 594 595 /** Populates the contact info fields based on the current contact information. */ updateContactInfo()596 private void updateContactInfo() { 597 ContactPhotoManager.getInstance(this) 598 .loadDialerThumbnailOrPhoto( 599 contactPhoto, 600 contact.hasContactUri() ? Uri.parse(contact.getContactUri()) : null, 601 contact.getPhotoId(), 602 contact.hasPhotoUri() ? Uri.parse(contact.getPhotoUri()) : null, 603 contact.getNameOrNumber(), 604 contact.getContactType()); 605 606 nameView.setText(contact.getNameOrNumber()); 607 toolbar.setTitle(contact.getNameOrNumber()); 608 if (!TextUtils.isEmpty(contact.getDisplayNumber())) { 609 numberView.setVisibility(View.VISIBLE); 610 String secondaryInfo = 611 TextUtils.isEmpty(contact.getNumberLabel()) 612 ? contact.getDisplayNumber() 613 : getString( 614 com.android.dialer.contacts.resources.R.string.call_subject_type_and_number, 615 contact.getNumberLabel(), 616 contact.getDisplayNumber()); 617 numberView.setText(secondaryInfo); 618 toolbar.setSubtitle(secondaryInfo); 619 } else { 620 numberView.setVisibility(View.GONE); 621 numberView.setText(null); 622 } 623 } 624 625 /** Animates compose UI into view */ runEntranceAnimation()626 private void runEntranceAnimation() { 627 if (!shouldAnimateEntrance) { 628 return; 629 } 630 shouldAnimateEntrance = false; 631 632 int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); 633 ValueAnimator contentAnimation = ValueAnimator.ofFloat(value, 0); 634 contentAnimation.setInterpolator(interpolator); 635 contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); 636 contentAnimation.addUpdateListener( 637 animation -> { 638 if (isLandscapeLayout()) { 639 windowContainer.setX((Float) animation.getAnimatedValue()); 640 } else { 641 windowContainer.setY((Float) animation.getAnimatedValue()); 642 } 643 }); 644 645 if (!isLandscapeLayout()) { 646 int colorFrom = ContextCompat.getColor(this, android.R.color.transparent); 647 int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color); 648 ValueAnimator backgroundAnimation = 649 ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); 650 backgroundAnimation.setInterpolator(interpolator); 651 backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds 652 backgroundAnimation.addUpdateListener( 653 animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); 654 655 AnimatorSet set = new AnimatorSet(); 656 set.play(contentAnimation).with(backgroundAnimation); 657 set.start(); 658 } else { 659 contentAnimation.start(); 660 } 661 } 662 663 /** Animates compose UI out of view and ends the activity. */ runExitAnimation()664 private void runExitAnimation() { 665 int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); 666 ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, value); 667 contentAnimation.setInterpolator(interpolator); 668 contentAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); 669 contentAnimation.addUpdateListener( 670 animation -> { 671 if (isLandscapeLayout()) { 672 windowContainer.setX((Float) animation.getAnimatedValue()); 673 } else { 674 windowContainer.setY((Float) animation.getAnimatedValue()); 675 } 676 if (animation.getAnimatedFraction() > .95) { 677 finish(); 678 } 679 }); 680 681 if (!isLandscapeLayout()) { 682 int colorTo = ContextCompat.getColor(this, android.R.color.transparent); 683 int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color); 684 ValueAnimator backgroundAnimation = 685 ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); 686 backgroundAnimation.setInterpolator(interpolator); 687 backgroundAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); 688 backgroundAnimation.addUpdateListener( 689 animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); 690 691 AnimatorSet set = new AnimatorSet(); 692 set.play(contentAnimation).with(backgroundAnimation); 693 set.start(); 694 } else { 695 contentAnimation.start(); 696 } 697 runningExitAnimation = true; 698 } 699 700 @Override showFullscreen(boolean fullscreen)701 public void showFullscreen(boolean fullscreen) { 702 inFullscreenMode = fullscreen; 703 ViewGroup.LayoutParams layoutParams = pager.getLayoutParams(); 704 if (isLandscapeLayout()) { 705 layoutParams.height = background.getHeight(); 706 toolbar.setVisibility(View.INVISIBLE); 707 contactContainer.setVisibility(View.GONE); 708 } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) { 709 layoutParams.height = background.getHeight() - toolbar.getHeight(); 710 toolbar.setVisibility(View.VISIBLE); 711 contactContainer.setVisibility(View.GONE); 712 } else { 713 layoutParams.height = 714 getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height); 715 toolbar.setVisibility(View.INVISIBLE); 716 contactContainer.setVisibility(View.VISIBLE); 717 } 718 pager.setLayoutParams(layoutParams); 719 } 720 721 @Override isFullscreen()722 public boolean isFullscreen() { 723 return inFullscreenMode; 724 } 725 animateSendAndCall(final boolean shouldHide)726 private void animateSendAndCall(final boolean shouldHide) { 727 // createCircularReveal doesn't respect animations being disabled, handle it here. 728 if (ViewUtil.areAnimationsDisabled(this)) { 729 isSendAndCallHidingOrHidden = shouldHide; 730 sendAndCall.setVisibility(shouldHide ? View.INVISIBLE : View.VISIBLE); 731 return; 732 } 733 734 // If the animation is changing directions, start it again. Else do nothing. 735 if (isSendAndCallHidingOrHidden != shouldHide) { 736 int centerX = sendAndCall.getWidth() / 2; 737 int centerY = sendAndCall.getHeight() / 2; 738 int startRadius = shouldHide ? centerX : 0; 739 int endRadius = shouldHide ? 0 : centerX; 740 741 // When the device rotates and state is restored, the send and call button may not be attached 742 // yet and this causes a crash when we attempt to to reveal it. To prevent this, we wait until 743 // {@code sendAndCall} is ready, then animate and reveal it. 744 ViewUtil.doOnPreDraw( 745 sendAndCall, 746 true, 747 () -> { 748 Animator animator = 749 ViewAnimationUtils.createCircularReveal( 750 sendAndCall, centerX, centerY, startRadius, endRadius); 751 animator.addListener( 752 new AnimatorListener() { 753 @Override 754 public void onAnimationStart(Animator animation) { 755 isSendAndCallHidingOrHidden = shouldHide; 756 sendAndCall.setVisibility(View.VISIBLE); 757 cameraIcon.setVisibility(View.VISIBLE); 758 galleryIcon.setVisibility(View.VISIBLE); 759 messageIcon.setVisibility(View.VISIBLE); 760 } 761 762 @Override 763 public void onAnimationEnd(Animator animation) { 764 if (isSendAndCallHidingOrHidden) { 765 sendAndCall.setVisibility(View.INVISIBLE); 766 } else { 767 // hide buttons to prevent overdrawing and talkback discoverability 768 cameraIcon.setVisibility(View.GONE); 769 galleryIcon.setVisibility(View.GONE); 770 messageIcon.setVisibility(View.GONE); 771 } 772 } 773 774 @Override 775 public void onAnimationCancel(Animator animation) {} 776 777 @Override 778 public void onAnimationRepeat(Animator animation) {} 779 }); 780 animator.start(); 781 }); 782 } 783 } 784 setMediaIconSelected(int position)785 private void setMediaIconSelected(int position) { 786 float alpha = 0.7f; 787 cameraIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_CAMERA ? 1 : alpha); 788 galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha); 789 messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha); 790 } 791 setFailedResultAndFinish()792 private void setFailedResultAndFinish() { 793 setResult( 794 RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.getNameOrNumber())); 795 finish(); 796 } 797 798 @NonNull getEnrichedCallManager()799 private EnrichedCallManager getEnrichedCallManager() { 800 return EnrichedCallComponent.get(this).getEnrichedCallManager(); 801 } 802 } 803