diff options
19 files changed, 355 insertions, 188 deletions
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 8973a329f..20152af03 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -18,6 +18,7 @@ package com.android.dialer.binary.basecomponent; import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.database.CallLogDatabaseComponent; +import com.android.dialer.calllog.ui.CallLogUiComponent; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.configprovider.ConfigProviderComponent; import com.android.dialer.duo.DuoComponent; @@ -44,6 +45,7 @@ public interface BaseDialerRootComponent extends CallLocationComponent.HasComponent, CallLogComponent.HasComponent, CallLogDatabaseComponent.HasComponent, + CallLogUiComponent.HasComponent, ConfigProviderComponent.HasComponent, DialerExecutorComponent.HasComponent, DuoComponent.HasComponent, diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index 68d4b95df..f90d657b8 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -60,7 +60,8 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { + (AnnotatedCallLog.TRANSCRIPTION + " integer, ") + (AnnotatedCallLog.VOICEMAIL_URI + " text, ") + (AnnotatedCallLog.CALL_TYPE + " integer, ") - + (AnnotatedCallLog.CAN_REPORT_AS_INVALID_NUMBER + " integer") + + (AnnotatedCallLog.CAN_REPORT_AS_INVALID_NUMBER + " integer, ") + + (AnnotatedCallLog.CP2_INFO_INCOMPLETE + " integer") + ");"; /** Deletes all but the first maxRows rows (by timestamp) to keep the table a manageable size. */ diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index 1b3e09095..9161d6087 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -185,6 +185,13 @@ public class AnnotatedCallLogContract { */ String CAN_REPORT_AS_INVALID_NUMBER = "can_report_as_invalid_number"; + /** + * True if the CP2 information is incomplete and needs to be queried at display time. + * + * <p>TYPE: INTEGER (boolean) + */ + String CP2_INFO_INCOMPLETE = "cp2_info_incomplete"; + String[] ALL_COMMON_COLUMNS = new String[] { _ID, @@ -207,7 +214,8 @@ public class AnnotatedCallLogContract { IS_BUSINESS, IS_VOICEMAIL, CALL_TYPE, - CAN_REPORT_AS_INVALID_NUMBER + CAN_REPORT_AS_INVALID_NUMBER, + CP2_INFO_INCOMPLETE }; } diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 6ec11ad13..935ea7406 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -276,6 +276,7 @@ public final class PhoneLookupDataSource .useMostRecentLong(AnnotatedCallLog.PHOTO_ID) .useMostRecentString(AnnotatedCallLog.LOOKUP_URI) .useMostRecentInt(AnnotatedCallLog.CAN_REPORT_AS_INVALID_NUMBER) + .useMostRecentInt(AnnotatedCallLog.CP2_INFO_INCOMPLETE) .combine(); } @@ -582,6 +583,8 @@ public final class PhoneLookupDataSource contentValues.put( AnnotatedCallLog.CAN_REPORT_AS_INVALID_NUMBER, PhoneLookupSelector.canReportAsInvalidNumber(phoneLookupInfo)); + contentValues.put( + AnnotatedCallLog.CP2_INFO_INCOMPLETE, phoneLookupInfo.getCp2Info().getIsIncomplete()); } private static Uri numberUri(String number) { diff --git a/java/com/android/dialer/calllog/model/CoalescedRow.java b/java/com/android/dialer/calllog/model/CoalescedRow.java index 1824ba146..2520d996a 100644 --- a/java/com/android/dialer/calllog/model/CoalescedRow.java +++ b/java/com/android/dialer/calllog/model/CoalescedRow.java @@ -40,9 +40,12 @@ public abstract class CoalescedRow { .setIsVoicemail(false) .setCallType(0) .setCanReportAsInvalidNumber(false) + .setCp2InfoIncomplete(false) .setCoalescedIds(CoalescedIds.getDefaultInstance()); } + public abstract Builder toBuilder(); + public abstract int id(); public abstract long timestamp(); @@ -95,6 +98,8 @@ public abstract class CoalescedRow { public abstract boolean canReportAsInvalidNumber(); + public abstract boolean cp2InfoIncomplete(); + public abstract CoalescedIds coalescedIds(); /** Builder for {@link CoalescedRow}. */ @@ -144,6 +149,8 @@ public abstract class CoalescedRow { public abstract Builder setCanReportAsInvalidNumber(boolean canReportAsInvalidNumber); + public abstract Builder setCp2InfoIncomplete(boolean cp2InfoIncomplete); + public abstract Builder setCoalescedIds(CoalescedIds coalescedIds); public abstract CoalescedRow build(); diff --git a/java/com/android/dialer/calllog/ui/CallLogUiComponent.java b/java/com/android/dialer/calllog/ui/CallLogUiComponent.java new file mode 100644 index 000000000..a8e3b225b --- /dev/null +++ b/java/com/android/dialer/calllog/ui/CallLogUiComponent.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.calllog.ui; + +import android.content.Context; +import com.android.dialer.inject.HasRootComponent; +import dagger.Subcomponent; + +/** Dagger component for the call log UI package. */ +@Subcomponent +public abstract class CallLogUiComponent { + + public abstract RealtimeRowProcessor realtimeRowProcessor(); + + public static CallLogUiComponent get(Context context) { + return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) + .callLogUiComponent(); + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + CallLogUiComponent callLogUiComponent(); + } +} diff --git a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java index 6d60bdda4..5c0ce2816 100644 --- a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java +++ b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java @@ -50,7 +50,8 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { private static final int IS_VOICEMAIL = 18; private static final int CALL_TYPE = 19; private static final int CAN_REPORT_AS_INVALID_NUMBER = 20; - private static final int COALESCED_IDS = 21; + private static final int CP2_INFO_INCOMPLETE = 21; + private static final int COALESCED_IDS = 22; CoalescedAnnotatedCallLogCursorLoader(Context context) { // CoalescedAnnotatedCallLog requires that PROJECTION be ALL_COLUMNS and the following params be @@ -102,6 +103,7 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader { .setIsVoicemail(cursor.getInt(IS_VOICEMAIL) == 1) .setCallType(cursor.getInt(CALL_TYPE)) .setCanReportAsInvalidNumber(cursor.getInt(CAN_REPORT_AS_INVALID_NUMBER) == 1) + .setCp2InfoIncomplete(cursor.getInt(CP2_INFO_INCOMPLETE) == 1) .setCoalescedIds(coalescedIds) .build(); } diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java index d5cfb7e24..6dd742be5 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java @@ -15,6 +15,7 @@ */ package com.android.dialer.calllog.ui; +import android.content.Context; import android.database.Cursor; import android.support.annotation.IntDef; import android.support.annotation.Nullable; @@ -45,15 +46,17 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { private final Cursor cursor; private final Clock clock; + private final RealtimeRowProcessor realtimeRowProcessor; /** Null when the "Today" header should not be displayed. */ @Nullable private final Integer todayHeaderPosition; /** Null when the "Older" header should not be displayed. */ @Nullable private final Integer olderHeaderPosition; - NewCallLogAdapter(Cursor cursor, Clock clock) { + NewCallLogAdapter(Context context, Cursor cursor, Clock clock) { this.cursor = cursor; this.clock = clock; + this.realtimeRowProcessor = CallLogUiComponent.get(context).realtimeRowProcessor(); // Calculate header adapter positions by reading cursor. long currentTimeMillis = clock.currentTimeMillis(); @@ -95,7 +98,8 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> { return new NewCallLogViewHolder( LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.new_call_log_entry, viewGroup, false), - clock); + clock, + realtimeRowProcessor); default: throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType); } diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index 719878cec..e422b5f83 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -171,7 +171,8 @@ public final class NewCallLogFragment extends Fragment } // TODO(zachh): Handle empty cursor by showing empty view. recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(new NewCallLogAdapter(newCursor, System::currentTimeMillis)); + recyclerView.setAdapter( + new NewCallLogAdapter(getContext(), newCursor, System::currentTimeMillis)); } @Override diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java index e45257f7b..5cceac989 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java @@ -30,12 +30,18 @@ import com.android.dialer.calllog.ui.menu.NewCallLogMenu; import com.android.dialer.calllogutils.CallLogEntryText; import com.android.dialer.calllogutils.CallLogIntents; import com.android.dialer.calllogutils.CallTypeIconsView; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.compat.telephony.TelephonyManagerCompat; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.oem.MotorolaUtils; import com.android.dialer.time.Clock; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import java.util.Locale; +import java.util.concurrent.ExecutorService; /** {@link RecyclerView.ViewHolder} for the new call log. */ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { @@ -50,8 +56,12 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { private final ImageView menuButton; private final Clock clock; + private final RealtimeRowProcessor realtimeRowProcessor; + private final ExecutorService uiExecutorService; - NewCallLogViewHolder(View view, Clock clock) { + private int currentRowId; + + NewCallLogViewHolder(View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor) { super(view); this.context = view.getContext(); primaryTextView = view.findViewById(R.id.primary_text); @@ -63,12 +73,29 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { menuButton = view.findViewById(R.id.menu_button); this.clock = clock; + this.realtimeRowProcessor = realtimeRowProcessor; + uiExecutorService = DialerExecutorComponent.get(context).uiExecutorService(); } /** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */ void bind(Cursor cursor) { CoalescedRow row = CoalescedAnnotatedCallLogCursorLoader.toRow(cursor); + currentRowId = row.id(); // Used to make sure async updates are applied to the correct views + + // Even if there is additional real time processing necessary, we still want to immediately show + // what information we have, rather than an empty card. For example, if CP2 information needs to + // be queried on the fly, we can still show the phone number until the contact name loads. + handleRow(row); + + // Note: This leaks the view holder via the callback (which is an inner class), but this is OK + // because we only create ~10 of them (and they'll be collected assuming all jobs finish). + Futures.addCallback( + realtimeRowProcessor.applyRealtimeProcessing(row), + new RealtimeRowFutureCallback(row.id()), + uiExecutorService); + } + private void handleRow(CoalescedRow row) { // TODO(zachh): Handle RTL properly. primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row)); secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row)); @@ -152,4 +179,30 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder { private void setOnClickListenerForMenuButon(CoalescedRow row) { menuButton.setOnClickListener(NewCallLogMenu.createOnClickListener(context, row)); } + + private class RealtimeRowFutureCallback implements FutureCallback<Optional<CoalescedRow>> { + private final int id; + + RealtimeRowFutureCallback(int id) { + this.id = id; + } + + /** + * @param updatedRow the updated row if an update is required, or absent if no updates are + * required + */ + @Override + public void onSuccess(Optional<CoalescedRow> updatedRow) { + // If the user scrolled then this ViewHolder may not correspond to the completed task and + // there's nothing to do. + if (updatedRow.isPresent() && id == currentRowId) { + handleRow(updatedRow.get()); + } + } + + @Override + public void onFailure(Throwable throwable) { + LogUtil.e("RealtimeRowFutureCallback.onFailure", "realtime processing failed", throwable); + } + } } diff --git a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java new file mode 100644 index 000000000..814efc779 --- /dev/null +++ b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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.calllog.ui; + +import android.support.annotation.MainThread; +import android.util.ArrayMap; +import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.calllog.model.CoalescedRow; +import com.android.dialer.common.concurrent.Annotations.Ui; +import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; +import com.android.dialer.phonelookup.cp2.Cp2PhoneLookup; +import com.android.dialer.phonelookup.selector.PhoneLookupSelector; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.Map; +import javax.inject.Inject; + +/** + * Does work necessary to update a {@link CoalescedRow} when it is requested to be displayed. + * + * <p>In most cases this is a no-op as most AnnotatedCallLog rows can be displayed immediately + * as-is. However, there are certain times that a row from the AnnotatedCallLog cannot be displayed + * without further work being performed. + * + * <p>For example, when there are many invalid numbers in the call log, we cannot efficiently update + * the CP2 information for all of them at once, and so information for those rows must be retrieved + * at display time. + */ +public final class RealtimeRowProcessor { + + private final ListeningExecutorService uiExecutor; + private final Cp2PhoneLookup cp2PhoneLookup; + private final PhoneLookupSelector phoneLookupSelector; + + private final Map<DialerPhoneNumber, Cp2Info> cache = new ArrayMap<>(); + + @Inject + RealtimeRowProcessor( + @Ui ListeningExecutorService uiExecutor, + Cp2PhoneLookup cp2PhoneLookup, + PhoneLookupSelector phoneLookupSelector) { + this.uiExecutor = uiExecutor; + this.cp2PhoneLookup = cp2PhoneLookup; + this.phoneLookupSelector = phoneLookupSelector; + } + + /** + * Converts a {@link CoalescedRow} to a future which is the result of performing additional work + * on the row. Returns {@link Optional#absent()} if no modifications were necessary. + */ + @MainThread + ListenableFuture<Optional<CoalescedRow>> applyRealtimeProcessing(final CoalescedRow row) { + // Cp2PhoneLookup can not always efficiently process all rows. + if (!row.cp2InfoIncomplete()) { + return Futures.immediateFuture(Optional.absent()); + } + + Cp2Info cachedCp2Info = cache.get(row.number()); + if (cachedCp2Info != null) { + if (cachedCp2Info.equals(Cp2Info.getDefaultInstance())) { + return Futures.immediateFuture(Optional.absent()); + } + return Futures.immediateFuture(Optional.of(applyCp2InfoToRow(cachedCp2Info, row))); + } + + ListenableFuture<Cp2Info> cp2InfoFuture = cp2PhoneLookup.lookupByNumber(row.number()); + return Futures.transform( + cp2InfoFuture, + cp2Info -> { + cache.put(row.number(), cp2Info); + if (!cp2Info.equals(Cp2Info.getDefaultInstance())) { + return Optional.of(applyCp2InfoToRow(cp2Info, row)); + } + return Optional.absent(); + }, + uiExecutor /* ensures the cache is updated on a single thread */); + } + + private CoalescedRow applyCp2InfoToRow(Cp2Info cp2Info, CoalescedRow row) { + PhoneLookupInfo phoneLookupInfo = PhoneLookupInfo.newBuilder().setCp2Info(cp2Info).build(); + // It is safe to overwrite any existing data because CP2 always has highest priority. + return row.toBuilder() + .setName(phoneLookupSelector.selectName(phoneLookupInfo)) + .setPhotoUri(phoneLookupSelector.selectPhotoUri(phoneLookupInfo)) + .setPhotoId(phoneLookupSelector.selectPhotoId(phoneLookupInfo)) + .setLookupUri(phoneLookupSelector.selectLookupUri(phoneLookupInfo)) + .setNumberTypeLabel(phoneLookupSelector.selectNumberLabel(phoneLookupInfo)) + .build(); + } +} diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index 0d312cbbe..5ae0fb68a 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -40,6 +40,7 @@ import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.phonenumberproto.PartitionedNumbers; import com.android.dialer.storage.Unencrypted; import com.android.dialer.telecom.TelecomCallUtil; @@ -51,6 +52,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; import java.util.ArrayList; import java.util.List; @@ -157,6 +159,34 @@ public final class Cp2PhoneLookup implements PhoneLookup<Cp2Info> { return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); } + /** + * Queries ContactsContract.PhoneLookup for the {@link Cp2Info} associated with the provided + * {@link DialerPhoneNumber}. Returns {@link Cp2Info#getDefaultInstance()} if there is no + * information. + */ + public ListenableFuture<Cp2Info> lookupByNumber(DialerPhoneNumber dialerPhoneNumber) { + return backgroundExecutorService.submit( + () -> { + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + String rawNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber); + if (rawNumber.isEmpty()) { + return Cp2Info.getDefaultInstance(); + } + Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>(); + try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { + if (cursor == null) { + LogUtil.w("Cp2PhoneLookup.lookup", "null cursor"); + return Cp2Info.getDefaultInstance(); + } + while (cursor.moveToNext()) { + cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + } + } + return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); + }); + } + @Override public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers); diff --git a/java/com/android/newbubble/BottomActionViewController.java b/java/com/android/newbubble/BottomActionViewController.java index 7c7105194..a34d3a2b1 100644 --- a/java/com/android/newbubble/BottomActionViewController.java +++ b/java/com/android/newbubble/BottomActionViewController.java @@ -32,14 +32,16 @@ final class BottomActionViewController { // the bubble, to prevent the bottom action view from animating if the user just wants to fling // the bubble. private static final int SHOW_TARGET_DELAY = 100; - private static final int SHOW_TARGET_DURATION = 350; - private static final int HIDE_TARGET_DURATION = 225; + private static final int SHOW_HIDE_TARGET_DURATION = 175; + private static final int HIGHLIGHT_TARGET_DURATION = 150; private static final float HIGHLIGHT_TARGET_SCALE = 1.5f; + private static final float UNHIGHLIGHT_TARGET_ALPHA = 0.38f; private final Context context; private final WindowManager windowManager; private final int gradientHeight; private final int bottomActionViewTop; + private final int textOffsetSize; private View bottomActionView; private View dismissView; @@ -54,6 +56,8 @@ final class BottomActionViewController { gradientHeight = context.getResources().getDimensionPixelSize(R.dimen.bubble_bottom_action_view_height); bottomActionViewTop = context.getResources().getDisplayMetrics().heightPixels - gradientHeight; + textOffsetSize = + context.getResources().getDimensionPixelSize(R.dimen.bubble_bottom_action_text_offset); } /** Creates and show the bottom action view. */ @@ -104,7 +108,7 @@ final class BottomActionViewController { .alpha(1f) .setInterpolator(new LinearInterpolator()) .setStartDelay(SHOW_TARGET_DELAY) - .setDuration(SHOW_TARGET_DURATION) + .setDuration(SHOW_HIDE_TARGET_DURATION) .start(); } @@ -117,7 +121,7 @@ final class BottomActionViewController { .animate() .alpha(0f) .setInterpolator(new LinearInterpolator()) - .setDuration(HIDE_TARGET_DURATION) + .setDuration(SHOW_HIDE_TARGET_DURATION) .withEndAction( () -> { // Use removeViewImmediate instead of removeView to avoid view flashing before removed @@ -143,32 +147,56 @@ final class BottomActionViewController { boolean shouldHighlightDismiss = y > bottomActionViewTop && x < middle; boolean shouldHighlightEndCall = y > bottomActionViewTop && x >= middle; + // Set target alpha back to 1 + if (!dismissHighlighted && endCallHighlighted && !shouldHighlightEndCall) { + dismissView.animate().alpha(1f).setDuration(HIGHLIGHT_TARGET_DURATION).start(); + } + if (!endCallHighlighted && dismissHighlighted && !shouldHighlightDismiss) { + endCallView.animate().alpha(1f).setDuration(HIGHLIGHT_TARGET_DURATION).start(); + } + + // Scale unhighlight target back to 1x if (!shouldHighlightDismiss && dismissHighlighted) { // Unhighlight dismiss - dismissView.animate().scaleX(1f).scaleY(1f).setDuration(HIDE_TARGET_DURATION).start(); + dismissView.animate().scaleX(1f).scaleY(1f).setDuration(HIGHLIGHT_TARGET_DURATION).start(); dismissHighlighted = false; } else if (!shouldHighlightEndCall && endCallHighlighted) { // Unhighlight end call - endCallView.animate().scaleX(1f).scaleY(1f).setDuration(HIDE_TARGET_DURATION).start(); + endCallView.animate().scaleX(1f).scaleY(1f).setDuration(HIGHLIGHT_TARGET_DURATION).start(); endCallHighlighted = false; } + // Scale highlight target larger if (shouldHighlightDismiss && !dismissHighlighted) { // Highlight dismiss + dismissView.setPivotY(dismissView.getHeight() / 2 + textOffsetSize); dismissView .animate() .scaleX(HIGHLIGHT_TARGET_SCALE) .scaleY(HIGHLIGHT_TARGET_SCALE) - .setDuration(SHOW_TARGET_DURATION) + .setDuration(HIGHLIGHT_TARGET_DURATION) + .start(); + // Fade the other target + endCallView + .animate() + .alpha(UNHIGHLIGHT_TARGET_ALPHA) + .setDuration(HIGHLIGHT_TARGET_DURATION) .start(); dismissHighlighted = true; } else if (shouldHighlightEndCall && !endCallHighlighted) { // Highlight end call + endCallView.setPivotY(dismissView.getHeight() / 2 + textOffsetSize); endCallView .animate() .scaleX(HIGHLIGHT_TARGET_SCALE) .scaleY(HIGHLIGHT_TARGET_SCALE) - .setDuration(SHOW_TARGET_DURATION) + .setDuration(HIGHLIGHT_TARGET_DURATION) + .start(); + // Fade the other target + dismissView + .animate() + .alpha(UNHIGHLIGHT_TARGET_ALPHA) + .setDuration(HIGHLIGHT_TARGET_DURATION) .start(); endCallHighlighted = true; } diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java index f5a036f93..202cfcd33 100644 --- a/java/com/android/newbubble/NewBubble.java +++ b/java/com/android/newbubble/NewBubble.java @@ -40,8 +40,6 @@ import android.support.v4.graphics.ColorUtils; import android.support.v4.os.BuildCompat; import android.support.v4.view.animation.LinearOutSlowInInterpolator; import android.text.TextUtils; -import android.transition.TransitionManager; -import android.transition.TransitionValues; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; @@ -49,7 +47,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; @@ -59,7 +56,6 @@ import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnticipateInterpolator; import android.view.animation.OvershootInterpolator; import android.widget.ImageView; -import android.widget.TextView; import android.widget.Toast; import android.widget.ViewAnimator; import com.android.dialer.common.LogUtil; @@ -84,13 +80,12 @@ public class NewBubble { // This class has some odd behavior that is not immediately obvious in order to avoid jank when // resizing. See http://go/bubble-resize for details. - // How long text should show after showText(CharSequence) is called - private static final int SHOW_TEXT_DURATION_MILLIS = 3000; // How long the new window should show before destroying the old one during resize operations. // This ensures the new window has had time to draw first. private static final int WINDOW_REDRAW_DELAY_MILLIS = 50; private static final int EXPAND_AND_COLLAPSE_ANIMATION_DURATION = 200; + private static final int HIDE_BUBBLE_ANIMATION_DURATION = 250; private static Boolean canShowBubblesForTesting = null; @@ -110,13 +105,11 @@ public class NewBubble { @Visibility private int visibility; private boolean expanded; - private boolean textShowing; - private boolean hideAfterText; private CharSequence textAfterShow; private int collapseEndAction; ViewHolder viewHolder; - private ViewPropertyAnimator collapseAnimation; + private AnimatorSet collapseAnimatorSet; private Integer overrideGravity; @VisibleForTesting AnimatorSet exitAnimatorSet; @@ -124,20 +117,6 @@ public class NewBubble { private final int leftBoundary; private int savedYPosition = -1; - private final Runnable collapseRunnable = - new Runnable() { - @Override - public void run() { - textShowing = false; - if (hideAfterText) { - // Always reset here since text shouldn't keep showing. - hideAndReset(); - } else { - viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON); - } - } - }; - /** Type of action after bubble collapse */ @Retention(RetentionPolicy.SOURCE) @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE}) @@ -309,7 +288,7 @@ public class NewBubble { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) public void startCollapse(@CollapseEnd int endAction, boolean shouldRecoverYPosition) { View expandedView = viewHolder.getExpandedView(); - if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) { + if (expandedView.getVisibility() != View.VISIBLE || collapseAnimatorSet != null) { // Drawer is already collapsed or animation is running. return; } @@ -353,31 +332,26 @@ public class NewBubble { fadeOut.setInterpolator(accelerateDecelerateInterpolator); // Play all animation together - AnimatorSet collapseAnimatorSet = new AnimatorSet(); + collapseAnimatorSet = new AnimatorSet(); collapseAnimatorSet.setDuration(EXPAND_AND_COLLAPSE_ANIMATION_DURATION); collapseAnimatorSet.playTogether(revealAnim, fadeOut, xValueAnimator); collapseAnimatorSet.addListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - collapseAnimation = null; + collapseAnimatorSet = null; expanded = false; - if (textShowing) { - // Will do resize once the text is done. - return; - } + // If collapse on the right side, the primary button move left a bit after drawer + // visibility becoming GONE. To avoid it, we create a new ViewHolder. + // It also set primary button clickable back to true, so no need to reset manually. + replaceViewHolder(); // If this collapse was to come before a hide, do it now. if (collapseEndAction == CollapseEnd.HIDE) { hide(); + collapseEndAction = CollapseEnd.NOTHING; } - collapseEndAction = CollapseEnd.NOTHING; - - // If collapse on the right side, the primary button move left a bit after drawer - // visibility becoming GONE. To avoid it, we create a new ViewHolder. - // It also set primary button clickable back to true, so no need to reset manually. - replaceViewHolder(); // Resume normal gravity after any resizing is done. handler.postDelayed( @@ -407,8 +381,6 @@ public class NewBubble { return; } - hideAfterText = false; - boolean isRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; if (windowParams == null) { @@ -489,10 +461,6 @@ public class NewBubble { /** Hide the bubble. */ public void hide() { - if (hideAfterText) { - // hideAndReset() will be called after showing text, do nothing here. - return; - } hideHelper(this::defaultAfterHidingAnimation); } @@ -564,76 +532,16 @@ public class NewBubble { } /** - * Display text in the main bubble. The bubble's drawer is not expandable while text is showing, - * and the drawer will be closed if already open. + * Display text. The bubble's drawer is not expandable while text is showing, and the drawer will + * be closed if already open. * * @param text the text to display to the user */ public void showText(@NonNull CharSequence text) { - textShowing = true; if (expanded) { startCollapse(CollapseEnd.NOTHING, false /* shouldRecoverYPosition */); - doShowText(text); - } else { - // Need to transition from old bounds to new bounds manually - NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds(); - // Prepare and capture start values - TransitionValues startValues = new TransitionValues(); - startValues.view = viewHolder.getPrimaryButton(); - transition.addTarget(startValues.view); - transition.captureStartValues(startValues); - - // If our view is not laid out yet, postpone showing the text. - if (startValues.values.isEmpty()) { - textAfterShow = text; - return; - } - - doShowText(text); - // Hide the text so we can animate it in - viewHolder.getPrimaryText().setAlpha(0); - - ViewAnimator primaryButton = viewHolder.getPrimaryButton(); - // Cancel the automatic transition scheduled in doShowText - TransitionManager.endTransitions((ViewGroup) primaryButton.getParent()); - primaryButton - .getViewTreeObserver() - .addOnPreDrawListener( - new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - primaryButton.getViewTreeObserver().removeOnPreDrawListener(this); - - // Prepare and capture end values, always use the size of primaryText since - // its invisibility makes primaryButton smaller than expected - TransitionValues endValues = new TransitionValues(); - endValues.values.put( - NewChangeOnScreenBounds.PROPNAME_WIDTH, - viewHolder.getPrimaryText().getWidth()); - endValues.values.put( - NewChangeOnScreenBounds.PROPNAME_HEIGHT, - viewHolder.getPrimaryText().getHeight()); - endValues.view = primaryButton; - transition.addTarget(endValues.view); - transition.captureEndValues(endValues); - - // animate the primary button bounds change - Animator bounds = - transition.createAnimator(primaryButton, startValues, endValues); - - // Animate the text in - Animator alpha = - ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f); - - AnimatorSet set = new AnimatorSet(); - set.play(bounds).before(alpha); - set.start(); - return false; - } - }); } - handler.removeCallbacks(collapseRunnable); - handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS); + Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); } @Nullable @@ -662,9 +570,6 @@ public class NewBubble { } void primaryButtonClick() { - if (textShowing || currentInfo.getActions().isEmpty()) { - return; - } if (expanded) { logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_CLICK_TO_COLLAPSE); startCollapse(CollapseEnd.NOTHING, true /* shouldRecoverYPosition */); @@ -702,12 +607,7 @@ public class NewBubble { // Make bubble non clickable to prevent further buggy actions viewHolder.setChildClickable(false); - if (textShowing) { - hideAfterText = true; - return; - } - - if (collapseAnimation != null) { + if (collapseAnimatorSet != null) { collapseEndAction = CollapseEnd.HIDE; return; } @@ -732,6 +632,7 @@ public class NewBubble { exitAnimatorSet.playTogether( scaleXAnimator, scaleYAnimator, avatarAlphaAnimator, iconAlphaAnimator); exitAnimatorSet.setInterpolator(new AnticipateInterpolator()); + exitAnimatorSet.setDuration(HIDE_BUBBLE_ANIMATION_DURATION); exitAnimatorSet.addListener( new AnimatorListenerAdapter() { @Override @@ -795,13 +696,6 @@ public class NewBubble { configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton()); } - @VisibleForTesting - void doShowText(@NonNull CharSequence text) { - TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent()); - viewHolder.getPrimaryText().setText(text); - viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT); - } - private void configureButton(Action action, NewCheckableButton button) { boolean isRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; @@ -844,7 +738,6 @@ public class NewBubble { viewHolder .getPrimaryButton() .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild()); - viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText()); viewHolder.getPrimaryIcon().setX(isDrawingFromRight() ? 0 : primaryIconMoveDistance); viewHolder .getPrimaryIcon() @@ -889,16 +782,10 @@ public class NewBubble { void bottomActionEndCall() { logBasicOrCallImpression(DialerImpression.Type.BUBBLE_V2_BOTTOM_ACTION_END_CALL); - // Hide without animation - hideHelper( - () -> { - defaultAfterHidingAnimation(); - DialerCall call = getCall(); - if (call != null) { - call.disconnect(); - Toast.makeText(context, R.string.incall_call_ended, Toast.LENGTH_SHORT).show(); - } - }); + DialerCall call = getCall(); + if (call != null) { + call.disconnect(); + } } private boolean isDrawingFromRight() { @@ -987,15 +874,11 @@ public class NewBubble { @VisibleForTesting class ViewHolder { - public static final int CHILD_INDEX_AVATAR_AND_ICON = 0; - public static final int CHILD_INDEX_TEXT = 1; - private NewMoveHandler moveHandler; private final NewWindowRoot root; private final ViewAnimator primaryButton; private final ImageView primaryIcon; private final ImageView primaryAvatar; - private final TextView primaryText; private final View arrow; private final NewCheckableButton fullScreenButton; @@ -1013,7 +896,6 @@ public class NewBubble { primaryButton = contentView.findViewById(R.id.bubble_button_primary); primaryAvatar = contentView.findViewById(R.id.bubble_icon_avatar); primaryIcon = contentView.findViewById(R.id.bubble_icon_primary); - primaryText = contentView.findViewById(R.id.bubble_text); arrow = contentView.findViewById(R.id.bubble_triangle); fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen); @@ -1032,6 +914,15 @@ public class NewBubble { }); root.setOnConfigurationChangedListener( (configuration) -> { + if (expanded) { + // Collapse immediately without animation + if (collapseAnimatorSet != null) { + collapseAnimatorSet.removeAllListeners(); + collapseAnimatorSet.cancel(); + } + setDrawerVisibility(View.GONE); + expanded = false; + } // The values in the current MoveHandler may be stale, so replace it. Then ensure the // Window is in bounds moveHandler = new NewMoveHandler(primaryButton, NewBubble.this); @@ -1087,10 +978,6 @@ public class NewBubble { return primaryAvatar; } - public TextView getPrimaryText() { - return primaryText; - } - public View getExpandedView() { return expandedView; } diff --git a/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml b/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml index bd13382ec..1109aa6d2 100644 --- a/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml +++ b/java/com/android/newbubble/res/drawable/bottom_action_scrim.xml @@ -20,5 +20,5 @@ <gradient android:angle="90" android:endColor="@android:color/transparent" - android:startColor="#AA000000"/> + android:startColor="#FF000000"/> </shape> diff --git a/java/com/android/newbubble/res/layout/bottom_action_base.xml b/java/com/android/newbubble/res/layout/bottom_action_base.xml index bf08e1be5..af7f7987f 100644 --- a/java/com/android/newbubble/res/layout/bottom_action_base.xml +++ b/java/com/android/newbubble/res/layout/bottom_action_base.xml @@ -18,6 +18,7 @@ android:layout_width="match_parent" android:layout_height="@dimen/bubble_bottom_action_view_height" android:orientation="horizontal" + android:gravity="center" android:background="@drawable/bottom_action_scrim"> <LinearLayout @@ -25,19 +26,19 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" - android:gravity="center_horizontal|center_vertical"> - <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/bubble_button_icon_padding" - android:src="@drawable/quantum_ic_clear_vd_theme_24" - android:tint="@color/bubble_button_color_white"/> + android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAllCaps="true" + android:layout_marginTop="@dimen/bubble_bottom_action_text_offset" android:textColor="@color/bubble_button_color_white" - android:text="Hide"/> + android:textSize="16sp" + android:fontFamily="roboto-medium" + android:text="@string/bubble_bottom_action_hide" + android:drawableStart="@drawable/quantum_ic_clear_vd_theme_24" + android:drawableTint="@color/bubble_button_color_white" + android:drawablePadding="10dp" + android:elevation="2dp"/> </LinearLayout> <LinearLayout @@ -45,19 +46,19 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" - android:gravity="center_horizontal|center_vertical"> - <ImageView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/bubble_button_icon_padding" - android:src="@drawable/quantum_ic_call_end_vd_theme_24" - android:tint="@color/bubble_button_color_white"/> + android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAllCaps="true" + android:layout_marginTop="@dimen/bubble_bottom_action_text_offset" android:textColor="@color/bubble_button_color_white" - android:text="End call"/> + android:textSize="16sp" + android:fontFamily="roboto-medium" + android:text="@string/bubble_bottom_action_end_call" + android:drawableStart="@drawable/quantum_ic_call_end_vd_theme_24" + android:drawableTint="@color/bubble_button_color_white" + android:drawablePadding="10dp" + android:elevation="2dp"/> </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml index f6ce26dd1..2b53b360e 100644 --- a/java/com/android/newbubble/res/layout/new_bubble_base.xml +++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml @@ -66,16 +66,6 @@ tools:backgroundTint="#FF0000AA" tools:src="@android:drawable/ic_btn_speak_now"/> </FrameLayout> - <TextView - android:id="@+id/bubble_text" - android:layout_width="wrap_content" - android:layout_height="@dimen/bubble_size" - android:paddingStart="@dimen/bubble_icon_padding" - android:paddingEnd="@dimen/bubble_icon_padding" - android:gravity="center" - android:minWidth="@dimen/bubble_size" - android:textAppearance="@style/TextAppearance.AppCompat" - tools:text="Call ended"/> </ViewAnimator> </RelativeLayout> <!-- The RelativeLayout below serves as boundary for @id/bubble_expanded_layout during animation --> diff --git a/java/com/android/newbubble/res/values/strings.xml b/java/com/android/newbubble/res/values/strings.xml index 5b82b181f..ce7d45606 100644 --- a/java/com/android/newbubble/res/values/strings.xml +++ b/java/com/android/newbubble/res/values/strings.xml @@ -24,4 +24,9 @@ <!-- A string to describe available action for accessibility user. It will be read as "Actions: double tap to collapse call action menu". --> <string name="a11y_bubble_primary_button_collapse_action">Collapse call action menu</string> + + <!-- The label of drag-and-drop target for dismissing bubble. [CHAR LIMIT=10]--> + <string name="bubble_bottom_action_hide">Hide</string> + <!-- The label of drag-and-drop target for ending call. [CHAR LIMIT=10]--> + <string name="bubble_bottom_action_end_call">End call</string> </resources>
\ No newline at end of file diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml index 040a5be1c..540f6653e 100644 --- a/java/com/android/newbubble/res/values/values.xml +++ b/java/com/android/newbubble/res/values/values.xml @@ -26,8 +26,8 @@ <dimen name="bubble_button_padding_horizontal">16dp</dimen> <dimen name="bubble_off_screen_size_horizontal">-4dp</dimen> - <!-- 64dp - 16dp(bubble_shadow_padding_size_vertical) --> - <dimen name="bubble_safe_margin_vertical">48dp</dimen> + <!-- 36dp - 16dp(bubble_shadow_padding_size_vertical) --> + <dimen name="bubble_safe_margin_vertical">20dp</dimen> <dimen name="bubble_shadow_padding_size_vertical">16dp</dimen> <dimen name="bubble_shadow_padding_size_vertical_minus">-16dp</dimen> @@ -42,5 +42,6 @@ <dimen name="bubble_small_icon_size">24dp</dimen> <dimen name="bubble_small_icon_padding">4dp</dimen> - <dimen name="bubble_bottom_action_view_height">180dp</dimen> + <dimen name="bubble_bottom_action_view_height">124dp</dimen> + <dimen name="bubble_bottom_action_text_offset">28dp</dimen> </resources> |