diff options
Diffstat (limited to 'java')
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( |