Integrate CallDetailsActivity with the new call log UI.

Bug: 70218437
Test: CallDetailsActivityTest, CallDetailsCursorLoaderTest, ModulesTest
PiperOrigin-RevId: 178918820
Change-Id: Ib8034190550e8ca8e6e7fd9ce521bfadc73e834f
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index c29f9e9..b314e26 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -19,9 +19,12 @@
 import android.Manifest.permission;
 import android.annotation.SuppressLint;
 import android.app.Activity;
+import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.CallLog;
@@ -35,11 +38,13 @@
 import android.support.v7.widget.Toolbar;
 import android.view.View;
 import android.widget.Toast;
+import com.android.dialer.CoalescedIds;
 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.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
@@ -62,6 +67,7 @@
 import com.android.dialer.postcall.PostCall;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.protos.ProtoParsers;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.i18n.phonenumbers.PhoneNumberUtil;
 import java.lang.ref.WeakReference;
@@ -71,10 +77,12 @@
 
 /** Displays the details of a specific call log entry. */
 public class CallDetailsActivity extends AppCompatActivity {
+  private static final int CALL_DETAILS_LOADER_ID = 0;
 
   public static final String EXTRA_PHONE_NUMBER = "phone_number";
   public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data";
   public static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
+  public static final String EXTRA_COALESCED_CALL_LOG_IDS = "coalesced_call_log_ids";
   public static final String EXTRA_CONTACT = "contact";
   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";
@@ -93,23 +101,47 @@
   private DialerContact contact;
   private CallDetailsAdapter adapter;
 
+  // This will be present only when the activity is launched from the new call log UI, i.e., a list
+  // of coalesced annotated call log IDs is included in the intent.
+  private Optional<CoalescedIds> coalescedCallLogIds = Optional.absent();
+
   public static boolean isLaunchIntent(Intent intent) {
     return intent.getComponent() != null
         && CallDetailsActivity.class.getName().equals(intent.getComponent().getClassName());
   }
 
+  /**
+   * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the old call log
+   * UI.
+   */
   public static Intent newInstance(
       Context context,
-      @NonNull CallDetailsEntries details,
-      @NonNull DialerContact contact,
+      CallDetailsEntries details,
+      DialerContact contact,
       boolean canReportCallerId,
       boolean canSupportAssistedDialing) {
-    Assert.isNotNull(details);
-    Assert.isNotNull(contact);
-
     Intent intent = new Intent(context, CallDetailsActivity.class);
-    ProtoParsers.put(intent, EXTRA_CONTACT, contact);
-    ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details);
+    ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact));
+    ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, Assert.isNotNull(details));
+    intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId);
+    intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing);
+    return intent;
+  }
+
+  /**
+   * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the new call log
+   * UI.
+   */
+  public static Intent newInstance(
+      Context context,
+      CoalescedIds coalescedAnnotatedCallLogIds,
+      DialerContact contact,
+      boolean canReportCallerId,
+      boolean canSupportAssistedDialing) {
+    Intent intent = new Intent(context, CallDetailsActivity.class);
+    ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact));
+    ProtoParsers.put(
+        intent, EXTRA_COALESCED_CALL_LOG_IDS, Assert.isNotNull(coalescedAnnotatedCallLogIds));
     intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId);
     intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing);
     return intent;
