/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.callcomposer; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; import android.support.v4.util.Pair; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Base64; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.QuickContactBadge; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.UiUtil; import com.android.dialer.common.concurrent.DialerExecutor; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.configprovider.ConfigProviderComponent; import com.android.dialer.constants.Constants; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.Session; import com.android.dialer.enrichedcall.Session.State; import com.android.dialer.enrichedcall.extensions.StateExtension; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.precall.PreCall; import com.android.dialer.protos.ProtoParsers; import com.android.dialer.storage.StorageComponent; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.UriUtils; import com.android.dialer.util.ViewUtil; import com.android.dialer.widget.BidiTextView; import com.android.dialer.widget.DialerToolbar; import com.android.dialer.widget.LockableViewPager; import com.android.incallui.callpending.CallPendingActivity; import com.google.protobuf.InvalidProtocolBufferException; import java.io.File; /** * Implements an activity which prompts for a call with additional media for an outgoing call. The * activity includes a pop up with: * * */ public class CallComposerActivity extends AppCompatActivity implements OnClickListener, OnPageChangeListener, CallComposerListener, EnrichedCallManager.StateChangedListener { public static final String KEY_CONTACT_NAME = "contact_name"; private static final String KEY_IS_FIRST_CALL_COMPOSE = "is_first_call_compose"; private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500; private static final int EXIT_ANIMATION_DURATION_MILLIS = 500; private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT"; private static final String ARG_CALL_COMPOSER_CONTACT_BASE64 = "CALL_COMPOSER_CONTACT_BASE64"; private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key"; private static final String SEND_AND_CALL_READY_KEY = "send_and_call_ready_key"; private static final String CURRENT_INDEX_KEY = "current_index_key"; private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key"; private static final String SESSION_ID_KEY = "session_id_key"; private final Handler timeoutHandler = ThreadUtil.getUiThreadHandler(); private final Runnable sessionStartedTimedOut = () -> { LogUtil.i("CallComposerActivity.sessionStartedTimedOutRunnable", "session never started"); setFailedResultAndFinish(); }; private final Runnable placeTelecomCallRunnable = () -> { LogUtil.i("CallComposerActivity.placeTelecomCallRunnable", "upload timed out."); placeTelecomCall(); }; // Counter for the number of message sent updates received from EnrichedCallManager private int messageSentCounter; private boolean pendingCallStarted; private DialerContact contact; private Long sessionId = Session.NO_SESSION_ID; private TextView nameView; private BidiTextView numberView; private QuickContactBadge contactPhoto; private RelativeLayout contactContainer; private DialerToolbar toolbar; private View sendAndCall; private TextView sendAndCallText; private ProgressBar loading; private ImageView cameraIcon; private ImageView galleryIcon; private ImageView messageIcon; private LockableViewPager pager; private CallComposerPagerAdapter adapter; private FrameLayout background; private LinearLayout windowContainer; private DialerExecutor copyAndResizeExecutor; private FastOutSlowInInterpolator interpolator; private boolean shouldAnimateEntrance = true; private boolean inFullscreenMode; private boolean isSendAndCallHidingOrHidden = true; private boolean sendAndCallReady; private boolean runningExitAnimation; private int currentIndex; public static Intent newIntent(Context context, DialerContact contact) { Intent intent = new Intent(context, CallComposerActivity.class); ProtoParsers.put(intent, ARG_CALL_COMPOSER_CONTACT, contact); return intent; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.call_composer_activity); nameView = findViewById(R.id.contact_name); numberView = findViewById(R.id.phone_number); contactPhoto = findViewById(R.id.contact_photo); cameraIcon = findViewById(R.id.call_composer_camera); galleryIcon = findViewById(R.id.call_composer_photo); messageIcon = findViewById(R.id.call_composer_message); contactContainer = findViewById(R.id.contact_bar); pager = findViewById(R.id.call_composer_view_pager); background = findViewById(R.id.background); windowContainer = findViewById(R.id.call_composer_container); toolbar = findViewById(R.id.toolbar); sendAndCall = findViewById(R.id.send_and_call_button); sendAndCallText = findViewById(R.id.send_and_call_text); loading = findViewById(R.id.call_composer_loading); interpolator = new FastOutSlowInInterpolator(); adapter = new CallComposerPagerAdapter( getSupportFragmentManager(), getResources().getInteger(R.integer.call_composer_message_limit)); pager.setAdapter(adapter); pager.addOnPageChangeListener(this); cameraIcon.setOnClickListener(this); galleryIcon.setOnClickListener(this); messageIcon.setOnClickListener(this); sendAndCall.setOnClickListener(this); onHandleIntent(getIntent()); if (savedInstanceState != null) { shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY); sendAndCallReady = savedInstanceState.getBoolean(SEND_AND_CALL_READY_KEY); pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY)); currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY); sessionId = savedInstanceState.getLong(SESSION_ID_KEY, Session.NO_SESSION_ID); onPageSelected(currentIndex); } // Since we can't animate the views until they are ready to be drawn, we use this listener to // track that and animate the call compose UI as soon as it's ready. ViewUtil.doOnPreDraw( windowContainer, false, () -> { showFullscreen(inFullscreenMode); runEntranceAnimation(); }); setMediaIconSelected(currentIndex); copyAndResizeExecutor = DialerExecutorComponent.get(getApplicationContext()) .dialerExecutorFactory() .createUiTaskBuilder( getFragmentManager(), "copyAndResizeImageToSend", new CopyAndResizeImageWorker(this.getApplicationContext())) .onSuccess(this::onCopyAndResizeImageSuccess) .onFailure(this::onCopyAndResizeImageFailure) .build(); } private void onCopyAndResizeImageSuccess(Pair output) { Uri shareableUri = FileProvider.getUriForFile( CallComposerActivity.this, Constants.get().getFileProviderAuthority(), output.first); placeRCSCall( MultimediaData.builder().setImage(grantUriPermission(shareableUri), output.second)); } private void onCopyAndResizeImageFailure(Throwable throwable) { // TODO(a bug) - gracefully handle message failure LogUtil.e("CallComposerActivity.onCopyAndResizeImageFailure", "copy Failed", throwable); } @Override protected void onResume() { super.onResume(); getEnrichedCallManager().registerStateChangedListener(this); if (pendingCallStarted) { // User went into incall ui and pressed disconnect before the image was done uploading. // Kill the activity and cancel the telecom call. timeoutHandler.removeCallbacks(placeTelecomCallRunnable); setResult(RESULT_OK); finish(); } else if (sessionId == Session.NO_SESSION_ID) { LogUtil.i("CallComposerActivity.onResume", "creating new session"); sessionId = getEnrichedCallManager().startCallComposerSession(contact.getNumber()); } else if (getEnrichedCallManager().getSession(sessionId) == null) { LogUtil.i( "CallComposerActivity.onResume", "session closed while activity paused, creating new"); sessionId = getEnrichedCallManager().startCallComposerSession(contact.getNumber()); } else { LogUtil.i("CallComposerActivity.onResume", "session still open, using old"); } if (sessionId == Session.NO_SESSION_ID) { LogUtil.w("CallComposerActivity.onResume", "failed to create call composer session"); setFailedResultAndFinish(); } refreshUiForCallComposerState(); } @Override protected void onDestroy() { super.onDestroy(); getEnrichedCallManager().unregisterStateChangedListener(this); timeoutHandler.removeCallbacksAndMessages(null); } /** * This listener is registered in onResume and removed in onDestroy, meaning that calls to this * method can come after onStop and updates to UI could cause crashes. */ @Override public void onEnrichedCallStateChanged() { refreshUiForCallComposerState(); } private void refreshUiForCallComposerState() { Session session = getEnrichedCallManager().getSession(sessionId); if (session == null) { return; } @State int state = session.getState(); LogUtil.i( "CallComposerActivity.refreshUiForCallComposerState", "state: %s", StateExtension.toString(state)); switch (state) { case Session.STATE_STARTING: timeoutHandler.postDelayed(sessionStartedTimedOut, getSessionStartedTimeoutMillis()); if (sendAndCallReady) { showLoadingUi(); } break; case Session.STATE_STARTED: timeoutHandler.removeCallbacks(sessionStartedTimedOut); if (sendAndCallReady) { sendAndCall(); } break; case Session.STATE_START_FAILED: case Session.STATE_CLOSED: if (pendingCallStarted) { placeTelecomCall(); } else { setFailedResultAndFinish(); } break; case Session.STATE_MESSAGE_SENT: if (++messageSentCounter == 3) { // When we compose EC with images, there are 3 steps: // 1. Message sent with no data // 2. Image uploaded // 3. url sent // Once we receive 3 message sent updates, we know that we can proceed with the call. timeoutHandler.removeCallbacks(placeTelecomCallRunnable); placeTelecomCall(); } break; case Session.STATE_MESSAGE_FAILED: case Session.STATE_NONE: default: break; } } @VisibleForTesting public long getSessionStartedTimeoutMillis() { return ConfigProviderComponent.get(this) .getConfigProvider() .getLong("ec_session_started_timeout", 10_000); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); onHandleIntent(intent); } @Override public void onClick(View view) { LogUtil.enterBlock("CallComposerActivity.onClick"); if (view == cameraIcon) { pager.setCurrentItem(CallComposerPagerAdapter.INDEX_CAMERA, true /* animate */); } else if (view == galleryIcon) { pager.setCurrentItem(CallComposerPagerAdapter.INDEX_GALLERY, true /* animate */); } else if (view == messageIcon) { pager.setCurrentItem(CallComposerPagerAdapter.INDEX_MESSAGE, true /* animate */); } else if (view == sendAndCall) { sendAndCall(); } else { throw Assert.createIllegalStateFailException("View on click not implemented: " + view); } } @Override public void sendAndCall() { if (!sessionReady()) { sendAndCallReady = true; showLoadingUi(); LogUtil.i("CallComposerActivity.onClick", "sendAndCall pressed, but the session isn't ready"); Logger.get(this) .logImpression( DialerImpression.Type .CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY); return; } sendAndCall.setEnabled(false); CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, currentIndex); MultimediaData.Builder builder = MultimediaData.builder(); if (fragment instanceof MessageComposerFragment) { MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment; builder.setText(messageComposerFragment.getMessage()); placeRCSCall(builder); } if (fragment instanceof GalleryComposerFragment) { GalleryComposerFragment galleryComposerFragment = (GalleryComposerFragment) fragment; // If the current data is not a copy, make one. if (!galleryComposerFragment.selectedDataIsCopy()) { copyAndResizeExecutor.executeParallel( galleryComposerFragment.getGalleryData().getFileUri()); } else { Uri shareableUri = FileProvider.getUriForFile( this, Constants.get().getFileProviderAuthority(), new File(galleryComposerFragment.getGalleryData().getFilePath())); builder.setImage( grantUriPermission(shareableUri), galleryComposerFragment.getGalleryData().getMimeType()); placeRCSCall(builder); } } if (fragment instanceof CameraComposerFragment) { CameraComposerFragment cameraComposerFragment = (CameraComposerFragment) fragment; cameraComposerFragment.getCameraUriWhenReady( uri -> { builder.setImage(grantUriPermission(uri), cameraComposerFragment.getMimeType()); placeRCSCall(builder); }); } } private void showLoadingUi() { loading.setVisibility(View.VISIBLE); pager.setSwipingLocked(true); } private boolean sessionReady() { Session session = getEnrichedCallManager().getSession(sessionId); return session != null && session.getState() == Session.STATE_STARTED; } @VisibleForTesting public void placeRCSCall(MultimediaData.Builder builder) { MultimediaData data = builder.build(); LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call, data: " + data); Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL); getEnrichedCallManager().sendCallComposerData(sessionId, data); maybeShowPrivacyToast(data); if (data.hasImageData() && ConfigProviderComponent.get(this) .getConfigProvider() .getBoolean("enable_delayed_ec_images", true) && !TelecomUtil.isInManagedCall(this)) { timeoutHandler.postDelayed(placeTelecomCallRunnable, getRCSTimeoutMillis()); startActivity( CallPendingActivity.getIntent( this, contact.getNameOrNumber(), contact.getDisplayNumber(), contact.getNumberLabel(), UriUtils.getLookupKeyFromUri(Uri.parse(contact.getContactUri())), getString(R.string.call_composer_image_uploading), Uri.parse(contact.getPhotoUri()), sessionId)); pendingCallStarted = true; } else { placeTelecomCall(); } } private void maybeShowPrivacyToast(MultimediaData data) { SharedPreferences preferences = StorageComponent.get(this).unencryptedSharedPrefs(); // Show a toast for privacy purposes if this is the first time a user uses call composer. if (preferences.getBoolean(KEY_IS_FIRST_CALL_COMPOSE, true)) { int privacyMessage = data.hasImageData() ? R.string.image_sent_messages : R.string.message_sent_messages; Toast toast = Toast.makeText(this, privacyMessage, Toast.LENGTH_LONG); int yOffset = getResources().getDimensionPixelOffset(R.dimen.privacy_toast_y_offset); toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, yOffset); toast.show(); preferences.edit().putBoolean(KEY_IS_FIRST_CALL_COMPOSE, false).apply(); } } @VisibleForTesting public long getRCSTimeoutMillis() { return ConfigProviderComponent.get(this) .getConfigProvider() .getLong("ec_image_upload_timeout", 15_000); } private void placeTelecomCall() { PreCall.start( this, new CallIntentBuilder(contact.getNumber(), CallInitiationType.Type.CALL_COMPOSER) // Call composer is only active if the number is associated with a known contact. .setAllowAssistedDial(true)); setResult(RESULT_OK); finish(); } /** Give permission to Messenger to view our image for RCS purposes. */ private Uri grantUriPermission(Uri uri) { // TODO(sail): Move this to the enriched call manager. grantUriPermission( "com.google.android.apps.messaging", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); return uri; } /** Animates {@code contactContainer} to align with content inside viewpager. */ @Override public void onPageSelected(int position) { if (position == CallComposerPagerAdapter.INDEX_MESSAGE) { sendAndCallText.setText(R.string.send_and_call); } else { sendAndCallText.setText(R.string.share_and_call); } if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) { UiUtil.hideKeyboardFrom(this, windowContainer); } currentIndex = position; CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position); animateSendAndCall(fragment.shouldHide()); setMediaIconSelected(position); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} @Override public void onPageScrollStateChanged(int state) {} @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState()); outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance); outState.putBoolean(SEND_AND_CALL_READY_KEY, sendAndCallReady); outState.putInt(CURRENT_INDEX_KEY, currentIndex); outState.putLong(SESSION_ID_KEY, sessionId); } @Override public void onBackPressed() { LogUtil.enterBlock("CallComposerActivity.onBackPressed"); if (!isSendAndCallHidingOrHidden) { ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer(); } else if (!runningExitAnimation) { // Unregister first to avoid receiving a callback when the session closes getEnrichedCallManager().unregisterStateChangedListener(this); // If the user presses the back button when the session fails, there's a race condition here // since we clean up failed sessions. if (getEnrichedCallManager().getSession(sessionId) != null) { getEnrichedCallManager().endCallComposerSession(sessionId); } runExitAnimation(); } } @Override public void composeCall(CallComposerFragment fragment) { // Since our ViewPager restores state to our fragments, it's possible that they could call // #composeCall, so we have to check if the calling fragment is the current fragment. if (adapter.instantiateItem(pager, currentIndex) != fragment) { return; } animateSendAndCall(fragment.shouldHide()); } /** * Reads arguments from the fragment arguments and populates the necessary instance variables. * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}. */ private void onHandleIntent(Intent intent) { if (intent.getExtras().containsKey(ARG_CALL_COMPOSER_CONTACT_BASE64)) { // Invoked from launch_call_composer.py. The proto is provided as a base64 encoded string. byte[] bytes = Base64.decode(intent.getStringExtra(ARG_CALL_COMPOSER_CONTACT_BASE64), Base64.DEFAULT); try { contact = DialerContact.parseFrom(bytes); } catch (InvalidProtocolBufferException e) { throw Assert.createAssertionFailException(e.toString()); } } else { contact = ProtoParsers.getTrusted( intent, ARG_CALL_COMPOSER_CONTACT, DialerContact.getDefaultInstance()); } updateContactInfo(); } @Override public boolean isLandscapeLayout() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } /** Populates the contact info fields based on the current contact information. */ private void updateContactInfo() { ContactPhotoManager.getInstance(this) .loadDialerThumbnailOrPhoto( contactPhoto, contact.hasContactUri() ? Uri.parse(contact.getContactUri()) : null, contact.getPhotoId(), contact.hasPhotoUri() ? Uri.parse(contact.getPhotoUri()) : null, contact.getNameOrNumber(), contact.getContactType()); nameView.setText(contact.getNameOrNumber()); toolbar.setTitle(contact.getNameOrNumber()); if (!TextUtils.isEmpty(contact.getDisplayNumber())) { numberView.setVisibility(View.VISIBLE); String secondaryInfo = TextUtils.isEmpty(contact.getNumberLabel()) ? contact.getDisplayNumber() : getString( com.android.dialer.contacts.resources.R.string.call_subject_type_and_number, contact.getNumberLabel(), contact.getDisplayNumber()); numberView.setText(secondaryInfo); toolbar.setSubtitle(secondaryInfo); } else { numberView.setVisibility(View.GONE); numberView.setText(null); } } /** Animates compose UI into view */ private void runEntranceAnimation() { if (!shouldAnimateEntrance) { return; } shouldAnimateEntrance = false; int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); ValueAnimator contentAnimation = ValueAnimator.ofFloat(value, 0); contentAnimation.setInterpolator(interpolator); contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); contentAnimation.addUpdateListener( animation -> { if (isLandscapeLayout()) { windowContainer.setX((Float) animation.getAnimatedValue()); } else { windowContainer.setY((Float) animation.getAnimatedValue()); } }); if (!isLandscapeLayout()) { int colorFrom = ContextCompat.getColor(this, android.R.color.transparent); int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color); ValueAnimator backgroundAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); backgroundAnimation.setInterpolator(interpolator); backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds backgroundAnimation.addUpdateListener( animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); AnimatorSet set = new AnimatorSet(); set.play(contentAnimation).with(backgroundAnimation); set.start(); } else { contentAnimation.start(); } } /** Animates compose UI out of view and ends the activity. */ private void runExitAnimation() { int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, value); contentAnimation.setInterpolator(interpolator); contentAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); contentAnimation.addUpdateListener( animation -> { if (isLandscapeLayout()) { windowContainer.setX((Float) animation.getAnimatedValue()); } else { windowContainer.setY((Float) animation.getAnimatedValue()); } if (animation.getAnimatedFraction() > .95) { finish(); } }); if (!isLandscapeLayout()) { int colorTo = ContextCompat.getColor(this, android.R.color.transparent); int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color); ValueAnimator backgroundAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); backgroundAnimation.setInterpolator(interpolator); backgroundAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); backgroundAnimation.addUpdateListener( animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); AnimatorSet set = new AnimatorSet(); set.play(contentAnimation).with(backgroundAnimation); set.start(); } else { contentAnimation.start(); } runningExitAnimation = true; } @Override public void showFullscreen(boolean fullscreen) { inFullscreenMode = fullscreen; ViewGroup.LayoutParams layoutParams = pager.getLayoutParams(); if (isLandscapeLayout()) { layoutParams.height = background.getHeight(); toolbar.setVisibility(View.INVISIBLE); contactContainer.setVisibility(View.GONE); } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) { layoutParams.height = background.getHeight() - toolbar.getHeight(); toolbar.setVisibility(View.VISIBLE); contactContainer.setVisibility(View.GONE); } else { layoutParams.height = getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height); toolbar.setVisibility(View.INVISIBLE); contactContainer.setVisibility(View.VISIBLE); } pager.setLayoutParams(layoutParams); } @Override public boolean isFullscreen() { return inFullscreenMode; } private void animateSendAndCall(final boolean shouldHide) { // createCircularReveal doesn't respect animations being disabled, handle it here. if (ViewUtil.areAnimationsDisabled(this)) { isSendAndCallHidingOrHidden = shouldHide; sendAndCall.setVisibility(shouldHide ? View.INVISIBLE : View.VISIBLE); return; } // If the animation is changing directions, start it again. Else do nothing. if (isSendAndCallHidingOrHidden != shouldHide) { int centerX = sendAndCall.getWidth() / 2; int centerY = sendAndCall.getHeight() / 2; int startRadius = shouldHide ? centerX : 0; int endRadius = shouldHide ? 0 : centerX; // When the device rotates and state is restored, the send and call button may not be attached // yet and this causes a crash when we attempt to to reveal it. To prevent this, we wait until // {@code sendAndCall} is ready, then animate and reveal it. ViewUtil.doOnPreDraw( sendAndCall, true, () -> { Animator animator = ViewAnimationUtils.createCircularReveal( sendAndCall, centerX, centerY, startRadius, endRadius); animator.addListener( new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { isSendAndCallHidingOrHidden = shouldHide; sendAndCall.setVisibility(View.VISIBLE); cameraIcon.setVisibility(View.VISIBLE); galleryIcon.setVisibility(View.VISIBLE); messageIcon.setVisibility(View.VISIBLE); } @Override public void onAnimationEnd(Animator animation) { if (isSendAndCallHidingOrHidden) { sendAndCall.setVisibility(View.INVISIBLE); } else { // hide buttons to prevent overdrawing and talkback discoverability cameraIcon.setVisibility(View.GONE); galleryIcon.setVisibility(View.GONE); messageIcon.setVisibility(View.GONE); } } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); animator.start(); }); } } private void setMediaIconSelected(int position) { float alpha = 0.7f; cameraIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_CAMERA ? 1 : alpha); galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha); messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha); } private void setFailedResultAndFinish() { setResult( RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.getNameOrNumber())); finish(); } @NonNull private EnrichedCallManager getEnrichedCallManager() { return EnrichedCallComponent.get(this).getEnrichedCallManager(); } }