diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2017-12-08 03:47:38 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2017-12-08 03:47:38 +0000 |
commit | fca74ac6e2540455ce280cd83a026f2825322245 (patch) | |
tree | 8cc90871e7cef664d498d359b57a95ff7a1b6a79 | |
parent | 6f7d6f1bc3f90d16f5b90c8424a5783f07bef1ec (diff) | |
parent | 81a7b490ebbf2d9a4213a79b52e7b999aa076b7f (diff) |
Merge changes I233163fe,I1dc2c8dd,I9769a7fb,Iad3b85b1,Id9088b12, ...
* changes:
Implemented PhoneLookupDataSource#onSuccesfulFill.
Fixed compile error in AOSP due to use of guava 23 API.
Add isActivated check to Duo interface
Bug: 68953167
Made PhoneLookupDataSource implementation async.
Renamed PhoneLookup#bulkUpdate to #getMostRecentPhoneLookupInfo.
Added bindings for ListeningExecutorServices.
Use explicit version constant for AD ceiling.
Add Assisted Dialing Call Details Implementation.
Switched CallLogDataSource interface to be Future based.
32 files changed, 1038 insertions, 491 deletions
diff --git a/java/com/android/bubble/Bubble.java b/java/com/android/bubble/Bubble.java index 392daaf28..a25316f5b 100644 --- a/java/com/android/bubble/Bubble.java +++ b/java/com/android/bubble/Bubble.java @@ -64,6 +64,7 @@ import android.widget.ViewAnimator; import com.android.bubble.BubbleInfo.Action; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; import java.util.List; /** @@ -96,7 +97,7 @@ public class Bubble { private BubbleInfo currentInfo; @Visibility private int visibility; - private boolean expanded; + @VisibleForTesting boolean expanded; private boolean textShowing; private boolean hideAfterText; private CharSequence textAfterShow; @@ -104,7 +105,7 @@ public class Bubble { @VisibleForTesting ViewHolder viewHolder; private ViewPropertyAnimator collapseAnimation; - private Integer overrideGravity; + @VisibleForTesting Integer overrideGravity; private ViewPropertyAnimator exitAnimator; private final Runnable collapseRunnable = @@ -497,13 +498,6 @@ public class Bubble { ViewGroup.LayoutParams layoutParams = primaryContainer.getLayoutParams(); ((FrameLayout.LayoutParams) layoutParams).gravity = onRight ? Gravity.RIGHT : Gravity.LEFT; primaryContainer.setLayoutParams(layoutParams); - - viewHolder - .getExpandedView() - .setBackgroundResource( - onRight - ? R.drawable.bubble_background_pill_rtl - : R.drawable.bubble_background_pill_ltr); } LayoutParams getWindowParams() { @@ -570,20 +564,23 @@ public class Bubble { backgroundRipple.getDrawable(0).setTint(primaryTint); viewHolder.getPrimaryButton().setBackground(backgroundRipple); - setBackgroundDrawable(viewHolder.getFirstButton(), primaryTint); - setBackgroundDrawable(viewHolder.getSecondButton(), primaryTint); - setBackgroundDrawable(viewHolder.getThirdButton(), primaryTint); + for (CheckableImageButton button : viewHolder.getActionButtons()) { + setBackgroundDrawable(button, primaryTint); + } int numButtons = currentInfo.getActions().size(); - viewHolder.getThirdButton().setVisibility(numButtons < 3 ? View.GONE : View.VISIBLE); - viewHolder.getSecondButton().setVisibility(numButtons < 2 ? View.GONE : View.VISIBLE); + for (CheckableImageButton button : viewHolder.getThirdButtons()) { + button.setVisibility(numButtons < 3 ? View.GONE : View.VISIBLE); + } + for (CheckableImageButton button : viewHolder.getSecondButtons()) { + button.setVisibility(numButtons < 2 ? View.GONE : View.VISIBLE); + } viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon()); updatePrimaryIconAnimation(); - - viewHolder - .getExpandedView() - .setBackgroundTintList(ColorStateList.valueOf(currentInfo.getPrimaryColor())); + for (View expandedView : viewHolder.getExpandedViews()) { + expandedView.setBackgroundTintList(ColorStateList.valueOf(currentInfo.getPrimaryColor())); + } updateButtonStates(); } @@ -613,11 +610,17 @@ public class Bubble { int numButtons = currentInfo.getActions().size(); if (numButtons >= 1) { - configureButton(currentInfo.getActions().get(0), viewHolder.getFirstButton()); + for (CheckableImageButton button : viewHolder.getFirstButtons()) { + configureButton(currentInfo.getActions().get(0), button); + } if (numButtons >= 2) { - configureButton(currentInfo.getActions().get(1), viewHolder.getSecondButton()); + for (CheckableImageButton button : viewHolder.getSecondButtons()) { + configureButton(currentInfo.getActions().get(1), button); + } if (numButtons >= 3) { - configureButton(currentInfo.getActions().get(2), viewHolder.getThirdButton()); + for (CheckableImageButton button : viewHolder.getThirdButtons()) { + configureButton(currentInfo.getActions().get(2), button); + } } } } @@ -788,10 +791,15 @@ public class Bubble { private final TextView primaryText; private final CheckableImageButton firstButton; + private final CheckableImageButton firstButtonRtl; private final CheckableImageButton secondButton; + private final CheckableImageButton secondButtonRtl; private final CheckableImageButton thirdButton; + private final CheckableImageButton thirdButtonRtl; private final View expandedView; + private final View expandedViewRtl; private final View shadowProvider; + private final View shadowProviderRtl; public ViewHolder(Context context) { // Window root is not in the layout file so that the inflater has a view to inflate into @@ -799,14 +807,19 @@ public class Bubble { LayoutInflater inflater = LayoutInflater.from(root.getContext()); View contentView = inflater.inflate(R.layout.bubble_base, root, true); expandedView = contentView.findViewById(R.id.bubble_expanded_layout); + expandedViewRtl = contentView.findViewById(R.id.bubble_expanded_layout_rtl); primaryButton = contentView.findViewById(R.id.bubble_button_primary); primaryIcon = contentView.findViewById(R.id.bubble_icon_primary); primaryText = contentView.findViewById(R.id.bubble_text); shadowProvider = contentView.findViewById(R.id.bubble_drawer_shadow_provider); + shadowProviderRtl = contentView.findViewById(R.id.bubble_drawer_shadow_provider_rtl); firstButton = contentView.findViewById(R.id.bubble_icon_first); + firstButtonRtl = contentView.findViewById(R.id.bubble_icon_first_rtl); secondButton = contentView.findViewById(R.id.bubble_icon_second); + secondButtonRtl = contentView.findViewById(R.id.bubble_icon_second_rtl); thirdButton = contentView.findViewById(R.id.bubble_icon_third); + thirdButtonRtl = contentView.findViewById(R.id.bubble_icon_third_rtl); root.setOnBackPressedListener( () -> { @@ -839,27 +852,34 @@ public class Bubble { int parentOffset = ((MarginLayoutParams) ((ViewGroup) expandedView.getParent()).getLayoutParams()) .leftMargin; - if (isDrawingFromRight()) { - int maxLeft = - shadowProvider.getRight() - - context.getResources().getDimensionPixelSize(R.dimen.bubble_size); - shadowProvider.setLeft( - Math.min(maxLeft, expandedView.getLeft() + translationX + parentOffset)); - } else { - int minRight = - shadowProvider.getLeft() - + context.getResources().getDimensionPixelSize(R.dimen.bubble_size); - shadowProvider.setRight( - Math.max(minRight, expandedView.getRight() + translationX + parentOffset)); - } + int minRight = + shadowProvider.getLeft() + + context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + shadowProvider.setRight( + Math.max(minRight, expandedView.getRight() + translationX + parentOffset)); + }); + expandedViewRtl + .getViewTreeObserver() + .addOnDrawListener( + () -> { + int translationX = (int) expandedViewRtl.getTranslationX(); + int parentOffset = + ((MarginLayoutParams) + ((ViewGroup) expandedViewRtl.getParent()).getLayoutParams()) + .leftMargin; + int maxLeft = + shadowProviderRtl.getRight() + - context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + shadowProviderRtl.setLeft( + Math.min(maxLeft, expandedViewRtl.getLeft() + translationX + parentOffset)); }); moveHandler = new MoveHandler(primaryButton, Bubble.this); } private void setChildClickable(boolean clickable) { - firstButton.setClickable(clickable); - secondButton.setClickable(clickable); - thirdButton.setClickable(clickable); + for (CheckableImageButton button : getActionButtons()) { + button.setClickable(clickable); + } primaryButton.setOnTouchListener(clickable ? moveHandler : null); } @@ -880,29 +900,65 @@ public class Bubble { return primaryText; } + /** Get list of all the action buttons from both LTR/RTL drawers. */ + public List<CheckableImageButton> getActionButtons() { + return Arrays.asList( + firstButton, firstButtonRtl, secondButton, secondButtonRtl, thirdButton, thirdButtonRtl); + } + + /** Get the first action button used in the current orientation drawer. */ public CheckableImageButton getFirstButton() { - return firstButton; + return isDrawingFromRight() ? firstButtonRtl : firstButton; + } + + /** Get both of the first action buttons from both LTR/RTL drawers. */ + public List<CheckableImageButton> getFirstButtons() { + return Arrays.asList(firstButton, firstButtonRtl); } + /** Get the second action button used in the current orientation drawer. */ public CheckableImageButton getSecondButton() { - return secondButton; + return isDrawingFromRight() ? secondButtonRtl : secondButton; } + /** Get both of the second action buttons from both LTR/RTL drawers. */ + public List<CheckableImageButton> getSecondButtons() { + return Arrays.asList(secondButton, secondButtonRtl); + } + + /** Get the third action button used in the current orientation drawer. */ public CheckableImageButton getThirdButton() { - return thirdButton; + return isDrawingFromRight() ? thirdButtonRtl : thirdButton; + } + + /** Get both of the third action buttons from both LTR/RTL drawers. */ + public List<CheckableImageButton> getThirdButtons() { + return Arrays.asList(thirdButton, thirdButtonRtl); } + /** Get the correct expanded view used in current bubble orientation. */ public View getExpandedView() { - return expandedView; + return isDrawingFromRight() ? expandedViewRtl : expandedView; } + /** Get both views of the LTR and RTL drawers. */ + public List<View> getExpandedViews() { + return Arrays.asList(expandedView, expandedViewRtl); + } + + /** Get the correct shadow provider view used in current bubble orientation. */ public View getShadowProvider() { - return shadowProvider; + return isDrawingFromRight() ? shadowProviderRtl : shadowProvider; } public void setDrawerVisibility(int visibility) { - expandedView.setVisibility(visibility); - shadowProvider.setVisibility(visibility); + if (isDrawingFromRight()) { + expandedViewRtl.setVisibility(visibility); + shadowProviderRtl.setVisibility(visibility); + } else { + expandedView.setVisibility(visibility); + shadowProvider.setVisibility(visibility); + } } public boolean isMoving() { diff --git a/java/com/android/bubble/res/layout/bubble_base.xml b/java/com/android/bubble/res/layout/bubble_base.xml index 3b5735cd0..0712db603 100644 --- a/java/com/android/bubble/res/layout/bubble_base.xml +++ b/java/com/android/bubble/res/layout/bubble_base.xml @@ -33,6 +33,19 @@ android:elevation="10dp" android:visibility="invisible" /> + <View + android:id="@+id/bubble_drawer_shadow_provider_rtl" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:layout_marginTop="@dimen/bubble_shadow_padding_size" + android:layout_marginBottom="@dimen/bubble_shadow_padding_size" + android:layout_marginRight="@dimen/bubble_shadow_padding_size" + android:layout_gravity="right" + android:background="@drawable/bubble_ripple_circle" + android:backgroundTint="@android:color/transparent" + android:elevation="10dp" + android:visibility="invisible" + /> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -49,7 +62,7 @@ android:paddingStart="32dp" android:paddingEnd="8dp" android:background="@drawable/bubble_background_pill_ltr" - android:layoutDirection="inherit" + android:layoutDirection="ltr" android:orientation="horizontal" android:visibility="gone" tools:backgroundTint="#FF0000FF" @@ -87,6 +100,58 @@ </LinearLayout> </FrameLayout> <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="48dp" + android:elevation="10dp" + android:paddingTop="@dimen/bubble_shadow_padding_size" + android:paddingBottom="@dimen/bubble_shadow_padding_size" + android:paddingLeft="@dimen/bubble_shadow_padding_size"> + <LinearLayout + android:id="@+id/bubble_expanded_layout_rtl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingRight="32dp" + android:paddingLeft="8dp" + android:background="@drawable/bubble_background_pill_rtl" + android:layoutDirection="rtl" + android:orientation="horizontal" + android:visibility="gone" + tools:backgroundTint="#FF0000FF" + tools:visibility="invisible"> + <com.android.bubble.CheckableImageButton + android:id="@+id/bubble_icon_first_rtl" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:layout_marginRight="4dp" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_icon_tint_states" + android:tintMode="src_in" + tools:background="@drawable/bubble_ripple_checkable_circle" + tools:src="@android:drawable/ic_lock_idle_lock"/> + <com.android.bubble.CheckableImageButton + android:id="@+id/bubble_icon_second_rtl" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:layout_marginRight="4dp" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_icon_tint_states" + android:tintMode="src_in" + tools:background="@drawable/bubble_ripple_checkable_circle" + tools:src="@android:drawable/ic_input_add"/> + <com.android.bubble.CheckableImageButton + android:id="@+id/bubble_icon_third_rtl" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:layout_marginRight="4dp" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_icon_tint_states" + android:tintMode="src_in" + tools:background="@drawable/bubble_ripple_checkable_circle" + tools:src="@android:drawable/ic_menu_call"/> + </LinearLayout> + </FrameLayout> + <FrameLayout android:id="@+id/bubble_primary_container" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/java/com/android/dialer/assisteddialing/ConcreteCreator.java b/java/com/android/dialer/assisteddialing/ConcreteCreator.java index 73817d7fc..5236ea8eb 100644 --- a/java/com/android/dialer/assisteddialing/ConcreteCreator.java +++ b/java/com/android/dialer/assisteddialing/ConcreteCreator.java @@ -41,8 +41,7 @@ public final class ConcreteCreator { // Floor set at N due to use of Optional. protected static final int BUILD_CODE_FLOOR = Build.VERSION_CODES.N; // Ceiling set at O_MR1 because this feature will ship as part of the framework in P. - // TODO(erfanian): Switch to public build constant when 27 is available in public master. - @VisibleForTesting public static final int BUILD_CODE_CEILING = 27; + @VisibleForTesting public static final int BUILD_CODE_CEILING = Build.VERSION_CODES.O_MR1; /** * Creates a new AssistedDialingMediator diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java index b51d833dc..c29f9e9ae 100644 --- a/java/com/android/dialer/calldetails/CallDetailsActivity.java +++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java @@ -33,13 +33,20 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.view.View; import android.widget.Toast; +import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; 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.concurrent.AsyncTaskExecutors; +import com.android.dialer.common.concurrent.DialerExecutor.FailureListener; +import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.constants.ActivityRequestCodes; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.duo.Duo; @@ -51,10 +58,12 @@ import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.logging.UiAction; import com.android.dialer.performancereport.PerformanceReport; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.postcall.PostCall; import com.android.dialer.precall.PreCall; import com.android.dialer.protos.ProtoParsers; import com.google.common.base.Preconditions; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; @@ -70,8 +79,8 @@ public class CallDetailsActivity extends AppCompatActivity { public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; - private final CallDetailsHeaderViewHolder.CallbackActionListener callbackActionListener = - new CallbackActionListener(this); + private final CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener = + new CallDetailsHeaderListener(this); private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener = new DeleteCallDetailsListener(this); private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener = @@ -166,7 +175,7 @@ public class CallDetailsActivity extends AppCompatActivity { this /* context */, contact, entries.getEntriesList(), - callbackActionListener, + callDetailsHeaderListener, reportCallIdListener, deleteCallDetailsListener); @@ -248,11 +257,11 @@ public class CallDetailsActivity extends AppCompatActivity { } } - private static final class CallbackActionListener - implements CallDetailsHeaderViewHolder.CallbackActionListener { - private final WeakReference<Activity> activityWeakReference; + private static final class CallDetailsHeaderListener + implements CallDetailsHeaderViewHolder.CallDetailsHeaderListener { + private final WeakReference<CallDetailsActivity> activityWeakReference; - CallbackActionListener(Activity activity) { + CallDetailsHeaderListener(CallDetailsActivity activity) { this.activityWeakReference = new WeakReference<>(activity); } @@ -303,9 +312,43 @@ public class CallDetailsActivity extends AppCompatActivity { PreCall.start(getActivity(), callIntentBuilder); } - private Activity getActivity() { + private CallDetailsActivity getActivity() { return Preconditions.checkNotNull(activityWeakReference.get()); } + + @Override + public void openAssistedDialingSettings(View unused) { + Intent intent = new Intent(getActivity(), AssistedDialingSettingActivity.class); + getActivity().startActivity(intent); + } + + @Override + public void createAssistedDialerNumberParserTask( + AssistedDialingNumberParseWorker worker, + SuccessListener<Integer> successListener, + FailureListener failureListener) { + DialerExecutorComponent.get(getActivity().getApplicationContext()) + .dialerExecutorFactory() + .createUiTaskBuilder( + getActivity().getFragmentManager(), + "CallDetailsActivity.createAssistedDialerNumberParserTask", + new AssistedDialingNumberParseWorker()) + .onSuccess(successListener) + .onFailure(failureListener) + .build() + .executeParallel(getActivity().contact.getNumber()); + } + } + + static class AssistedDialingNumberParseWorker implements Worker<String, Integer> { + + @Override + public Integer doInBackground(@NonNull String phoneNumber) { + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + DialerPhoneNumber parsedNumber = dialerPhoneNumberUtil.parse(phoneNumber, null); + return parsedNumber.getDialerInternalPhoneNumber().getCountryCode(); + } } private static final class DeleteCallDetailsListener diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java index 07590597e..9095b86ea 100644 --- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java +++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java @@ -24,7 +24,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener; -import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallbackActionListener; +import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener; import com.android.dialer.calllogutils.CallTypeHelper; import com.android.dialer.calllogutils.CallbackActionHelper; import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; @@ -41,7 +41,7 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol private static final int FOOTER_VIEW_TYPE = 3; private final DialerContact contact; - private final CallbackActionListener callbackActionListener; + private final CallDetailsHeaderListener callDetailsHeaderListener; private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener; private final DeleteCallDetailsListener deleteCallDetailsListener; private final CallTypeHelper callTypeHelper; @@ -51,12 +51,12 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol Context context, @NonNull DialerContact contact, @NonNull List<CallDetailsEntry> callDetailsEntries, - CallbackActionListener callbackActionListener, + CallDetailsHeaderListener callDetailsHeaderListener, CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener, DeleteCallDetailsListener deleteCallDetailsListener) { this.contact = Assert.isNotNull(contact); this.callDetailsEntries = callDetailsEntries; - this.callbackActionListener = callbackActionListener; + this.callDetailsHeaderListener = callDetailsHeaderListener; this.reportCallIdListener = reportCallIdListener; this.deleteCallDetailsListener = deleteCallDetailsListener; callTypeHelper = new CallTypeHelper(context.getResources(), DuoComponent.get(context).getDuo()); @@ -68,7 +68,7 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol switch (viewType) { case HEADER_VIEW_TYPE: return new CallDetailsHeaderViewHolder( - inflater.inflate(R.layout.contact_container, parent, false), callbackActionListener); + inflater.inflate(R.layout.contact_container, parent, false), callDetailsHeaderListener); case CALL_ENTRY_VIEW_TYPE: return new CallDetailsEntryViewHolder( inflater.inflate(R.layout.call_details_entry, parent, false)); @@ -87,6 +87,8 @@ final class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol public void onBindViewHolder(ViewHolder holder, int position) { if (position == 0) { // Header ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact, getCallbackAction()); + ((CallDetailsHeaderViewHolder) holder) + .updateAssistedDialingInfo(callDetailsEntries.get(position)); } else if (position == getItemCount() - 1) { ((CallDetailsFooterViewHolder) holder).setPhoneNumber(contact.getNumber()); } else { diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java index 1e08963ed..604a5e8dc 100644 --- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java +++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java @@ -25,9 +25,16 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.QuickContactBadge; +import android.widget.RelativeLayout; import android.widget.TextView; +import com.android.dialer.calldetails.CallDetailsActivity.AssistedDialingNumberParseWorker; +import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.FailureListener; +import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; +import com.android.dialer.compat.telephony.TelephonyManagerCompat; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.logging.InteractionEvent; @@ -35,20 +42,22 @@ import com.android.dialer.logging.Logger; /** ViewHolder for Header/Contact in {@link CallDetailsActivity}. */ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder - implements OnClickListener { + implements OnClickListener, FailureListener { - private final CallbackActionListener callbackActionListener; + private final CallDetailsHeaderListener callDetailsHeaderListener; private final ImageView callbackButton; private final TextView nameView; private final TextView numberView; private final TextView networkView; private final QuickContactBadge contactPhoto; private final Context context; + private final TextView assistedDialingInternationalDirectDialCodeAndCountryCodeText; + private final RelativeLayout assistedDialingContainer; private DialerContact contact; private @CallbackAction int callbackAction; - CallDetailsHeaderViewHolder(View container, CallbackActionListener callbackActionListener) { + CallDetailsHeaderViewHolder(View container, CallDetailsHeaderListener callDetailsHeaderListener) { super(container); context = container.getContext(); callbackButton = container.findViewById(R.id.call_back_button); @@ -56,14 +65,71 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder numberView = container.findViewById(R.id.phone_number); networkView = container.findViewById(R.id.network); contactPhoto = container.findViewById(R.id.quick_contact_photo); + assistedDialingInternationalDirectDialCodeAndCountryCodeText = + container.findViewById(R.id.assisted_dialing_text); + assistedDialingContainer = container.findViewById(R.id.assisted_dialing_container); + + assistedDialingContainer.setOnClickListener( + callDetailsHeaderListener::openAssistedDialingSettings); callbackButton.setOnClickListener(this); - this.callbackActionListener = callbackActionListener; + this.callDetailsHeaderListener = callDetailsHeaderListener; Logger.get(context) .logQuickContactOnTouch( contactPhoto, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_DETAILS, true); } + private boolean hasAssistedDialingFeature(Integer features) { + return (features & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING) + == TelephonyManagerCompat.FEATURES_ASSISTED_DIALING; + } + + void updateAssistedDialingInfo(CallDetailsEntry callDetailsEntry) { + + if (callDetailsEntry != null && hasAssistedDialingFeature(callDetailsEntry.getFeatures())) { + showAssistedDialingContainer(true); + callDetailsHeaderListener.createAssistedDialerNumberParserTask( + new CallDetailsActivity.AssistedDialingNumberParseWorker(), + this::updateAssistedDialingText, + this::onFailure); + + } else { + showAssistedDialingContainer(false); + } + } + + private void showAssistedDialingContainer(boolean shouldShowContainer) { + if (shouldShowContainer) { + assistedDialingContainer.setVisibility(View.VISIBLE); + } else { + LogUtil.i( + "CallDetailsHeaderViewHolder.updateAssistedDialingInfo", + "hiding assisted dialing ui elements"); + assistedDialingContainer.setVisibility(View.GONE); + } + } + + private void updateAssistedDialingText(Integer countryCode) { + + // Try and handle any poorly formed inputs. + if (countryCode <= 0) { + onFailure(new IllegalStateException()); + return; + } + + LogUtil.i( + "CallDetailsHeaderViewHolder.updateAssistedDialingText", "Updating Assisted Dialing Text"); + assistedDialingInternationalDirectDialCodeAndCountryCodeText.setText( + context.getString( + R.string.assisted_dialing_country_code_entry, String.valueOf(countryCode))); + } + + @Override + public void onFailure(Throwable unused) { + assistedDialingInternationalDirectDialCodeAndCountryCodeText.setText( + R.string.assisted_dialing_country_code_entry_failure); + } + /** Populates the contact info fields based on the current contact information. */ void updateContactInfo(DialerContact contact, @CallbackAction int callbackAction) { this.contact = contact; @@ -128,13 +194,14 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder if (view == callbackButton) { switch (callbackAction) { case CallbackAction.IMS_VIDEO: - callbackActionListener.placeImsVideoCall(contact.getNumber()); + callDetailsHeaderListener.placeImsVideoCall(contact.getNumber()); break; case CallbackAction.DUO: - callbackActionListener.placeDuoVideoCall(contact.getNumber()); + callDetailsHeaderListener.placeDuoVideoCall(contact.getNumber()); break; case CallbackAction.VOICE: - callbackActionListener.placeVoiceCall(contact.getNumber(), contact.getPostDialDigits()); + callDetailsHeaderListener.placeVoiceCall( + contact.getNumber(), contact.getPostDialDigits()); break; case CallbackAction.NONE: default: @@ -145,8 +212,8 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder } } - /** Listener for making a callback */ - interface CallbackActionListener { + /** Listener for the call details header */ + interface CallDetailsHeaderListener { /** Places an IMS video call. */ void placeImsVideoCall(String phoneNumber); @@ -156,5 +223,13 @@ public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder /** Place a traditional voice call. */ void placeVoiceCall(String phoneNumber, String postDialDigits); + + /** Access the Assisted Dialing settings * */ + void openAssistedDialingSettings(View view); + + void createAssistedDialerNumberParserTask( + AssistedDialingNumberParseWorker worker, + SuccessListener<Integer> onSuccess, + FailureListener onFailure); } } diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml index b01a6cc13..5f531ab43 100644 --- a/java/com/android/dialer/calldetails/res/layout/contact_container.xml +++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml @@ -19,49 +19,54 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/call_details_top_margin" - android:gravity="center_vertical" android:paddingTop="@dimen/contact_container_padding_top_start" - android:paddingStart="@dimen/contact_container_padding_top_start" android:paddingBottom="@dimen/contact_container_padding_bottom_end" - android:paddingEnd="@dimen/contact_container_padding_bottom_end"> + android:paddingStart="@dimen/contact_container_padding_top_start" + android:paddingEnd="@dimen/contact_container_padding_bottom_end" + android:gravity="center_vertical" + android:orientation="vertical"> <QuickContactBadge android:id="@+id/quick_contact_photo" android:layout_width="@dimen/call_details_contact_photo_size" android:layout_height="@dimen/call_details_contact_photo_size" - android:layout_centerVertical="true" android:padding="@dimen/call_details_contact_photo_padding" android:focusable="true"/> <LinearLayout - android:orientation="vertical" + android:id="@+id/contact_information" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_alignParentTop="true" android:layout_toEndOf="@+id/quick_contact_photo" android:layout_toStartOf="@+id/call_back_button" - android:layout_centerVertical="true"> + android:gravity="center_vertical" + android:minHeight="@dimen/call_details_contact_photo_size" + android:orientation="vertical"> <TextView android:id="@+id/contact_name" + style="@style/PrimaryText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/photo_text_margin" - style="@style/PrimaryText"/> + android:layout_marginStart="@dimen/photo_text_margin"/> <TextView android:id="@+id/phone_number" + style="@style/SecondaryText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/photo_text_margin" - style="@style/SecondaryText"/> + android:layout_marginStart="@dimen/photo_text_margin"/> <TextView android:id="@+id/network" + style="@style/SecondaryText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/photo_text_margin" - android:visibility="gone" - style="@style/SecondaryText"/> + android:visibility="gone"/> + + </LinearLayout> <ImageView @@ -69,10 +74,40 @@ android:layout_width="@dimen/call_back_button_size" android:layout_height="@dimen/call_back_button_size" android:layout_alignParentEnd="true" - android:layout_centerVertical="true" android:background="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/call" android:scaleType="center" android:src="@drawable/quantum_ic_call_white_24" android:tint="@color/secondary_text_color"/> + + + <RelativeLayout + android:id="@+id/assisted_dialing_container" + android:layout_width="match_parent" + android:layout_height="@dimen/ad_container_height" + android:layout_below="@+id/contact_information" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical"> + + <ImageView + android:id="@+id/assisted_dialing_globe" + android:layout_width="@dimen/ad_icon_size" + android:layout_height="@dimen/ad_icon_size" + android:layout_marginTop="@dimen/ad_icon_margin_top_offset" + android:layout_marginStart="@dimen/ad_icon_margin_start_offset" + android:scaleType="fitCenter" + android:src="@drawable/quantum_ic_language_vd_theme_24" + android:tint="@color/secondary_text_color"/> + + <TextView + android:id="@+id/assisted_dialing_text" + style="@style/SecondaryText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/ad_text_margin_start" + android:layout_marginEnd="@dimen/ad_end_margin" + android:layout_toRightOf="@id/assisted_dialing_globe"/> + + </RelativeLayout> + </RelativeLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/dimens.xml b/java/com/android/dialer/calldetails/res/values/dimens.xml index 694c8f47c..8c84b1b9b 100644 --- a/java/com/android/dialer/calldetails/res/values/dimens.xml +++ b/java/com/android/dialer/calldetails/res/values/dimens.xml @@ -18,7 +18,7 @@ <dimen name="call_details_top_margin">6dp</dimen> <!-- contact container --> - <dimen name="contact_container_padding_bottom_end">16dp</dimen> + <dimen name="contact_container_padding_bottom_end">8dp</dimen> <dimen name="contact_container_padding_top_start">12dp</dimen> <dimen name="call_details_contact_photo_size">48dp</dimen> <dimen name="call_details_contact_photo_padding">4dp</dimen> @@ -34,4 +34,14 @@ <dimen name="ec_container_height">48dp</dimen> <dimen name="ec_photo_size">40dp</dimen> <dimen name="ec_divider_top_bottom_margin">8dp</dimen> + + <!-- Assisted Dialing --> + <dimen name="ad_container_height">48dp</dimen> + <dimen name="ad_icon_size">18dp</dimen> + <dimen name="ad_end_margin">16dp</dimen> + <dimen name="ad_text_margin_start">8dp</dimen> + <!-- Used to help smooth the text alignment to the center of the icon --> + <dimen name="ad_icon_margin_top_offset">2dp</dimen> + <dimen name="ad_icon_margin_start_offset">60dp</dimen> + </resources>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/strings.xml b/java/com/android/dialer/calldetails/res/values/strings.xml index 74ac71c32..f81696034 100644 --- a/java/com/android/dialer/calldetails/res/values/strings.xml +++ b/java/com/android/dialer/calldetails/res/values/strings.xml @@ -49,4 +49,10 @@ <!-- Toast message which appears when a contact's caller id is reported as incorrect. [CHAR LIMIT=NONE] --> <string name="report_caller_id_toast">Number reported</string> + + <!-- Assisted dialing section header. [CHAR LIMIT=NONE] --> + <string name="assisted_dialing_country_code_entry">Assisted dialing: Used country code +<xliff:g example="1" id="ad_country_code">%1$s</xliff:g></string> + + <!-- A fallback string for the assisted dialing header incase parsing failes.. [CHAR LIMIT=NONE] --> + <string name="assisted_dialing_country_code_entry_failure">Assisted dialing was used</string> </resources> diff --git a/java/com/android/dialer/calllog/CallLogModule.java b/java/com/android/dialer/calllog/CallLogModule.java index 9926cebb9..6c85fd631 100644 --- a/java/com/android/dialer/calllog/CallLogModule.java +++ b/java/com/android/dialer/calllog/CallLogModule.java @@ -18,7 +18,6 @@ package com.android.dialer.calllog; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.DataSources; -import com.android.dialer.calllog.datasources.contacts.ContactsDataSource; import com.android.dialer.calllog.datasources.phonelookup.PhoneLookupDataSource; import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource; import com.google.common.collect.ImmutableList; @@ -32,11 +31,10 @@ public abstract class CallLogModule { @Provides static DataSources provideCallLogDataSources( SystemCallLogDataSource systemCallLogDataSource, - ContactsDataSource contactsDataSource, PhoneLookupDataSource phoneLookupDataSource) { // System call log must be first, see getDataSourcesExcludingSystemCallLog below. ImmutableList<CallLogDataSource> allDataSources = - ImmutableList.of(systemCallLogDataSource, contactsDataSource, phoneLookupDataSource); + ImmutableList.of(systemCallLogDataSource, phoneLookupDataSource); return new DataSources() { @Override public SystemCallLogDataSource getSystemCallLogDataSource() { diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java index d9924b23f..e5cc3eb89 100644 --- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java @@ -16,204 +16,173 @@ package com.android.dialer.calllog; -import android.annotation.TargetApi; import android.content.Context; -import android.content.OperationApplicationException; import android.content.SharedPreferences; -import android.os.Build; -import android.os.RemoteException; -import android.support.annotation.WorkerThread; import com.android.dialer.calllog.database.CallLogDatabaseComponent; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.calllog.datasources.DataSources; -import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.Annotations.UiSerial; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; +import com.android.dialer.common.concurrent.DialerFutureSerializer; +import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.storage.Unencrypted; -import com.google.common.util.concurrent.ListenableScheduledFuture; -import com.google.common.util.concurrent.ListeningScheduledExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; +import javax.inject.Singleton; /** Brings the annotated call log up to date, if necessary. */ +@Singleton public class RefreshAnnotatedCallLogWorker { - /* - * This is a reasonable time that it might take between related call log writes, that also - * shouldn't slow down single-writes too much. For example, when populating the database using - * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 - * call log entries. - */ - private static final long WAIT_MILLIS = 100L; - private final Context appContext; private final DataSources dataSources; private final SharedPreferences sharedPreferences; - private final ListeningScheduledExecutorService listeningScheduledExecutorService; - private ListenableScheduledFuture<Void> scheduledFuture; + private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; + // Used to ensure that only one refresh flow runs at a time. (Note that + // RefreshAnnotatedCallLogWorker is a @Singleton.) + private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); @Inject RefreshAnnotatedCallLogWorker( @ApplicationContext Context appContext, DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences, - @UiSerial ScheduledExecutorService serialUiExecutorService) { + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.appContext = appContext; this.dataSources = dataSources; this.sharedPreferences = sharedPreferences; - this.listeningScheduledExecutorService = - MoreExecutors.listeningDecorator(serialUiExecutorService); + this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; } /** Checks if the annotated call log is dirty and refreshes it if necessary. */ - public ListenableScheduledFuture<Void> refreshWithDirtyCheck() { + public ListenableFuture<Void> refreshWithDirtyCheck() { return refresh(true); } /** Refreshes the annotated call log, bypassing dirty checks. */ - public ListenableScheduledFuture<Void> refreshWithoutDirtyCheck() { + public ListenableFuture<Void> refreshWithoutDirtyCheck() { return refresh(false); } - private ListenableScheduledFuture<Void> refresh(boolean checkDirty) { - if (scheduledFuture != null) { - LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "cancelling waiting task"); - scheduledFuture.cancel(false /* mayInterrupt */); - } - scheduledFuture = - listeningScheduledExecutorService.schedule( - () -> doInBackground(checkDirty), WAIT_MILLIS, TimeUnit.MILLISECONDS); - return scheduledFuture; - } - - @WorkerThread - private Void doInBackground(boolean checkDirty) - throws RemoteException, OperationApplicationException { - LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackground"); - - long startTime = System.currentTimeMillis(); - checkDirtyAndRebuildIfNecessary(appContext, checkDirty); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.doInBackground", - "took %dms", - System.currentTimeMillis() - startTime); - return null; + private ListenableFuture<Void> refresh(boolean checkDirty) { + LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "submitting serialized refresh request"); + return dialerFutureSerializer.submitAsync( + () -> checkDirtyAndRebuildIfNecessary(appContext, checkDirty), lightweightExecutorService); } - @WorkerThread - private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean checkDirty) - throws RemoteException, OperationApplicationException { - Assert.isWorkerThread(); - - long startTime = System.currentTimeMillis(); - - // Default to true. If the pref doesn't exist, the annotated call log hasn't been created and - // we just skip isDirty checks and force a rebuild. - boolean forceRebuildPrefValue = - sharedPreferences.getBoolean(CallLogFramework.PREF_FORCE_REBUILD, true); - if (forceRebuildPrefValue) { - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "annotated call log has been marked dirty or does not exist"); - } - - boolean isDirty = !checkDirty || forceRebuildPrefValue || isDirty(appContext); - - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "isDirty took: %dms", - System.currentTimeMillis() - startTime); - if (isDirty) { - startTime = System.currentTimeMillis(); - rebuild(appContext); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "rebuild took: %dms", - System.currentTimeMillis() - startTime); - } + private ListenableFuture<Void> checkDirtyAndRebuildIfNecessary( + Context appContext, boolean checkDirty) { + ListenableFuture<Boolean> forceRebuildFuture = + backgroundExecutorService.submit( + () -> { + LogUtil.i( + "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", + "starting refresh flow"); + if (!checkDirty) { + return true; + } + // Default to true. If the pref doesn't exist, the annotated call log hasn't been + // created and we just skip isDirty checks and force a rebuild. + boolean forceRebuildPrefValue = + sharedPreferences.getBoolean(CallLogFramework.PREF_FORCE_REBUILD, true); + if (forceRebuildPrefValue) { + LogUtil.i( + "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", + "annotated call log has been marked dirty or does not exist"); + } + return forceRebuildPrefValue; + }); + + // After checking the "force rebuild" shared pref, conditionally call isDirty. + ListenableFuture<Boolean> isDirtyFuture = + Futures.transformAsync( + forceRebuildFuture, + forceRebuild -> + Preconditions.checkNotNull(forceRebuild) + ? Futures.immediateFuture(true) + : isDirty(appContext), + lightweightExecutorService); + + // After determining isDirty, conditionally call rebuild. + return Futures.transformAsync( + isDirtyFuture, + isDirty -> + Preconditions.checkNotNull(isDirty) + ? rebuild(appContext) + : Futures.immediateFuture(null), + lightweightExecutorService); } - @WorkerThread - private boolean isDirty(Context appContext) { - Assert.isWorkerThread(); - + private ListenableFuture<Boolean> isDirty(Context appContext) { + List<ListenableFuture<Boolean>> isDirtyFutures = new ArrayList<>(); for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) { - String dataSourceName = getName(dataSource); - long startTime = System.currentTimeMillis(); - LogUtil.i("RefreshAnnotatedCallLogWorker.isDirty", "running isDirty for %s", dataSourceName); - boolean isDirty = dataSource.isDirty(appContext); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.isDirty", - "%s.isDirty returned %b in %dms", - dataSourceName, - isDirty, - System.currentTimeMillis() - startTime); - if (isDirty) { - return true; - } + isDirtyFutures.add(dataSource.isDirty(appContext)); } - return false; + // Simultaneously invokes isDirty on all data sources, returning as soon as one returns true. + return DialerFutures.firstMatching(isDirtyFutures, Preconditions::checkNotNull, false); } - @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources - @WorkerThread - private void rebuild(Context appContext) throws RemoteException, OperationApplicationException { - Assert.isWorkerThread(); - + private ListenableFuture<Void> rebuild(Context appContext) { CallLogMutations mutations = new CallLogMutations(); - // System call log data source must go first! + // Start by filling the data sources--the system call log data source must go first! CallLogDataSource systemCallLogDataSource = dataSources.getSystemCallLogDataSource(); - String dataSourceName = getName(systemCallLogDataSource); - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName); - long startTime = System.currentTimeMillis(); - systemCallLogDataSource.fill(appContext, mutations); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.rebuild", - "%s.fill took: %dms", - dataSourceName, - System.currentTimeMillis() - startTime); + ListenableFuture<Void> fillFuture = systemCallLogDataSource.fill(appContext, mutations); + // After the system call log data source is filled, call fill sequentially on each remaining + // data source. This must be done sequentially because mutations are not threadsafe and are + // passed from source to source. for (CallLogDataSource dataSource : dataSources.getDataSourcesExcludingSystemCallLog()) { - dataSourceName = getName(dataSource); - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName); - startTime = System.currentTimeMillis(); - dataSource.fill(appContext, mutations); - LogUtil.i( - "CallLogFramework.rebuild", - "%s.fill took: %dms", - dataSourceName, - System.currentTimeMillis() - startTime); - } - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "applying mutations to database"); - startTime = System.currentTimeMillis(); - CallLogDatabaseComponent.get(appContext) - .mutationApplier() - .applyToDatabase(mutations, appContext); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.rebuild", - "applyToDatabase took: %dms", - System.currentTimeMillis() - startTime); - - for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) { - dataSourceName = getName(dataSource); - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "onSuccessfulFill'ing %s", dataSourceName); - startTime = System.currentTimeMillis(); - dataSource.onSuccessfulFill(appContext); - LogUtil.i( - "CallLogFramework.rebuild", - "%s.onSuccessfulFill took: %dms", - dataSourceName, - System.currentTimeMillis() - startTime); + fillFuture = + Futures.transformAsync( + fillFuture, + unused -> dataSource.fill(appContext, mutations), + lightweightExecutorService); } - sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); - } - private static String getName(CallLogDataSource dataSource) { - return dataSource.getClass().getSimpleName(); + // After all data sources are filled, apply mutations (at this point "fillFuture" is the result + // of filling the last data source). + ListenableFuture<Void> applyMutationsFuture = + Futures.transformAsync( + fillFuture, + unused -> + CallLogDatabaseComponent.get(appContext) + .mutationApplier() + .applyToDatabase(mutations, appContext), + lightweightExecutorService); + + // After mutations applied, call onSuccessfulFill for each data source (in parallel). + ListenableFuture<List<Void>> onSuccessfulFillFuture = + Futures.transformAsync( + applyMutationsFuture, + unused -> { + List<ListenableFuture<Void>> onSuccessfulFillFutures = new ArrayList<>(); + for (CallLogDataSource dataSource : + dataSources.getDataSourcesIncludingSystemCallLog()) { + onSuccessfulFillFutures.add(dataSource.onSuccessfulFill(appContext)); + } + return Futures.allAsList(onSuccessfulFillFutures); + }, + lightweightExecutorService); + + // After onSuccessfulFill is called for every data source, write the shared pref. + return Futures.transform( + onSuccessfulFillFuture, + unused -> { + sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); + return null; + }, + backgroundExecutorService); } } diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java index 825e84f91..2427624a4 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java @@ -245,12 +245,12 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { throw new IllegalArgumentException("Unknown uri: " + uri); } int rows = database.delete(AnnotatedCallLog.TABLE, selection, selectionArgs); - if (rows > 0) { - if (!isApplyingBatch()) { - notifyChange(uri); - } - } else { + if (rows == 0) { LogUtil.w("AnnotatedCallLogContentProvider.delete", "no rows deleted"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); } return rows; } @@ -268,7 +268,15 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { int match = uriMatcher.match(uri); switch (match) { case ANNOTATED_CALL_LOG_TABLE_CODE: - break; + int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); + if (rows == 0) { + LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); + } + return rows; case ANNOTATED_CALL_LOG_TABLE_ID_CODE: Assert.checkArgument( !values.containsKey(AnnotatedCallLog._ID), "Do not specify _ID when updating by ID"); @@ -276,23 +284,21 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { Assert.checkArgument( selectionArgs == null, "Do not specify selection args when updating by ID"); selection = getSelectionWithId(ContentUris.parseId(uri)); - break; + rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); + if (rows == 0) { + LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); + } + return rows; case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: - throw new UnsupportedOperationException(); case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: throw new UnsupportedOperationException(); default: throw new IllegalArgumentException("Unknown uri: " + uri); } - int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); - if (rows > 0) { - if (!isApplyingBatch()) { - notifyChange(uri); - } - } else { - LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); - } - return rows; } /** diff --git a/java/com/android/dialer/calllog/database/MutationApplier.java b/java/com/android/dialer/calllog/database/MutationApplier.java index 21c8a507d..eee810eb8 100644 --- a/java/com/android/dialer/calllog/database/MutationApplier.java +++ b/java/com/android/dialer/calllog/database/MutationApplier.java @@ -28,6 +28,10 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.Arrays; import java.util.Map.Entry; @@ -36,19 +40,30 @@ import javax.inject.Inject; /** Applies {@link CallLogMutations} to the annotated call log. */ public class MutationApplier { + private final ListeningExecutorService backgroundExecutorService; + @Inject - MutationApplier() {} + MutationApplier(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + this.backgroundExecutorService = backgroundExecutorService; + } /** Applies the provided {@link CallLogMutations} to the annotated call log. */ + public ListenableFuture<Void> applyToDatabase(CallLogMutations mutations, Context appContext) { + if (mutations.isEmpty()) { + return Futures.immediateFuture(null); + } + return backgroundExecutorService.submit( + () -> { + applyToDatabaseInternal(mutations, appContext); + return null; + }); + } + @WorkerThread - public void applyToDatabase(CallLogMutations mutations, Context appContext) + private void applyToDatabaseInternal(CallLogMutations mutations, Context appContext) throws RemoteException, OperationApplicationException { Assert.isWorkerThread(); - if (mutations.isEmpty()) { - return; - } - ArrayList<ContentProviderOperation> operations = new ArrayList<>(); if (!mutations.getInserts().isEmpty()) { diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java index 3fff3ba53..60654a81a 100644 --- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java @@ -21,6 +21,7 @@ import android.content.Context; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract; +import com.google.common.util.concurrent.ListenableFuture; import java.util.List; /** @@ -64,8 +65,7 @@ public interface CallLogDataSource { * * @see CallLogDataSource class doc for complete lifecyle information */ - @WorkerThread - boolean isDirty(Context appContext); + ListenableFuture<Boolean> isDirty(Context appContext); /** * Computes the set of mutations necessary to update the annotated call log with respect to this @@ -76,8 +76,7 @@ public interface CallLogDataSource { * contain inserts from the system call log, and these inserts should be modified by each data * source. */ - @WorkerThread - void fill(Context appContext, CallLogMutations mutations); + ListenableFuture<Void> fill(Context appContext, CallLogMutations mutations); /** * Called after database mutations have been applied to all data sources. This is useful for @@ -86,8 +85,7 @@ public interface CallLogDataSource { * * @see CallLogDataSource class doc for complete lifecyle information */ - @WorkerThread - void onSuccessfulFill(Context appContext); + ListenableFuture<Void> onSuccessfulFill(Context appContext); /** * Combines raw annotated call log rows into a single coalesced row. diff --git a/java/com/android/dialer/calllog/datasources/DataSources.java b/java/com/android/dialer/calllog/datasources/DataSources.java index 113a9f7b1..9fe6c1db3 100644 --- a/java/com/android/dialer/calllog/datasources/DataSources.java +++ b/java/com/android/dialer/calllog/datasources/DataSources.java @@ -16,13 +16,12 @@ package com.android.dialer.calllog.datasources; -import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource; import com.google.common.collect.ImmutableList; /** Immutable lists of data sources used to populate the annotated call log. */ public interface DataSources { - SystemCallLogDataSource getSystemCallLogDataSource(); + CallLogDataSource getSystemCallLogDataSource(); ImmutableList<CallLogDataSource> getDataSourcesIncludingSystemCallLog(); diff --git a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java deleted file mode 100644 index f0384b09a..000000000 --- a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2017 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.datasources.contacts; - -import android.content.ContentValues; -import android.content.Context; -import android.support.annotation.MainThread; -import android.support.annotation.WorkerThread; -import com.android.dialer.calllog.datasources.CallLogDataSource; -import com.android.dialer.calllog.datasources.CallLogMutations; -import com.android.dialer.common.Assert; -import java.util.List; -import javax.inject.Inject; - -/** Responsible for maintaining the contacts related columns in the annotated call log. */ -public final class ContactsDataSource implements CallLogDataSource { - - @Inject - public ContactsDataSource() {} - - @WorkerThread - @Override - public boolean isDirty(Context appContext) { - Assert.isWorkerThread(); - - // TODO(zachh): Implementation. - return false; - } - - @WorkerThread - @Override - public void fill( - Context appContext, - CallLogMutations mutations) { - Assert.isWorkerThread(); - // TODO(zachh): Implementation. - } - - @Override - public void onSuccessfulFill(Context appContext) { - // TODO(zachh): Implementation. - } - - @Override - public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) { - // TODO(zachh): Implementation. - return new ContentValues(); - } - - @MainThread - @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { - // TODO(zachh): Guard against missing permissions during callback registration. - } -} diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 010cb8541..fa7d3be16 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -16,9 +16,13 @@ package com.android.dialer.calllog.datasources.phonelookup; +import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; +import android.content.OperationApplicationException; import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import android.text.TextUtils; @@ -29,22 +33,29 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupSelector; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.Callable; import javax.inject.Inject; /** @@ -54,26 +65,42 @@ import javax.inject.Inject; public final class PhoneLookupDataSource implements CallLogDataSource { private final PhoneLookup phoneLookup; + private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; + + /** + * Keyed by normalized number (the primary key for PhoneLookupHistory). + * + * <p>This is state saved between the {@link #fill(Context, CallLogMutations)} and {@link + * #onSuccessfulFill(Context)} operations. + */ + private final Map<String, PhoneLookupInfo> phoneLookupHistoryRowsToUpdate = new ArrayMap<>(); + + /** + * Normalized numbers (the primary key for PhoneLookupHistory) which should be deleted from + * PhoneLookupHistory. + * + * <p>This is state saved between the {@link #fill(Context, CallLogMutations)} and {@link + * #onSuccessfulFill(Context)} operations. + */ + private final Set<String> phoneLookupHistoryRowsToDelete = new ArraySet<>(); @Inject - PhoneLookupDataSource(PhoneLookup phoneLookup) { + PhoneLookupDataSource( + PhoneLookup phoneLookup, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.phoneLookup = phoneLookup; + this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; } - @WorkerThread @Override - public boolean isDirty(Context appContext) { - ImmutableSet<DialerPhoneNumber> uniqueDialerPhoneNumbers = - queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext); - - try { - // TODO(zachh): Would be good to rework call log architecture to properly use futures. - // TODO(zachh): Consider how individual lookups should behave wrt timeouts/exceptions and - // handle appropriately here. - return phoneLookup.isDirty(uniqueDialerPhoneNumbers).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } + public ListenableFuture<Boolean> isDirty(Context appContext) { + ListenableFuture<ImmutableSet<DialerPhoneNumber>> phoneNumbers = + backgroundExecutorService.submit( + () -> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext)); + return Futures.transformAsync(phoneNumbers, phoneLookup::isDirty, lightweightExecutorService); } /** @@ -92,8 +119,9 @@ public final class PhoneLookupDataSource implements CallLogDataSource { * <li>For inserts, uses the contents of PhoneLookupHistory to populate the fields of the * provided mutations. (Note that at this point, data may not be fully up-to-date, but the * next steps will take care of that.) - * <li>Uses all of the numbers from AnnotatedCallLog to invoke CompositePhoneLookup:bulkUpdate - * <li>Looks through the results of bulkUpdate + * <li>Uses all of the numbers from AnnotatedCallLog to invoke (composite) {@link + * PhoneLookup#getMostRecentPhoneLookupInfo(ImmutableMap)} + * <li>Looks through the results of getMostRecentPhoneLookupInfo * <ul> * <li>For each number, checks if the original PhoneLookupInfo differs from the new one * <li>If so, it applies the update to the mutations and (in onSuccessfulFill) writes the @@ -101,48 +129,132 @@ public final class PhoneLookupDataSource implements CallLogDataSource { * </ul> * </ul> */ - @WorkerThread @Override - public void fill(Context appContext, CallLogMutations mutations) { - Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber = - queryIdAndNumberFromAnnotatedCallLog(appContext); - ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> originalPhoneLookupInfosByNumber = - queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()); - ImmutableMap.Builder<Long, PhoneLookupInfo> originalPhoneLookupHistoryDataByAnnotatedCallLogId = - ImmutableMap.builder(); - for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : - originalPhoneLookupInfosByNumber.entrySet()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - PhoneLookupInfo phoneLookupInfo = entry.getValue(); - for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { - originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo); - } - } - populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); + public ListenableFuture<Void> fill(Context appContext, CallLogMutations mutations) { + // Clear state saved since the last call to fill. This is necessary in case fill is called but + // onSuccessfulFill is not called during a previous flow. + phoneLookupHistoryRowsToUpdate.clear(); + phoneLookupHistoryRowsToDelete.clear(); - ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> updatedInfoMap; - try { - updatedInfoMap = phoneLookup.bulkUpdate(originalPhoneLookupInfosByNumber).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - ImmutableMap.Builder<Long, PhoneLookupInfo> rowsToUpdate = ImmutableMap.builder(); - for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : updatedInfoMap.entrySet()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - PhoneLookupInfo upToDateInfo = entry.getValue(); - if (!originalPhoneLookupInfosByNumber.get(dialerPhoneNumber).equals(upToDateInfo)) { - for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { - rowsToUpdate.put(id, upToDateInfo); - } - } - } - updateMutations(rowsToUpdate.build(), mutations); + // First query information from annotated call log. + ListenableFuture<Map<DialerPhoneNumber, Set<Long>>> annotatedCallLogIdsByNumberFuture = + backgroundExecutorService.submit(() -> queryIdAndNumberFromAnnotatedCallLog(appContext)); + + // Use it to create the original info map. + ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> originalInfoMapFuture = + Futures.transform( + annotatedCallLogIdsByNumberFuture, + annotatedCallLogIdsByNumber -> + queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()), + backgroundExecutorService); + + // Use the original info map to generate the updated info map by delegating to phoneLookup. + ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> updatedInfoMapFuture = + Futures.transformAsync( + originalInfoMapFuture, + phoneLookup::getMostRecentPhoneLookupInfo, + lightweightExecutorService); + + // This is the computation that will use the result of all of the above. + Callable<ImmutableMap<Long, PhoneLookupInfo>> computeRowsToUpdate = + () -> { + // These get() calls are safe because we are using whenAllSucceed below. + Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber = + annotatedCallLogIdsByNumberFuture.get(); + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> originalInfoMap = + originalInfoMapFuture.get(); + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> updatedInfoMap = + updatedInfoMapFuture.get(); + + // First populate the insert mutations + ImmutableMap.Builder<Long, PhoneLookupInfo> + originalPhoneLookupHistoryDataByAnnotatedCallLogId = ImmutableMap.builder(); + for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : originalInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfo phoneLookupInfo = entry.getValue(); + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo); + } + } + populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); + + // Compute and save the PhoneLookupHistory rows which can be deleted in onSuccessfulFill. + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + phoneLookupHistoryRowsToDelete.addAll( + computePhoneLookupHistoryRowsToDelete( + annotatedCallLogIdsByNumber, mutations, dialerPhoneNumberUtil)); + + // Now compute the rows to update. + ImmutableMap.Builder<Long, PhoneLookupInfo> rowsToUpdate = ImmutableMap.builder(); + for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : updatedInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfo upToDateInfo = entry.getValue(); + if (!originalInfoMap.get(dialerPhoneNumber).equals(upToDateInfo)) { + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + rowsToUpdate.put(id, upToDateInfo); + } + // Also save the updated information so that it can be written to PhoneLookupHistory + // in onSuccessfulFill. + String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber); + phoneLookupHistoryRowsToUpdate.put(normalizedNumber, upToDateInfo); + } + } + return rowsToUpdate.build(); + }; + + ListenableFuture<ImmutableMap<Long, PhoneLookupInfo>> rowsToUpdateFuture = + Futures.whenAllSucceed( + annotatedCallLogIdsByNumberFuture, updatedInfoMapFuture, originalInfoMapFuture) + .call( + computeRowsToUpdate, + backgroundExecutorService /* PhoneNumberUtil may do disk IO */); + + // Finally update the mutations with the computed rows. + return Futures.transform( + rowsToUpdateFuture, + rowsToUpdate -> { + updateMutations(rowsToUpdate, mutations); + return null; + }, + lightweightExecutorService); } - @WorkerThread @Override - public void onSuccessfulFill(Context appContext) { - // TODO(zachh): Update PhoneLookupHistory. + public ListenableFuture<Void> onSuccessfulFill(Context appContext) { + // First update and/or delete the appropriate rows in PhoneLookupHistory. + ListenableFuture<Void> writePhoneLookupHistory = + backgroundExecutorService.submit(() -> writePhoneLookupHistory(appContext)); + + // If that succeeds, delegate to the composite PhoneLookup to notify all PhoneLookups that both + // the AnnotatedCallLog and PhoneLookupHistory have been successfully updated. + return Futures.transformAsync( + writePhoneLookupHistory, + unused -> phoneLookup.onSuccessfulBulkUpdate(), + lightweightExecutorService); + } + + @WorkerThread + private Void writePhoneLookupHistory(Context appContext) + throws RemoteException, OperationApplicationException { + ArrayList<ContentProviderOperation> operations = new ArrayList<>(); + long currentTimestamp = System.currentTimeMillis(); + for (Entry<String, PhoneLookupInfo> entry : phoneLookupHistoryRowsToUpdate.entrySet()) { + String normalizedNumber = entry.getKey(); + PhoneLookupInfo phoneLookupInfo = entry.getValue(); + ContentValues contentValues = new ContentValues(); + contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, phoneLookupInfo.toByteArray()); + contentValues.put(PhoneLookupHistory.LAST_MODIFIED, currentTimestamp); + operations.add( + ContentProviderOperation.newUpdate(numberUri(normalizedNumber)) + .withValues(contentValues) + .build()); + } + for (String normalizedNumber : phoneLookupHistoryRowsToDelete) { + operations.add(ContentProviderOperation.newDelete(numberUri(normalizedNumber)).build()); + } + appContext.getContentResolver().applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations); + return null; } @WorkerThread @@ -377,7 +489,47 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } } + private Set<String> computePhoneLookupHistoryRowsToDelete( + Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber, + CallLogMutations mutations, + DialerPhoneNumberUtil dialerPhoneNumberUtil) { + if (mutations.getDeletes().isEmpty()) { + return ImmutableSet.of(); + } + // First convert the dialer phone numbers to normalized numbers; we need to combine entries + // because different DialerPhoneNumbers can map to the same normalized number. + Map<String, Set<Long>> idsByNormalizedNumber = new ArrayMap<>(); + for (Entry<DialerPhoneNumber, Set<Long>> entry : annotatedCallLogIdsByNumber.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + Set<Long> idsForDialerPhoneNumber = entry.getValue(); + String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber); + Set<Long> idsForNormalizedNumber = idsByNormalizedNumber.get(normalizedNumber); + if (idsForNormalizedNumber == null) { + idsForNormalizedNumber = new ArraySet<>(); + idsByNormalizedNumber.put(normalizedNumber, idsForNormalizedNumber); + } + idsForNormalizedNumber.addAll(idsForDialerPhoneNumber); + } + // Now look through and remove all IDs that were scheduled for delete; after doing that, if + // there are no remaining IDs left for a normalized number, the number can be deleted from + // PhoneLookupHistory. + Set<String> normalizedNumbersToDelete = new ArraySet<>(); + for (Entry<String, Set<Long>> entry : idsByNormalizedNumber.entrySet()) { + String normalizedNumber = entry.getKey(); + Set<Long> idsForNormalizedNumber = entry.getValue(); + idsForNormalizedNumber.removeAll(mutations.getDeletes()); + if (idsForNormalizedNumber.isEmpty()) { + normalizedNumbersToDelete.add(normalizedNumber); + } + } + return normalizedNumbersToDelete; + } + private static String selectName(PhoneLookupInfo phoneLookupInfo) { return PhoneLookupSelector.selectName(phoneLookupInfo); } + + private static Uri numberUri(String number) { + return PhoneLookupHistory.CONTENT_URI.buildUpon().appendEncodedPath(number).build(); + } } diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index ef40c308e..91db915ef 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -45,11 +45,14 @@ import com.android.dialer.calllog.datasources.util.RowCombiner; import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.storage.StorageComponent; import com.android.dialer.theme.R; import com.android.dialer.util.PermissionsUtil; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.util.Arrays; import java.util.List; @@ -66,10 +69,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { @VisibleForTesting static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; + private final ListeningExecutorService backgroundExecutorService; + @Nullable private Long lastTimestampProcessed; @Inject - public SystemCallLogDataSource() {} + SystemCallLogDataSource(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + this.backgroundExecutorService = backgroundExecutorService; + } @MainThread @Override @@ -94,9 +101,23 @@ public class SystemCallLogDataSource implements CallLogDataSource { ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks)); } - @WorkerThread @Override - public boolean isDirty(Context appContext) { + public ListenableFuture<Boolean> isDirty(Context appContext) { + return backgroundExecutorService.submit(() -> isDirtyInternal(appContext)); + } + + @Override + public ListenableFuture<Void> fill(Context appContext, CallLogMutations mutations) { + return backgroundExecutorService.submit(() -> fillInternal(appContext, mutations)); + } + + @Override + public ListenableFuture<Void> onSuccessfulFill(Context appContext) { + return backgroundExecutorService.submit(() -> onSuccessfulFillInternal(appContext)); + } + + @WorkerThread + private boolean isDirtyInternal(Context appContext) { Assert.isWorkerThread(); /* @@ -113,15 +134,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { } @WorkerThread - @Override - public void fill(Context appContext, CallLogMutations mutations) { + private Void fillInternal(Context appContext, CallLogMutations mutations) { Assert.isWorkerThread(); lastTimestampProcessed = null; if (!PermissionsUtil.hasPermission(appContext, permission.READ_CALL_LOG)) { LogUtil.i("SystemCallLogDataSource.fill", "no call log permissions"); - return; + return null; } // This data source should always run first so the mutations should always be empty. @@ -136,11 +156,11 @@ public class SystemCallLogDataSource implements CallLogDataSource { handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds); handleDeletes(appContext, annotatedCallLogIds, mutations); + return null; } @WorkerThread - @Override - public void onSuccessfulFill(Context appContext) { + private Void onSuccessfulFillInternal(Context appContext) { // If a fill operation was a no-op, lastTimestampProcessed could still be null. if (lastTimestampProcessed != null) { StorageComponent.get(appContext) @@ -149,6 +169,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed) .apply(); } + return null; } @Override diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index 6833452c6..a5dccaf69 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -17,6 +17,7 @@ package com.android.dialer.calllog.ui; import android.database.Cursor; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; @@ -31,16 +32,26 @@ import com.android.dialer.calllog.CallLogFramework.CallLogUi; import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.common.concurrent.UiListener; -import com.google.common.util.concurrent.ListenableScheduledFuture; +import com.google.common.util.concurrent.ListenableFuture; /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ public final class NewCallLogFragment extends Fragment implements CallLogUi, LoaderCallbacks<Cursor> { + /* + * This is a reasonable time that it might take between related call log writes, that also + * shouldn't slow down single-writes too much. For example, when populating the database using + * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 + * call log entries. + */ + private static final long WAIT_MILLIS = 100L; + private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; private UiListener<Void> refreshAnnotatedCallLogListener; private RecyclerView recyclerView; + @Nullable private Runnable refreshAnnotatedCallLogRunnable; public NewCallLogFragment() { LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment"); @@ -81,7 +92,7 @@ public final class NewCallLogFragment extends Fragment callLogFramework.attachUi(this); // TODO(zachh): Consider doing this when fragment becomes visible. - checkAnnotatedCallLogDirtyAndRefreshIfNecessary(); + refreshAnnotatedCallLog(true /* checkDirty */); } @Override @@ -90,6 +101,9 @@ public final class NewCallLogFragment extends Fragment LogUtil.enterBlock("NewCallLogFragment.onPause"); + // This is pending work that we don't actually need to follow through with. + ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); callLogFramework.detachUi(); } @@ -107,18 +121,35 @@ public final class NewCallLogFragment extends Fragment return view; } - private void checkAnnotatedCallLogDirtyAndRefreshIfNecessary() { - LogUtil.enterBlock("NewCallLogFragment.checkAnnotatedCallLogDirtyAndRefreshIfNecessary"); - ListenableScheduledFuture<Void> future = refreshAnnotatedCallLogWorker.refreshWithDirtyCheck(); - refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + private void refreshAnnotatedCallLog(boolean checkDirty) { + LogUtil.enterBlock("NewCallLogFragment.refreshAnnotatedCallLog"); + + // If we already scheduled a refresh, cancel it and schedule a new one so that repeated requests + // in quick succession don't result in too much work. For example, if we get 10 requests in + // 10ms, and a complete refresh takes a constant 200ms, the refresh will take 300ms (100ms wait + // and 1 iteration @200ms) instead of 2 seconds (10 iterations @ 200ms) since the work requests + // are serialized in RefreshAnnotatedCallLogWorker. + // + // We might get many requests in quick succession, for example, when the simulator inserts + // hundreds of rows into the system call log, or when the data for a new call is incrementally + // written to different columns as it becomes available. + ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + + refreshAnnotatedCallLogRunnable = + () -> { + ListenableFuture<Void> future = + checkDirty + ? refreshAnnotatedCallLogWorker.refreshWithDirtyCheck() + : refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); + refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + }; + ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS); } @Override public void invalidateUi() { LogUtil.enterBlock("NewCallLogFragment.invalidateUi"); - ListenableScheduledFuture<Void> future = - refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); - refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + refreshAnnotatedCallLog(false /* checkDirty */); } @Override diff --git a/java/com/android/dialer/common/concurrent/Annotations.java b/java/com/android/dialer/common/concurrent/Annotations.java index 5e3954cf9..62d5b318e 100644 --- a/java/com/android/dialer/common/concurrent/Annotations.java +++ b/java/com/android/dialer/common/concurrent/Annotations.java @@ -39,4 +39,12 @@ public class Annotations { /** Annotation for retrieving the UI serial executor. */ @Qualifier public @interface UiSerial {} + + /** Annotation for retrieving the lightweight executor. */ + @Qualifier + public @interface LightweightExecutor {} + + /** Annotation for retrieving the background executor. */ + @Qualifier + public @interface BackgroundExecutor {} } diff --git a/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java b/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java index ab01654aa..317807b1d 100644 --- a/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java +++ b/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java @@ -40,14 +40,14 @@ import javax.inject.Inject; public class DefaultDialerExecutorFactory implements DialerExecutorFactory { private final ExecutorService nonUiThreadPool; private final ScheduledExecutorService nonUiSerialExecutor; - private final Executor uiThreadPool; + private final ExecutorService uiThreadPool; private final ScheduledExecutorService uiSerialExecutor; @Inject DefaultDialerExecutorFactory( @NonUiParallel ExecutorService nonUiThreadPool, @NonUiSerial ScheduledExecutorService nonUiSerialExecutor, - @UiParallel Executor uiThreadPool, + @UiParallel ExecutorService uiThreadPool, @UiSerial ScheduledExecutorService uiSerialExecutor) { this.nonUiThreadPool = nonUiThreadPool; this.nonUiSerialExecutor = nonUiSerialExecutor; diff --git a/java/com/android/dialer/common/concurrent/DialerExecutorModule.java b/java/com/android/dialer/common/concurrent/DialerExecutorModule.java index 5e0190e8d..98738ed37 100644 --- a/java/com/android/dialer/common/concurrent/DialerExecutorModule.java +++ b/java/com/android/dialer/common/concurrent/DialerExecutorModule.java @@ -17,16 +17,18 @@ package com.android.dialer.common.concurrent; import android.os.AsyncTask; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.Annotations.NonUiParallel; import com.android.dialer.common.concurrent.Annotations.NonUiSerial; import com.android.dialer.common.concurrent.Annotations.Ui; import com.android.dialer.common.concurrent.Annotations.UiParallel; import com.android.dialer.common.concurrent.Annotations.UiSerial; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import dagger.Binds; import dagger.Module; import dagger.Provides; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -85,8 +87,8 @@ public abstract class DialerExecutorModule { @Provides @UiParallel - static Executor provideUiThreadPool() { - return AsyncTask.THREAD_POOL_EXECUTOR; + static ExecutorService provideUiThreadPool() { + return (ExecutorService) AsyncTask.THREAD_POOL_EXECUTOR; } @Provides @@ -105,4 +107,19 @@ public abstract class DialerExecutorModule { } }); } + + @Provides + @Singleton + @LightweightExecutor + static ListeningExecutorService provideLightweightExecutor(@UiParallel ExecutorService delegate) { + return MoreExecutors.listeningDecorator(delegate); + } + + @Provides + @Singleton + @BackgroundExecutor + static ListeningExecutorService provideBackgroundExecutor( + @NonUiParallel ExecutorService delegate) { + return MoreExecutors.listeningDecorator(delegate); + } } diff --git a/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java new file mode 100644 index 000000000..de37a2915 --- /dev/null +++ b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 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.common.concurrent; + +import com.google.common.util.concurrent.AsyncCallable; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.Executor; + +/** + * Serializes execution of a set of operations. This class guarantees that a submitted callable will + * not be called before previously submitted callables have completed. + */ +public final class DialerFutureSerializer { + + /** Enqueues a task to run when the previous task (if any) completes. */ + public <T> ListenableFuture<T> submitAsync(final AsyncCallable<T> callable, Executor executor) { + // TODO(zachh): This is just a dummy implementation until we fix guava API level issues. + try { + return callable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java index cadce4d48..6bed818da 100644 --- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java +++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java @@ -77,6 +77,12 @@ public class TelephonyManagerCompat { BuildCompat.isAtLeastOMR1() ? "android.telephony.extra.IS_REFRESH" : "is_refresh"; /** + * Indicates the call underwent Assisted Dialing; typically set as a feature available from the + * CallLog. + */ + public static final Integer FEATURES_ASSISTED_DIALING = 0x10; + + /** * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality) * diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java index 839c1d3a8..ff694c053 100644 --- a/java/com/android/dialer/duo/Duo.java +++ b/java/com/android/dialer/duo/Duo.java @@ -31,6 +31,12 @@ public interface Duo { boolean isEnabled(); + /** + * @return true if Duo is installed and the user has gone through the set-up flow confirming their + * phone number. + */ + boolean isActivated(@NonNull Context context); + @MainThread boolean isReachable(@NonNull Context context, @Nullable String number); diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java index 82b9c79e3..7cc8f7808 100644 --- a/java/com/android/dialer/duo/stub/DuoStub.java +++ b/java/com/android/dialer/duo/stub/DuoStub.java @@ -40,6 +40,11 @@ public class DuoStub implements Duo { return false; } + @Override + public boolean isActivated(@NonNull Context context) { + return false; + } + @MainThread @Override public boolean isReachable(@NonNull Context context, @Nullable String number) { diff --git a/java/com/android/dialer/phonelookup/PhoneLookup.java b/java/com/android/dialer/phonelookup/PhoneLookup.java index 183277569..eeab4dadd 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/PhoneLookup.java @@ -27,8 +27,8 @@ import com.google.common.util.concurrent.ListenableFuture; * Provides operations related to retrieving information about phone numbers. * * <p>Some operations defined by this interface are generally targeted towards specific use cases; - * for example {@link #isDirty(ImmutableSet)}, {@link #bulkUpdate(ImmutableMap)}, and {@link - * #onSuccessfulBulkUpdate()} are generally intended to be used by the call log. + * for example {@link #isDirty(ImmutableSet)}, {@link #getMostRecentPhoneLookupInfo(ImmutableMap)}, + * and {@link #onSuccessfulBulkUpdate()} are generally intended to be used by the call log. */ public interface PhoneLookup { @@ -48,9 +48,9 @@ public interface PhoneLookup { ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers); /** - * Performs a bulk update of this {@link PhoneLookup}. The returned map must contain the exact - * same keys as the provided map. Most implementations will rely on last modified timestamps to - * efficiently only update the data which needs to be updated. + * Get the most recent phone lookup information for this {@link PhoneLookup}. The returned map + * must contain the exact same keys as the provided map. Most implementations will rely on last + * modified timestamps to efficiently only update the data which needs to be updated. * * <p>If there are no changes required, it is valid for this method to simply return the provided * {@code existingInfoMap}. @@ -58,16 +58,16 @@ public interface PhoneLookup { * <p>If there is no longer information associated with a number (for example, a local contact was * deleted) the returned map should contain an empty {@link PhoneLookupInfo} for that number. */ - ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( + ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> getMostRecentPhoneLookupInfo( ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap); /** - * Called when the results of the {@link #bulkUpdate(ImmutableMap)} have been applied by the - * caller. + * Called when the results of the {@link #getMostRecentPhoneLookupInfo(ImmutableMap)} have been + * applied by the caller. * * <p>Typically implementations will use this to store a "last processed" timestamp so that future - * invocations of {@link #isDirty(ImmutableSet)} and {@link #bulkUpdate(ImmutableMap)} can be - * efficiently implemented. + * invocations of {@link #isDirty(ImmutableSet)} and {@link + * #getMostRecentPhoneLookupInfo(ImmutableMap)} can be efficiently implemented. */ ListenableFuture<Void> onSuccessfulBulkUpdate(); } diff --git a/java/com/android/dialer/phonelookup/PhoneLookupModule.java b/java/com/android/dialer/phonelookup/PhoneLookupModule.java index 400caff3a..39b0a5083 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookupModule.java +++ b/java/com/android/dialer/phonelookup/PhoneLookupModule.java @@ -27,7 +27,12 @@ import dagger.Provides; public abstract class PhoneLookupModule { @Provides - static PhoneLookup providePhoneLookup(Cp2PhoneLookup cp2PhoneLookup) { - return new CompositePhoneLookup(ImmutableList.of(cp2PhoneLookup)); + static ImmutableList<PhoneLookup> providePhoneLookupList(Cp2PhoneLookup cp2PhoneLookup) { + return ImmutableList.of(cp2PhoneLookup); + } + + @Provides + static PhoneLookup providePhoneLookup(CompositePhoneLookup compositePhoneLookup) { + return compositePhoneLookup; } } diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index f432e27ae..ee2244615 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -20,6 +20,7 @@ import android.support.annotation.NonNull; import android.telecom.Call; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; @@ -29,9 +30,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; /** * {@link PhoneLookup} which delegates to a configured set of {@link PhoneLookup PhoneLookups}, @@ -40,9 +42,14 @@ import java.util.List; public final class CompositePhoneLookup implements PhoneLookup { private final ImmutableList<PhoneLookup> phoneLookups; + private final ListeningExecutorService lightweightExecutorService; - public CompositePhoneLookup(ImmutableList<PhoneLookup> phoneLookups) { + @Inject + CompositePhoneLookup( + ImmutableList<PhoneLookup> phoneLookups, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.phoneLookups = phoneLookups; + this.lightweightExecutorService = lightweightExecutorService; } /** @@ -68,7 +75,7 @@ public final class CompositePhoneLookup implements PhoneLookup { } return mergedInfo.build(); }, - MoreExecutors.directExecutor()); + lightweightExecutorService); } @Override @@ -90,12 +97,13 @@ public final class CompositePhoneLookup implements PhoneLookup { * the dependent lookups does not complete, the returned future will also not complete. */ @Override - public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( - ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap) { + public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> + getMostRecentPhoneLookupInfo( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap) { List<ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { - futures.add(phoneLookup.bulkUpdate(existingInfoMap)); + futures.add(phoneLookup.getMostRecentPhoneLookupInfo(existingInfoMap)); } return Futures.transform( Futures.allAsList(futures), @@ -117,7 +125,7 @@ public final class CompositePhoneLookup implements PhoneLookup { } return combinedMap.build(); }, - MoreExecutors.directExecutor()); + lightweightExecutorService); } @Override @@ -127,6 +135,6 @@ public final class CompositePhoneLookup implements PhoneLookup { futures.add(phoneLookup.onSuccessfulBulkUpdate()); } return Futures.transform( - Futures.allAsList(futures), unused -> null, MoreExecutors.directExecutor()); + Futures.allAsList(futures), unused -> null, lightweightExecutorService); } } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index cfb8fb7b8..03e05b563 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -30,6 +30,7 @@ import android.telecom.Call; import android.text.TextUtils; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.common.Assert; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; @@ -39,7 +40,7 @@ import com.android.dialer.storage.Unencrypted; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -71,25 +72,29 @@ public final class Cp2PhoneLookup implements PhoneLookup { private final Context appContext; private final SharedPreferences sharedPreferences; + private final ListeningExecutorService backgroundExecutorService; + @Nullable private Long currentLastTimestampProcessed; @Inject Cp2PhoneLookup( - @ApplicationContext Context appContext, @Unencrypted SharedPreferences sharedPreferences) { + @ApplicationContext Context appContext, + @Unencrypted SharedPreferences sharedPreferences, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService) { this.appContext = appContext; this.sharedPreferences = sharedPreferences; + this.backgroundExecutorService = backgroundExecutorService; } @Override public ListenableFuture<PhoneLookupInfo> lookup(@NonNull Call call) { // TODO(zachh): Implementation. - return MoreExecutors.newDirectExecutorService().submit(PhoneLookupInfo::getDefaultInstance); + return backgroundExecutorService.submit(PhoneLookupInfo::getDefaultInstance); } @Override public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { - // TODO(calderwoodra): consider a different thread pool - return MoreExecutors.newDirectExecutorService().submit(() -> isDirtyInternal(phoneNumbers)); + return backgroundExecutorService.submit(() -> isDirtyInternal(phoneNumbers)); } private boolean isDirtyInternal(ImmutableSet<DialerPhoneNumber> phoneNumbers) { @@ -183,10 +188,10 @@ public final class Cp2PhoneLookup implements PhoneLookup { } @Override - public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( - ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap) { - return MoreExecutors.newDirectExecutorService() - .submit(() -> bulkUpdateInternal(existingInfoMap)); + public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> + getMostRecentPhoneLookupInfo( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap) { + return backgroundExecutorService.submit(() -> bulkUpdateInternal(existingInfoMap)); } private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> bulkUpdateInternal( @@ -234,17 +239,16 @@ public final class Cp2PhoneLookup implements PhoneLookup { @Override public ListenableFuture<Void> onSuccessfulBulkUpdate() { - return MoreExecutors.newDirectExecutorService() - .submit( - () -> { - if (currentLastTimestampProcessed != null) { - sharedPreferences - .edit() - .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) - .apply(); - } - return null; - }); + return backgroundExecutorService.submit( + () -> { + if (currentLastTimestampProcessed != null) { + sharedPreferences + .edit() + .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) + .apply(); + } + return null; + }); } /** diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java index e85654e99..5c0c00f81 100644 --- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java +++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java @@ -17,7 +17,10 @@ package com.android.dialer.phonelookup.database; import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; import android.content.ContentValues; +import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; @@ -31,6 +34,7 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import java.util.ArrayList; /** * {@link ContentProvider} for the PhoneLookupHistory. @@ -50,13 +54,6 @@ import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContra */ public class PhoneLookupHistoryContentProvider extends ContentProvider { - /** - * We sometimes run queries where we potentially pass numbers into a where clause using the - * (?,?,?,...) syntax. The maximum number of host parameters is 999, so that's the maximum size - * this table can be. See https://www.sqlite.org/limits.html for more details. - */ - private static final int MAX_ROWS = 999; - // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory private static final int PHONE_LOOKUP_HISTORY_TABLE_CODE = 1; // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123 @@ -77,9 +74,16 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { private PhoneLookupHistoryDatabaseHelper databaseHelper; + private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>(); + + /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */ + private boolean isApplyingBatch() { + return applyingBatch.get() != null && applyingBatch.get(); + } + @Override public boolean onCreate() { - databaseHelper = new PhoneLookupHistoryDatabaseHelper(getContext(), MAX_ROWS); + databaseHelper = new PhoneLookupHistoryDatabaseHelper(getContext()); return true; } @@ -168,7 +172,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { .buildUpon() .appendEncodedPath(values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER)) .build(); - notifyChange(insertedUri); + if (!isApplyingBatch()) { + notifyChange(insertedUri); + } return insertedUri; } @@ -197,7 +203,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { LogUtil.w("PhoneLookupHistoryContentProvider.delete", "no rows deleted"); return rows; } - notifyChange(uri); + if (!isApplyingBatch()) { + notifyChange(uri); + } return rows; } @@ -206,6 +214,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update * operation will actually be a "replace" operation, inserting a new row if one does not already * exist. + * + * <p>All columns in an existing row will be replaced which means you must specify all required + * columns in {@code values} when using this method. */ @Override public int update( @@ -225,7 +236,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); return rows; } - notifyChange(uri); + if (!isApplyingBatch()) { + notifyChange(uri); + } return rows; case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: Assert.checkArgument( @@ -237,14 +250,65 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { String normalizedNumber = uri.getLastPathSegment(); values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber); - database.replace(PhoneLookupHistory.TABLE, null, values); - notifyChange(uri); + long result = database.replace(PhoneLookupHistory.TABLE, null, values); + Assert.checkArgument(result != -1, "replacing PhoneLookupHistory row failed"); + if (!isApplyingBatch()) { + notifyChange(uri); + } return 1; default: throw new IllegalArgumentException("Unknown uri: " + uri); } } + /** + * {@inheritDoc} + * + * <p>Note: When applyBatch is used with the PhoneLookupHistory, only a single notification for + * the content URI is generated, not individual notifications for each affected URI. + */ + @NonNull + @Override + public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + ContentProviderResult[] results = new ContentProviderResult[operations.size()]; + if (operations.isEmpty()) { + return results; + } + + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + try { + applyingBatch.set(true); + database.beginTransaction(); + for (int i = 0; i < operations.size(); i++) { + ContentProviderOperation operation = operations.get(i); + int match = uriMatcher.match(operation.getUri()); + switch (match) { + case PHONE_LOOKUP_HISTORY_TABLE_CODE: + case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: + ContentProviderResult result = operation.apply(this, results, i); + if (operation.isInsert()) { + if (result.uri == null) { + throw new OperationApplicationException("error inserting row"); + } + } else if (result.count == 0) { + throw new OperationApplicationException("error applying operation"); + } + results[i] = result; + break; + default: + throw new IllegalArgumentException("Unknown uri: " + operation.getUri()); + } + } + database.setTransactionSuccessful(); + } finally { + applyingBatch.set(false); + database.endTransaction(); + } + notifyChange(PhoneLookupHistory.CONTENT_URI); + return results; + } + private void notifyChange(Uri uri) { getContext().getContentResolver().notifyChange(uri, null); } diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java index 70d88cee4..43b6f102c 100644 --- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java +++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java @@ -22,17 +22,15 @@ import android.database.sqlite.SQLiteOpenHelper; import android.os.SystemClock; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; -import java.util.Locale; /** {@link SQLiteOpenHelper} for the PhoneLookupHistory database. */ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { - private final int maxRows; - PhoneLookupHistoryDatabaseHelper(Context appContext, int maxRows) { + PhoneLookupHistoryDatabaseHelper(Context appContext) { super(appContext, "phone_lookup_history.db", null, 1); - this.maxRows = maxRows; } + // TODO(zachh): LAST_MODIFIED is no longer read and can be deleted. private static final String CREATE_TABLE_SQL = "create table if not exists " + PhoneLookupHistory.TABLE @@ -42,28 +40,6 @@ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { + (PhoneLookupHistory.LAST_MODIFIED + " long not null") + ");"; - /** Deletes all but the first maxRows rows (by timestamp) to keep the table a manageable size. */ - private static final String CREATE_TRIGGER_SQL = - "create trigger delete_old_rows after insert on " - + PhoneLookupHistory.TABLE - + " when (select count(*) from " - + PhoneLookupHistory.TABLE - + ") > %d" - + " begin delete from " - + PhoneLookupHistory.TABLE - + " where " - + PhoneLookupHistory.NORMALIZED_NUMBER - + " in (select " - + PhoneLookupHistory.NORMALIZED_NUMBER - + " from " - + PhoneLookupHistory.TABLE - + " order by " - + PhoneLookupHistory.LAST_MODIFIED - + " limit (select count(*)-%d" - + " from " - + PhoneLookupHistory.TABLE - + " )); end;"; - private static final String CREATE_INDEX_ON_LAST_MODIFIED_SQL = "create index last_modified_index on " + PhoneLookupHistory.TABLE @@ -76,7 +52,6 @@ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { LogUtil.enterBlock("PhoneLookupHistoryDatabaseHelper.onCreate"); long startTime = SystemClock.uptimeMillis(); db.execSQL(CREATE_TABLE_SQL); - db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows)); db.execSQL(CREATE_INDEX_ON_LAST_MODIFIED_SQL); // TODO(zachh): Consider logging impression. LogUtil.i( |