@@ -166,10 +198,30 @@
   }
 
   private void onHandleIntent(Intent intent) {
+    boolean hasCallDetailsEntries = intent.hasExtra(EXTRA_CALL_DETAILS_ENTRIES);
+    boolean hasCoalescedCallLogIds = intent.hasExtra(EXTRA_COALESCED_CALL_LOG_IDS);
+    Assert.checkArgument(
+        (hasCallDetailsEntries && !hasCoalescedCallLogIds)
+            || (!hasCallDetailsEntries && hasCoalescedCallLogIds),
+        "One and only one of EXTRA_CALL_DETAILS_ENTRIES and EXTRA_COALESCED_CALL_LOG_IDS "
+            + "can be included in the intent.");
+
     contact = ProtoParsers.getTrusted(intent, EXTRA_CONTACT, DialerContact.getDefaultInstance());
-    entries =
-        ProtoParsers.getTrusted(
-            intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance());
+    if (hasCallDetailsEntries) {
+      entries =
+          ProtoParsers.getTrusted(
+              intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance());
+    } else {
+      entries = CallDetailsEntries.getDefaultInstance();
+      coalescedCallLogIds =
+          Optional.of(
+              ProtoParsers.getTrusted(
+                  intent, EXTRA_COALESCED_CALL_LOG_IDS, CoalescedIds.getDefaultInstance()));
+      getLoaderManager()
+          .initLoader(
+              CALL_DETAILS_LOADER_ID, /* args = */ null, new CallDetailsLoaderCallbacks(this));
+    }
+
     adapter =
         new CallDetailsAdapter(
             this /* context */,
@@ -191,6 +243,43 @@
     super.onBackPressed();
   }
 
+  /**
+   * {@link LoaderCallbacks} for {@link CallDetailsCursorLoader}, which loads call detail entries
+   * from {@link AnnotatedCallLog}.
+   */
+  private static final class CallDetailsLoaderCallbacks implements LoaderCallbacks<Cursor> {
+    private final CallDetailsActivity activity;
+
+    CallDetailsLoaderCallbacks(CallDetailsActivity callDetailsActivity) {
+      this.activity = callDetailsActivity;
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+      Assert.checkState(activity.coalescedCallLogIds.isPresent());
+
+      return new CallDetailsCursorLoader(activity, activity.coalescedCallLogIds.get());
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+      updateCallDetailsEntries(CallDetailsCursorLoader.toCallDetailsEntries(data));
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+      updateCallDetailsEntries(CallDetailsEntries.getDefaultInstance());
+    }
+
+    private void updateCallDetailsEntries(CallDetailsEntries newEntries) {
+      activity.entries = newEntries;
+      activity.adapter.updateCallDetailsEntries(newEntries.getEntriesList());
+      EnrichedCallComponent.get(activity)
+          .getEnrichedCallManager()
+          .requestAllHistoricalData(activity.contact.getNumber(), newEntries);
+    }
+  }
+
   /** Delete specified calls from the call log. */
   private static class DeleteCallsTask extends AsyncTask<Void, Void, Void> {
     // Use a weak reference to hold the Activity so that there is no memory leak.
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
index 9095b86..030366e 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -115,7 +115,9 @@
 
   @Override
   public int getItemCount() {
-    return callDetailsEntries.size() + 2; // Header + footer
+    return callDetailsEntries.isEmpty()
+        ? 0
+        : callDetailsEntries.size() + 2; // plus header and footer
   }
 
   void updateCallDetailsEntries(List<CallDetailsEntry> entries) {
diff --git a/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java b/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java
new file mode 100644
index 0000000..8385253
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsCursorLoader.java
@@ -0,0 +1,139 @@
+/*
+ * 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.calldetails;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import com.android.dialer.CoalescedIds;
+import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.common.Assert;
+import com.android.dialer.duo.DuoConstants;
+
+/**
+ * A {@link CursorLoader} that loads call detail entries from {@link AnnotatedCallLog} for {@link
+ * CallDetailsActivity}.
+ */
+public final class CallDetailsCursorLoader extends CursorLoader {
+
+  // Columns in AnnotatedCallLog that are needed to build a CallDetailsEntry proto.
+  // Be sure to update (1) constants that store indexes of the elements and (2) method
+  // toCallDetailsEntry(Cursor) when updating this array.
+  public static final String[] COLUMNS_FOR_CALL_DETAILS =
+      new String[] {
+        AnnotatedCallLog._ID,
+        AnnotatedCallLog.CALL_TYPE,
+        AnnotatedCallLog.FEATURES,
+        AnnotatedCallLog.TIMESTAMP,
+        AnnotatedCallLog.DURATION,
+        AnnotatedCallLog.DATA_USAGE,
+        AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME
+      };
+
+  // Indexes for COLUMNS_FOR_CALL_DETAILS
+  private static final int ID = 0;
+  private static final int CALL_TYPE = 1;
+  private static final int FEATURES = 2;
+  private static final int TIMESTAMP = 3;
+  private static final int DURATION = 4;
+  private static final int DATA_USAGE = 5;
+  private static final int PHONE_ACCOUNT_COMPONENT_NAME = 6;
+
+  CallDetailsCursorLoader(Context context, CoalescedIds coalescedIds) {
+    super(
+        context,
+        AnnotatedCallLog.CONTENT_URI,
+        COLUMNS_FOR_CALL_DETAILS,
+        annotatedCallLogIdsSelection(coalescedIds),
+        annotatedCallLogIdsSelectionArgs(coalescedIds),
+        AnnotatedCallLog.TIMESTAMP + " DESC");
+  }
+
+  /**
+   * Build a string of the form "COLUMN_NAME IN (?, ?, ..., ?)", where COLUMN_NAME is the name of
+   * the ID column in {@link AnnotatedCallLog}.
+   *
+   * <p>This string will be used as the {@code selection} parameter to initialize the loader.
+   */
+  private static String annotatedCallLogIdsSelection(CoalescedIds coalescedIds) {
+    // First, build a string of question marks ('?') separated by commas (',').
+    StringBuilder questionMarks = new StringBuilder();
+    for (int i = 0; i < coalescedIds.getCoalescedIdCount(); i++) {
+      if (i != 0) {
+        questionMarks.append(", ");
+      }
+      questionMarks.append("?");
+    }
+
+    return AnnotatedCallLog._ID + " IN (" + questionMarks + ")";
+  }
+
+  /**
+   * Returns a string that will be used as the {@code selectionArgs} parameter to initialize the
+   * loader.
+   */
+  private static String[] annotatedCallLogIdsSelectionArgs(CoalescedIds coalescedIds) {
+    String[] args = new String[coalescedIds.getCoalescedIdCount()];
+
+    for (int i = 0; i < coalescedIds.getCoalescedIdCount(); i++) {
+      args[i] = String.valueOf(coalescedIds.getCoalescedId(i));
+    }
+
+    return args;
+  }
+
+  /**
+   * Creates a new {@link CallDetailsEntries} from the entire data set loaded by this loader.
+   *
+   * @param cursor A cursor pointing to the data set loaded by this loader. The caller must ensure
+   *     the cursor is not null and the data set it points to is not empty.
+   * @return A {@link CallDetailsEntries} proto.
+   */
+  static CallDetailsEntries toCallDetailsEntries(Cursor cursor) {
+    Assert.isNotNull(cursor);
+    Assert.checkArgument(cursor.moveToFirst());
+
+    CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder();
+
+    do {
+      entries.addEntries(toCallDetailsEntry(cursor));
+    } while (cursor.moveToNext());
+
+    return entries.build();
+  }
+
+  /** Creates a new {@link CallDetailsEntry} from the provided cursor using its current position. */
+  private static CallDetailsEntry toCallDetailsEntry(Cursor cursor) {
+    CallDetailsEntry.Builder entry = CallDetailsEntry.newBuilder();
+    entry
+        .setCallId(cursor.getLong(ID))
+        .setCallType(cursor.getInt(CALL_TYPE))
+        .setFeatures(cursor.getInt(FEATURES))
+        .setDate(cursor.getLong(TIMESTAMP))
+        .setDuration(cursor.getLong(DURATION))
+        .setDataUsage(cursor.getLong(DATA_USAGE));
+
+    String phoneAccountComponentName = cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME);
+    entry.setIsDuoCall(
+        DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
+            .flattenToString()
+            .equals(phoneAccountComponentName));
+
+    return entry.build();
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
index 550e284..cccaa73 100644
--- a/java/com/android/dialer/calllog/ui/menu/Modules.java
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -24,7 +24,6 @@
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
 import com.android.dialer.calldetails.CallDetailsActivity;
-import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.calllogutils.PhoneAccountUtils;
@@ -65,7 +64,7 @@
 
     // TODO(zachh): Revisit if DialerContact is the best thing to pass to CallDetails; could
     // it use a ContactPrimaryActionInfo instead?
-    addModuleForAccessingCallDetails(context, createDialerContactFromRow(row), modules);
+    addModuleForAccessingCallDetails(context, row, modules);
 
     return modules;
   }
@@ -182,10 +181,9 @@
   }
 
   private static void addModuleForAccessingCallDetails(
-      Context context, DialerContact dialerContact, List<ContactActionModule> modules) {
-    // TODO(zachh): Load CallDetailsEntries and canReportInaccurateNumber in
-    // CallDetailsActivity (see also isPeopleApiSource(sourceType)).
-    CallDetailsEntries callDetailsEntries = CallDetailsEntries.getDefaultInstance();
+      Context context, CoalescedRow row, List<ContactActionModule> modules) {
+    // TODO(zachh): Load canReportInaccurateNumber in CallDetailsActivity
+    // (see also isPeopleApiSource(sourceType)).
     boolean canReportInaccurateNumber = false;
     boolean canSupportAssistedDialing = false; // TODO(zachh): Properly set value.
 
@@ -194,8 +192,8 @@
             context,
             CallDetailsActivity.newInstance(
                 context,
-                callDetailsEntries,
-                dialerContact,
+                row.coalescedIds(),
+                createDialerContactFromRow(row),
                 canReportInaccurateNumber,
                 canSupportAssistedDialing),
             R.string.call_details_menu_label,