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