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