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