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.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