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