| /* |
| * 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: |
| * |
| * <ul> |
| * <li>Contact galleryIcon |
| * <li>Name |
| * <li>Number |
| * <li>Media options to attach a gallery image, camera image or a message |
| * </ul> |
| */ |
| 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<Uri> 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<File, String> 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(); |
| } |
| } |