summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/calllog/ui
diff options
context:
space:
mode:
authorzachh <zachh@google.com>2018-01-09 17:29:06 -0800
committerCopybara-Service <copybara-piper@google.com>2018-01-09 18:27:41 -0800
commitff88f7bbe07fb3e267af49427adf1a91e3fc21e9 (patch)
tree34211b16307c159d1726bddc0ec2e2468f66981d /java/com/android/dialer/calllog/ui
parent9f8d0676caa3a00849fa18def2996399ee1bc708 (diff)
Added RealtimeRowProcessor.
This is for performing work inside of the call log's RecyclerView, when the view holder is bound. Most of the time, this should be a no-op but there are possible edge cases where the call log data cannot be updated efficiently through the standard batch mechanism. One example of this is when there are too many invalid numbers in the call log; the CP2 information for invalid numbers cannot be efficiently batch updated so we fetch this information at display time. (Note that we do handle up to 5 invalid numbers in the batch update mechanism, but if there are more than that we fallback to this realtime processing.) Test: unit, manual PiperOrigin-RevId: 181400016 Change-Id: Iea6b380742e757b48d19f319fe46dc5fae837604
Diffstat (limited to 'java/com/android/dialer/calllog/ui')
-rw-r--r--java/com/android/dialer/calllog/ui/CallLogUiComponent.java37
-rw-r--r--java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java4
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogAdapter.java8
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogFragment.java3
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java55
-rw-r--r--java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java107
6 files changed, 209 insertions, 5 deletions
diff --git a/java/com/android/dialer/calllog/ui/CallLogUiComponent.java b/java/com/android/dialer/calllog/ui/CallLogUiComponent.java
new file mode 100644
index 000000000..a8e3b225b
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/CallLogUiComponent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.calllog.ui;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+
+/** Dagger component for the call log UI package. */
+@Subcomponent
+public abstract class CallLogUiComponent {
+
+ public abstract RealtimeRowProcessor realtimeRowProcessor();
+
+ public static CallLogUiComponent get(Context context) {
+ return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component())
+ .callLogUiComponent();
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ CallLogUiComponent callLogUiComponent();
+ }
+}
diff --git a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
index 6d60bdda4..5c0ce2816 100644
--- a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
+++ b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
@@ -50,7 +50,8 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader {
private static final int IS_VOICEMAIL = 18;
private static final int CALL_TYPE = 19;
private static final int CAN_REPORT_AS_INVALID_NUMBER = 20;
- private static final int COALESCED_IDS = 21;
+ private static final int CP2_INFO_INCOMPLETE = 21;
+ private static final int COALESCED_IDS = 22;
CoalescedAnnotatedCallLogCursorLoader(Context context) {
// CoalescedAnnotatedCallLog requires that PROJECTION be ALL_COLUMNS and the following params be
@@ -102,6 +103,7 @@ final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader {
.setIsVoicemail(cursor.getInt(IS_VOICEMAIL) == 1)
.setCallType(cursor.getInt(CALL_TYPE))
.setCanReportAsInvalidNumber(cursor.getInt(CAN_REPORT_AS_INVALID_NUMBER) == 1)
+ .setCp2InfoIncomplete(cursor.getInt(CP2_INFO_INCOMPLETE) == 1)
.setCoalescedIds(coalescedIds)
.build();
}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index d5cfb7e24..6dd742be5 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -15,6 +15,7 @@
*/
package com.android.dialer.calllog.ui;
+import android.content.Context;
import android.database.Cursor;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
@@ -45,15 +46,17 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> {
private final Cursor cursor;
private final Clock clock;
+ private final RealtimeRowProcessor realtimeRowProcessor;
/** Null when the "Today" header should not be displayed. */
@Nullable private final Integer todayHeaderPosition;
/** Null when the "Older" header should not be displayed. */
@Nullable private final Integer olderHeaderPosition;
- NewCallLogAdapter(Cursor cursor, Clock clock) {
+ NewCallLogAdapter(Context context, Cursor cursor, Clock clock) {
this.cursor = cursor;
this.clock = clock;
+ this.realtimeRowProcessor = CallLogUiComponent.get(context).realtimeRowProcessor();
// Calculate header adapter positions by reading cursor.
long currentTimeMillis = clock.currentTimeMillis();
@@ -95,7 +98,8 @@ final class NewCallLogAdapter extends RecyclerView.Adapter<ViewHolder> {
return new NewCallLogViewHolder(
LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.new_call_log_entry, viewGroup, false),
- clock);
+ clock,
+ realtimeRowProcessor);
default:
throw Assert.createUnsupportedOperationFailException("Unsupported view type: " + viewType);
}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index 719878cec..e422b5f83 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -171,7 +171,8 @@ public final class NewCallLogFragment extends Fragment
}
// TODO(zachh): Handle empty cursor by showing empty view.
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- recyclerView.setAdapter(new NewCallLogAdapter(newCursor, System::currentTimeMillis));
+ recyclerView.setAdapter(
+ new NewCallLogAdapter(getContext(), newCursor, System::currentTimeMillis));
}
@Override
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index e45257f7b..5cceac989 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -30,12 +30,18 @@ import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
import com.android.dialer.calllogutils.CallLogEntryText;
import com.android.dialer.calllogutils.CallLogIntents;
import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
import com.android.dialer.contactphoto.ContactPhotoManager;
import com.android.dialer.lettertile.LetterTileDrawable;
import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.time.Clock;
+import com.google.common.base.Optional;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
import java.util.Locale;
+import java.util.concurrent.ExecutorService;
/** {@link RecyclerView.ViewHolder} for the new call log. */
final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
@@ -50,8 +56,12 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
private final ImageView menuButton;
private final Clock clock;
+ private final RealtimeRowProcessor realtimeRowProcessor;
+ private final ExecutorService uiExecutorService;
- NewCallLogViewHolder(View view, Clock clock) {
+ private int currentRowId;
+
+ NewCallLogViewHolder(View view, Clock clock, RealtimeRowProcessor realtimeRowProcessor) {
super(view);
this.context = view.getContext();
primaryTextView = view.findViewById(R.id.primary_text);
@@ -63,12 +73,29 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
menuButton = view.findViewById(R.id.menu_button);
this.clock = clock;
+ this.realtimeRowProcessor = realtimeRowProcessor;
+ uiExecutorService = DialerExecutorComponent.get(context).uiExecutorService();
}
/** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */
void bind(Cursor cursor) {
CoalescedRow row = CoalescedAnnotatedCallLogCursorLoader.toRow(cursor);
+ currentRowId = row.id(); // Used to make sure async updates are applied to the correct views
+
+ // Even if there is additional real time processing necessary, we still want to immediately show
+ // what information we have, rather than an empty card. For example, if CP2 information needs to
+ // be queried on the fly, we can still show the phone number until the contact name loads.
+ handleRow(row);
+
+ // Note: This leaks the view holder via the callback (which is an inner class), but this is OK
+ // because we only create ~10 of them (and they'll be collected assuming all jobs finish).
+ Futures.addCallback(
+ realtimeRowProcessor.applyRealtimeProcessing(row),
+ new RealtimeRowFutureCallback(row.id()),
+ uiExecutorService);
+ }
+ private void handleRow(CoalescedRow row) {
// TODO(zachh): Handle RTL properly.
primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row));
secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row));
@@ -152,4 +179,30 @@ final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
private void setOnClickListenerForMenuButon(CoalescedRow row) {
menuButton.setOnClickListener(NewCallLogMenu.createOnClickListener(context, row));
}
+
+ private class RealtimeRowFutureCallback implements FutureCallback<Optional<CoalescedRow>> {
+ private final int id;
+
+ RealtimeRowFutureCallback(int id) {
+ this.id = id;
+ }
+
+ /**
+ * @param updatedRow the updated row if an update is required, or absent if no updates are
+ * required
+ */
+ @Override
+ public void onSuccess(Optional<CoalescedRow> updatedRow) {
+ // If the user scrolled then this ViewHolder may not correspond to the completed task and
+ // there's nothing to do.
+ if (updatedRow.isPresent() && id == currentRowId) {
+ handleRow(updatedRow.get());
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable throwable) {
+ LogUtil.e("RealtimeRowFutureCallback.onFailure", "realtime processing failed", throwable);
+ }
+ }
}
diff --git a/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java
new file mode 100644
index 000000000..814efc779
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/RealtimeRowProcessor.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllog.ui;
+
+import android.support.annotation.MainThread;
+import android.util.ArrayMap;
+import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.common.concurrent.Annotations.Ui;
+import com.android.dialer.phonelookup.PhoneLookupInfo;
+import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
+import com.android.dialer.phonelookup.cp2.Cp2PhoneLookup;
+import com.android.dialer.phonelookup.selector.PhoneLookupSelector;
+import com.google.common.base.Optional;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import java.util.Map;
+import javax.inject.Inject;
+
+/**
+ * Does work necessary to update a {@link CoalescedRow} when it is requested to be displayed.
+ *
+ * <p>In most cases this is a no-op as most AnnotatedCallLog rows can be displayed immediately
+ * as-is. However, there are certain times that a row from the AnnotatedCallLog cannot be displayed
+ * without further work being performed.
+ *
+ * <p>For example, when there are many invalid numbers in the call log, we cannot efficiently update
+ * the CP2 information for all of them at once, and so information for those rows must be retrieved
+ * at display time.
+ */
+public final class RealtimeRowProcessor {
+
+ private final ListeningExecutorService uiExecutor;
+ private final Cp2PhoneLookup cp2PhoneLookup;
+ private final PhoneLookupSelector phoneLookupSelector;
+
+ private final Map<DialerPhoneNumber, Cp2Info> cache = new ArrayMap<>();
+
+ @Inject
+ RealtimeRowProcessor(
+ @Ui ListeningExecutorService uiExecutor,
+ Cp2PhoneLookup cp2PhoneLookup,
+ PhoneLookupSelector phoneLookupSelector) {
+ this.uiExecutor = uiExecutor;
+ this.cp2PhoneLookup = cp2PhoneLookup;
+ this.phoneLookupSelector = phoneLookupSelector;
+ }
+
+ /**
+ * Converts a {@link CoalescedRow} to a future which is the result of performing additional work
+ * on the row. Returns {@link Optional#absent()} if no modifications were necessary.
+ */
+ @MainThread
+ ListenableFuture<Optional<CoalescedRow>> applyRealtimeProcessing(final CoalescedRow row) {
+ // Cp2PhoneLookup can not always efficiently process all rows.
+ if (!row.cp2InfoIncomplete()) {
+ return Futures.immediateFuture(Optional.absent());
+ }
+
+ Cp2Info cachedCp2Info = cache.get(row.number());
+ if (cachedCp2Info != null) {
+ if (cachedCp2Info.equals(Cp2Info.getDefaultInstance())) {
+ return Futures.immediateFuture(Optional.absent());
+ }
+ return Futures.immediateFuture(Optional.of(applyCp2InfoToRow(cachedCp2Info, row)));
+ }
+
+ ListenableFuture<Cp2Info> cp2InfoFuture = cp2PhoneLookup.lookupByNumber(row.number());
+ return Futures.transform(
+ cp2InfoFuture,
+ cp2Info -> {
+ cache.put(row.number(), cp2Info);
+ if (!cp2Info.equals(Cp2Info.getDefaultInstance())) {
+ return Optional.of(applyCp2InfoToRow(cp2Info, row));
+ }
+ return Optional.absent();
+ },
+ uiExecutor /* ensures the cache is updated on a single thread */);
+ }
+
+ private CoalescedRow applyCp2InfoToRow(Cp2Info cp2Info, CoalescedRow row) {
+ PhoneLookupInfo phoneLookupInfo = PhoneLookupInfo.newBuilder().setCp2Info(cp2Info).build();
+ // It is safe to overwrite any existing data because CP2 always has highest priority.
+ return row.toBuilder()
+ .setName(phoneLookupSelector.selectName(phoneLookupInfo))
+ .setPhotoUri(phoneLookupSelector.selectPhotoUri(phoneLookupInfo))
+ .setPhotoId(phoneLookupSelector.selectPhotoId(phoneLookupInfo))
+ .setLookupUri(phoneLookupSelector.selectLookupUri(phoneLookupInfo))
+ .setNumberTypeLabel(phoneLookupSelector.selectNumberLabel(phoneLookupInfo))
+ .build();
+ }
+}