diff options
Diffstat (limited to 'java/com/android/dialer/callcomposer/CallComposerActivity.java')
-rw-r--r-- | java/com/android/dialer/callcomposer/CallComposerActivity.java | 295 |
1 files changed, 202 insertions, 93 deletions
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java index 074fc6de1..fa380cc19 100644 --- a/java/com/android/dialer/callcomposer/CallComposerActivity.java +++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java @@ -23,51 +23,63 @@ 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.view.ViewPager; +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.View.OnLayoutChangeListener; import android.view.ViewAnimationUtils; import android.view.ViewGroup; -import android.view.WindowManager.LayoutParams; 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 com.android.contacts.common.ContactPhotoManager; +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.DialerExecutors; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.configprovider.ConfigProviderBindings; 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.EnrichedCallManager.State; 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.protos.ProtoParsers; import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.UriUtils; import com.android.dialer.util.ViewUtil; 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; @@ -86,10 +98,10 @@ public class CallComposerActivity extends AppCompatActivity implements OnClickListener, OnPageChangeListener, CallComposerListener, - OnLayoutChangeListener, 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; @@ -98,11 +110,27 @@ public class CallComposerActivity extends AppCompatActivity 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 CallComposerContact contact; + 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; @@ -113,23 +141,25 @@ public class CallComposerActivity extends AppCompatActivity private View sendAndCall; private TextView sendAndCallText; + private ProgressBar loading; private ImageView cameraIcon; private ImageView galleryIcon; private ImageView messageIcon; - private ViewPager pager; + 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 layoutChanged; + private boolean sendAndCallReady; private int currentIndex; - public static Intent newIntent(Context context, CallComposerContact contact) { + 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; @@ -140,19 +170,20 @@ public class CallComposerActivity extends AppCompatActivity 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 = (DialerToolbar) findViewById(R.id.toolbar); + 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 = (TextView) findViewById(R.id.send_and_call_text); + sendAndCallText = findViewById(R.id.send_and_call_text); + loading = findViewById(R.id.call_composer_loading); interpolator = new FastOutSlowInInterpolator(); adapter = @@ -162,7 +193,6 @@ public class CallComposerActivity extends AppCompatActivity pager.setAdapter(adapter); pager.addOnPageChangeListener(this); - background.addOnLayoutChangeListener(this); cameraIcon.setOnClickListener(this); galleryIcon.setOnClickListener(this); messageIcon.setOnClickListener(this); @@ -172,17 +202,13 @@ public class CallComposerActivity extends AppCompatActivity 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); } - int adjustMode = - isLandscapeLayout() - ? LayoutParams.SOFT_INPUT_ADJUST_PAN - : LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - getWindow().setSoftInputMode(adjustMode); // 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( @@ -194,13 +220,42 @@ public class CallComposerActivity extends AppCompatActivity }); setMediaIconSelected(currentIndex); + + copyAndResizeExecutor = + DialerExecutors.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(b/34279096) - gracefully handle message failure + LogUtil.e("CallComposerActivity.onCopyAndResizeImageFailure", "copy Failed", throwable); } @Override protected void onResume() { super.onResume(); getEnrichedCallManager().registerStateChangedListener(this); - if (sessionId == Session.NO_SESSION_ID) { + 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) { @@ -218,11 +273,16 @@ public class CallComposerActivity extends AppCompatActivity } @Override - protected void onPause() { - super.onPause(); + 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(); @@ -240,12 +300,50 @@ public class CallComposerActivity extends AppCompatActivity "state: %s", StateExtension.toString(state)); - if (state == EnrichedCallManager.STATE_START_FAILED - || state == EnrichedCallManager.STATE_CLOSED) { - setFailedResultAndFinish(); + 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 ConfigProviderBindings.get(this).getLong("ec_session_started_timeout", 10_000); + } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -264,13 +362,15 @@ public class CallComposerActivity extends AppCompatActivity } else if (view == sendAndCall) { sendAndCall(); } else { - Assert.fail(); + 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( @@ -292,28 +392,8 @@ public class CallComposerActivity extends AppCompatActivity GalleryComposerFragment galleryComposerFragment = (GalleryComposerFragment) fragment; // If the current data is not a copy, make one. if (!galleryComposerFragment.selectedDataIsCopy()) { - DialerExecutors.createUiTaskBuilder( - getFragmentManager(), - "copyAndResizeImageToSend", - new CopyAndResizeImageWorker(this.getApplicationContext())) - .onSuccess( - output -> { - Uri shareableUri = - FileProvider.getUriForFile( - CallComposerActivity.this, - Constants.get().getFileProviderAuthority(), - output.first); - - builder.setImage(grantUriPermission(shareableUri), output.second); - placeRCSCall(builder); - }) - .onFailure( - throwable -> { - // TODO(b/34279096) - gracefully handle message failure - LogUtil.e("CallComposerActivity.onCopyFailed", "copy Failed", throwable); - }) - .build() - .executeParallel(galleryComposerFragment.getGalleryData().getFileUri()); + copyAndResizeExecutor.executeParallel( + galleryComposerFragment.getGalleryData().getFileUri()); } else { Uri shareableUri = FileProvider.getUriForFile( @@ -338,19 +418,65 @@ public class CallComposerActivity extends AppCompatActivity } } + private void showLoadingUi() { + loading.setVisibility(View.VISIBLE); + pager.setSwipingLocked(true); + } + private boolean sessionReady() { Session session = getEnrichedCallManager().getSession(sessionId); - if (session == null) { - return false; + 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() + && ConfigProviderBindings.get(this).getBoolean("enable_delayed_ec_images", true) + && !TelecomUtil.isInCall(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(); } + } - return session.getState() == EnrichedCallManager.STATE_STARTED; + private void maybeShowPrivacyToast(MultimediaData data) { + SharedPreferences preferences = + DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(this); + // 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(); + } } - 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()); + @VisibleForTesting + public long getRCSTimeoutMillis() { + return ConfigProviderBindings.get(this).getLong("ec_image_upload_timeout", 15_000); + } + + private void placeTelecomCall() { TelecomUtil.placeCall( this, new CallIntentBuilder(contact.getNumber(), CallInitiationType.Type.CALL_COMPOSER).build()); @@ -360,7 +486,7 @@ public class CallComposerActivity extends AppCompatActivity /** Give permission to Messenger to view our image for RCS purposes. */ private Uri grantUriPermission(Uri uri) { - // TODO: Move this to the enriched call manager. + // TODO(sail): Move this to the enriched call manager. grantUriPermission( "com.google.android.apps.messaging", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); return uri; @@ -376,10 +502,6 @@ public class CallComposerActivity extends AppCompatActivity } 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); @@ -398,6 +520,7 @@ public class CallComposerActivity extends AppCompatActivity 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); } @@ -424,28 +547,6 @@ public class CallComposerActivity extends AppCompatActivity 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}. @@ -456,14 +557,14 @@ public class CallComposerActivity extends AppCompatActivity byte[] bytes = Base64.decode(intent.getStringExtra(ARG_CALL_COMPOSER_CONTACT_BASE64), Base64.DEFAULT); try { - contact = CallComposerContact.parseFrom(bytes); + contact = DialerContact.parseFrom(bytes); } catch (InvalidProtocolBufferException e) { throw Assert.createAssertionFailException(e.toString()); } } else { contact = ProtoParsers.getTrusted( - intent, ARG_CALL_COMPOSER_CONTACT, CallComposerContact.getDefaultInstance()); + intent, ARG_CALL_COMPOSER_CONTACT, DialerContact.getDefaultInstance()); } updateContactInfo(); } @@ -634,12 +735,20 @@ public class CallComposerActivity extends AppCompatActivity 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); } } |