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.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 
    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
    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
    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
    234   protected void onPause() {
    235     super.onPause();
    236     getEnrichedCallManager().unregisterStateChangedListener(this);
    237     timeoutHandler.removeCallbacks(sessionStartedTimedOut);
    238   }
    239 
    240   @Override
    241   public void onEnrichedCallStateChanged() {
    242     refreshUiForCallComposerState();
    243   }
    244 
    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
    283   public long getSessionStartedTimeoutMillis() {
    284     return ConfigProviderBindings.get(this).getLong("ec_session_started_timeout", 10_000);
    285   }
    286 
    287   @Override
    288   protected void onNewIntent(Intent intent) {
    289     super.onNewIntent(intent);
    290     onHandleIntent(intent);
    291   }
    292 
    293   @Override
    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
    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 
    381   private void showLoadingUi() {
    382     loading.setVisibility(View.VISIBLE);
    383     pager.setSwipingLocked(true);
    384   }
    385 
    386   private boolean sessionReady() {
    387     Session session = getEnrichedCallManager().getSession(sessionId);
    388     return session != null && session.getState() == Session.STATE_STARTED;
    389   }
    390 
    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. */
    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
    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
    442   public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
    443 
    444   @Override
    445   public void onPageScrollStateChanged(int state) {}
    446 
    447   @Override
    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
    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
    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    */
    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
    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. */
    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 */
    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. */
    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
    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
    632   public boolean isFullscreen() {
    633     return inFullscreenMode;
    634   }
    635 
    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 
    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 
    694   private void setFailedResultAndFinish() {
    695     setResult(
    696         RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.getNameOrNumber()));
    697     finish();
    698   }
    699 
    700   @NonNull
    701   private EnrichedCallManager getEnrichedCallManager() {
    702     return EnrichedCallComponent.get(this).getEnrichedCallManager();
    703   }
    704 }
    705