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
17package com.android.dialer.callcomposer;
18
19import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorSet;
22import android.animation.ArgbEvaluator;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.res.Configuration;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.support.annotation.NonNull;
32import android.support.annotation.VisibleForTesting;
33import android.support.v4.content.ContextCompat;
34import android.support.v4.content.FileProvider;
35import android.support.v4.view.ViewPager.OnPageChangeListener;
36import android.support.v4.view.animation.FastOutSlowInInterpolator;
37import android.support.v7.app.AppCompatActivity;
38import android.text.TextUtils;
39import android.util.Base64;
40import android.view.Gravity;
41import android.view.View;
42import android.view.View.OnClickListener;
43import android.view.ViewAnimationUtils;
44import android.view.ViewGroup;
45import android.widget.FrameLayout;
46import android.widget.ImageView;
47import android.widget.LinearLayout;
48import android.widget.ProgressBar;
49import android.widget.QuickContactBadge;
50import android.widget.RelativeLayout;
51import android.widget.TextView;
52import android.widget.Toast;
53import com.android.contacts.common.ContactPhotoManager;
54import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener;
55import com.android.dialer.callintent.CallInitiationType;
56import com.android.dialer.callintent.CallIntentBuilder;
57import com.android.dialer.common.Assert;
58import com.android.dialer.common.LogUtil;
59import com.android.dialer.common.UiUtil;
60import com.android.dialer.common.concurrent.DialerExecutors;
61import com.android.dialer.common.concurrent.ThreadUtil;
62import com.android.dialer.configprovider.ConfigProviderBindings;
63import com.android.dialer.constants.Constants;
64import com.android.dialer.dialercontact.DialerContact;
65import com.android.dialer.enrichedcall.EnrichedCallComponent;
66import com.android.dialer.enrichedcall.EnrichedCallManager;
67import com.android.dialer.enrichedcall.Session;
68import com.android.dialer.enrichedcall.Session.State;
69import com.android.dialer.enrichedcall.extensions.StateExtension;
70import com.android.dialer.logging.DialerImpression;
71import com.android.dialer.logging.Logger;
72import com.android.dialer.multimedia.MultimediaData;
73import com.android.dialer.protos.ProtoParsers;
74import com.android.dialer.telecom.TelecomUtil;
75import com.android.dialer.util.DialerUtils;
76import com.android.dialer.util.ViewUtil;
77import com.android.dialer.widget.DialerToolbar;
78import com.android.dialer.widget.LockableViewPager;
79import com.google.protobuf.InvalidProtocolBufferException;
80import 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 */
93public 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