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