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