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