summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2017-12-08 03:47:38 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-12-08 03:47:38 +0000
commitfca74ac6e2540455ce280cd83a026f2825322245 (patch)
tree8cc90871e7cef664d498d359b57a95ff7a1b6a79
parent6f7d6f1bc3f90d16f5b90c8424a5783f07bef1ec (diff)
parent81a7b490ebbf2d9a4213a79b52e7b999aa076b7f (diff)
Merge changes I233163fe,I1dc2c8dd,I9769a7fb,Iad3b85b1,Id9088b12, ...
* changes: Implemented PhoneLookupDataSource#onSuccesfulFill. Fixed compile error in AOSP due to use of guava 23 API. Add isActivated check to Duo interface Bug: 68953167 Made PhoneLookupDataSource implementation async. Renamed PhoneLookup#bulkUpdate to #getMostRecentPhoneLookupInfo. Added bindings for ListeningExecutorServices. Use explicit version constant for AD ceiling. Add Assisted Dialing Call Details Implementation. Switched CallLogDataSource interface to be Future based.
-rw-r--r--java/com/android/bubble/Bubble.java144
-rw-r--r--java/com/android/bubble/res/layout/bubble_base.xml67
-rw-r--r--java/com/android/dialer/assisteddialing/ConcreteCreator.java3
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsActivity.java59
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsAdapter.java12
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java93
-rw-r--r--java/com/android/dialer/calldetails/res/layout/contact_container.xml61
-rw-r--r--java/com/android/dialer/calldetails/res/values/dimens.xml12
-rw-r--r--java/com/android/dialer/calldetails/res/values/strings.xml6
-rw-r--r--java/com/android/dialer/calllog/CallLogModule.java4
-rw-r--r--java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java265
-rw-r--r--java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java40
-rw-r--r--java/com/android/dialer/calllog/database/MutationApplier.java27
-rw-r--r--java/com/android/dialer/calllog/datasources/CallLogDataSource.java10
-rw-r--r--java/com/android/dialer/calllog/datasources/DataSources.java3
-rw-r--r--java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java70
-rw-r--r--java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java260
-rw-r--r--java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java37
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogFragment.java49
-rw-r--r--java/com/android/dialer/common/concurrent/Annotations.java8
-rw-r--r--java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java4
-rw-r--r--java/com/android/dialer/common/concurrent/DialerExecutorModule.java23
-rw-r--r--java/com/android/dialer/common/concurrent/DialerFutureSerializer.java39
-rw-r--r--java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java6
-rw-r--r--java/com/android/dialer/duo/Duo.java6
-rw-r--r--java/com/android/dialer/duo/stub/DuoStub.java5
-rw-r--r--java/com/android/dialer/phonelookup/PhoneLookup.java20
-rw-r--r--java/com/android/dialer/phonelookup/PhoneLookupModule.java9
-rw-r--r--java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java24
-rw-r--r--java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java44
-rw-r--r--java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java90
-rw-r--r--java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java29
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(