From 2ca4318cc1ee57dda907ba2069bd61d162b1baef Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Thu, 31 Aug 2017 06:57:16 -0700 Subject: Update Dialer source to latest internal Google revision. Previously, Android's Dialer app was developed in an internal Google source control system and only exported to public during AOSP drops. The Dialer team is now switching to a public development model similar to the telephony team. This CL represents all internal Google changes that were committed to Dialer between the public O release and today's tip of tree on internal master. This CL squashes those changes into a single commit. In subsequent changes, changes will be exported on a per-commit basis. Test: make, flash install, run Merged-In: I45270eaa8ce732d71a1bd84b08c7fa0e99af3160 Change-Id: I529aaeb88535b9533c0ae4ef4e6c1222d4e0f1c8 PiperOrigin-RevId: 167068436 --- .../dialer/callcomposer/CallComposerActivity.java | 295 ++++++++++++++------- 1 file changed, 202 insertions(+), 93 deletions(-) (limited to 'java/com/android/dialer/callcomposer/CallComposerActivity.java') 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 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 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); } } -- cgit v1.2.3