/* * 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.res.Configuration; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; import android.support.v4.view.ViewPager; 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.View; import android.view.View.OnClickListener; import android.view.View.OnLayoutChangeListener; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.QuickContactBadge; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toolbar; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.util.UriUtils; import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener; import com.android.dialer.callcomposer.nano.CallComposerContact; import com.android.dialer.callcomposer.util.CopyAndResizeImageTask; import com.android.dialer.callcomposer.util.CopyAndResizeImageTask.Callback; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.callintent.nano.CallInitiationType; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.UiUtil; import com.android.dialer.compat.CompatUtils; import com.android.dialer.constants.Constants; import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.EnrichedCallManager.State; import com.android.dialer.enrichedcall.Session; import com.android.dialer.enrichedcall.extensions.StateExtension; import com.android.dialer.logging.Logger; import com.android.dialer.logging.nano.DialerImpression; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.protos.ProtoParsers; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.ViewUtil; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 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, OnLayoutChangeListener, EnrichedCallManager.StateChangedListener { public static final String KEY_CONTACT_NAME = "contact_name"; 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 ENTRANCE_ANIMATION_KEY = "entrance_animation_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 CallComposerContact contact; private Long sessionId = Session.NO_SESSION_ID; private TextView nameView; private TextView numberView; private QuickContactBadge contactPhoto; private RelativeLayout contactContainer; private Toolbar toolbar; private View sendAndCall; private TextView sendAndCallText; private ImageView cameraIcon; private ImageView galleryIcon; private ImageView messageIcon; private ViewPager pager; private CallComposerPagerAdapter adapter; private FrameLayout background; private LinearLayout windowContainer; private FastOutSlowInInterpolator interpolator; private boolean shouldAnimateEntrance = true; private boolean inFullscreenMode; private boolean isSendAndCallHidingOrHidden = true; private boolean layoutChanged; private int currentIndex; public static Intent newIntent(Context context, CallComposerContact 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 = (TextView) findViewById(R.id.contact_name); numberView = (TextView) findViewById(R.id.phone_number); contactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo); cameraIcon = (ImageView) findViewById(R.id.call_composer_camera); galleryIcon = (ImageView) findViewById(R.id.call_composer_photo); messageIcon = (ImageView) findViewById(R.id.call_composer_message); contactContainer = (RelativeLayout) findViewById(R.id.contact_bar); pager = (ViewPager) findViewById(R.id.call_composer_view_pager); background = (FrameLayout) findViewById(R.id.background); windowContainer = (LinearLayout) findViewById(R.id.call_composer_container); toolbar = (Toolbar) findViewById(R.id.toolbar); sendAndCall = findViewById(R.id.send_and_call_button); sendAndCallText = (TextView) findViewById(R.id.send_and_call_text); interpolator = new FastOutSlowInInterpolator(); adapter = new CallComposerPagerAdapter( getSupportFragmentManager(), getResources().getInteger(R.integer.call_composer_message_limit)); pager.setAdapter(adapter); pager.addOnPageChangeListener(this); setActionBar(toolbar); toolbar.setNavigationIcon(R.drawable.quantum_ic_close_white_24); toolbar.setNavigationOnClickListener(v -> finish()); background.addOnLayoutChangeListener(this); cameraIcon.setOnClickListener(this); galleryIcon.setOnClickListener(this); messageIcon.setOnClickListener(this); sendAndCall.setOnClickListener(this); onHandleIntent(getIntent()); if (savedInstanceState != null) { shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_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(0); } @Override protected void onResume() { super.onResume(); getEnrichedCallManager().registerStateChangedListener(this); if (sessionId == Session.NO_SESSION_ID) { LogUtil.i("CallComposerActivity.onResume", "creating new session"); sessionId = getEnrichedCallManager().startCallComposerSession(contact.number); } else if (getEnrichedCallManager().getSession(sessionId) == null) { LogUtil.i( "CallComposerActivity.onResume", "session closed while activity paused, creating new"); sessionId = getEnrichedCallManager().startCallComposerSession(contact.number); } 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 onPause() { super.onPause(); getEnrichedCallManager().unregisterStateChangedListener(this); } @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)); if (state == EnrichedCallManager.STATE_START_FAILED || state == EnrichedCallManager.STATE_CLOSED) { setFailedResultAndFinish(); } } @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) { if (!sessionReady()) { 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()) { new CopyAndResizeImageTask( CallComposerActivity.this, galleryComposerFragment.getGalleryData().getFileUri(), new Callback() { @Override public void onCopySuccessful(File file, String mimeType) { Uri shareableUri = FileProvider.getUriForFile( CallComposerActivity.this, Constants.get().getFileProviderAuthority(), file); builder.setImage(grantUriPermission(shareableUri), mimeType); placeRCSCall(builder); } @Override public void onCopyFailed(Throwable throwable) { // TODO(b/33753902) LogUtil.e("CallComposerActivity.onCopyFailed", "copy Failed", throwable); } }) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } 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); }); } } else { Assert.fail(); } } private boolean sessionReady() { Session session = getEnrichedCallManager().getSession(sessionId); if (session == null) { return false; } return session.getState() == EnrichedCallManager.STATE_STARTED; } private void placeRCSCall(MultimediaData.Builder builder) { LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call"); Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL); getEnrichedCallManager().sendCallComposerData(sessionId, builder.build()); TelecomUtil.placeCall( this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build()); setResult(RESULT_OK); finish(); } /** Give permission to Messenger to view our image for RCS purposes. */ private Uri grantUriPermission(Uri uri) { // TODO: 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); } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode && !isLandscapeLayout()) { UiUtil.openKeyboardFrom(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.putInt(CURRENT_INDEX_KEY, currentIndex); outState.putLong(SESSION_ID_KEY, sessionId); } @Override public void onBackPressed() { if (!isSendAndCallHidingOrHidden) { ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer(); } else { // Unregister first to avoid receiving a callback when the session closes getEnrichedCallManager().unregisterStateChangedListener(this); 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()); } // To detect when the keyboard changes. @Override public void onLayoutChange( View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { // To prevent infinite layout change loops if (layoutChanged) { layoutChanged = false; return; } layoutChanged = true; showFullscreen(contactContainer.getTop() < 0 || inFullscreenMode); } /** * 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) { Bundle arguments = intent.getExtras(); if (arguments == null) { throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null."); } if (arguments.get(ARG_CALL_COMPOSER_CONTACT) instanceof String) { byte[] bytes = Base64.decode(arguments.getString(ARG_CALL_COMPOSER_CONTACT), Base64.DEFAULT); try { contact = CallComposerContact.parseFrom(bytes); } catch (InvalidProtocolBufferNanoException e) { Assert.fail(e.toString()); } } else { contact = ProtoParsers.getFromInstanceState( arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact()); } updateContactInfo(); } @Override public boolean isLandscapeLayout() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } /** * Populates the contact info fields based on the current contact information. Copied from {@link * com.android.contacts.common.dialog.CallSubjectDialog}. */ private void updateContactInfo() { if (contact.contactUri != null) { setPhoto( contact.photoId, Uri.parse(contact.photoUri), Uri.parse(contact.contactUri), contact.nameOrNumber, contact.isBusiness); } else { contactPhoto.setVisibility(View.GONE); } nameView.setText(contact.nameOrNumber); getActionBar().setTitle(contact.nameOrNumber); if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) { numberView.setVisibility(View.VISIBLE); String secondaryInfo = getString( com.android.contacts.common.R.string.call_subject_type_and_number, contact.numberLabel, contact.displayNumber); numberView.setText(secondaryInfo); toolbar.setSubtitle(secondaryInfo); } else { numberView.setVisibility(View.GONE); numberView.setText(null); } } /** * Sets the photo on the quick contact galleryIcon. Copied from {@link * com.android.contacts.common.dialog.CallSubjectDialog}. */ private void setPhoto( long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) { contactPhoto.assignContactUri(contactUri); if (CompatUtils.isLollipopCompatible()) { contactPhoto.setOverlay(null); } int contactType; if (isBusiness) { contactType = ContactPhotoManager.TYPE_BUSINESS; } else { contactType = ContactPhotoManager.TYPE_DEFAULT; } String lookupKey = null; if (contactUri != null) { lookupKey = UriUtils.getLookupKeyFromUri(contactUri); } ContactPhotoManager.DefaultImageRequest request = new ContactPhotoManager.DefaultImageRequest( displayName, lookupKey, contactType, true /* isCircular */); if (photoId == 0 && photoUri != null) { contactPhoto.setImageDrawable( getDrawable(R.drawable.product_logo_avatar_anonymous_color_120)); } else { ContactPhotoManager.getInstance(this) .loadThumbnail( contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request); } } /** 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(); } } @Override public void showFullscreen(boolean fullscreen) { inFullscreenMode = fullscreen; ViewGroup.LayoutParams layoutParams = pager.getLayoutParams(); if (isLandscapeLayout()) { layoutParams.height = background.getHeight() - messageIcon.getHeight(); toolbar.setVisibility(View.INVISIBLE); contactContainer.setVisibility(View.GONE); } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) { layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.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); } @Override public void onAnimationEnd(Animator animation) { if (isSendAndCallHidingOrHidden) { sendAndCall.setVisibility(View.INVISIBLE); } } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); animator.start(); }); } } private void setMediaIconSelected(int position) { float alpha = 0.54f; 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.nameOrNumber)); finish(); } @NonNull private EnrichedCallManager getEnrichedCallManager() { return EnrichedCallComponent.get(this).getEnrichedCallManager(); } }