Group calls in the call log according to their corresponding actions.

Currently different types of calls can be grouped together (e.g., 1 voice call and 1 IMS call to/from the same number), which makes it difficult to choose the icon for the call detail UI's call back button.

This CL adds an extra constraint that separates different call types (Lightbringer, IMS, and voice). This way calls in the call detail UI are in the same category and an appropriate icon can be set.

Bug: 66026167
Test: CallLogGroupBuilderTest.addGroups_MixedEntries_PartiallyGroupedByAction
PiperOrigin-RevId: 171602617
Change-Id: Id8170206009ba836a40c38a86914c71d5c7701dc
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index e79c89c..2283215 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -64,6 +64,7 @@
 import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
 import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
 import com.android.dialer.calllogutils.PhoneAccountUtils;
 import com.android.dialer.calllogutils.PhoneCallDetails;
 import com.android.dialer.common.Assert;
@@ -479,8 +480,17 @@
   @NonNull private final Set<Uri> mHiddenItemUris = new ArraySet<>();
 
   private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener;
+
   /**
-   * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into
+   * Map, keyed by call ID, used to track the callback action for a call. Calls associated with the
+   * same callback action will be put into the same primary call group in {@link
+   * com.android.dialer.app.calllog.CallLogGroupBuilder}. This information is used to set the
+   * callback icon and trigger the corresponding action.
+   */
+  private final Map<Long, Integer> mCallbackActions = new ArrayMap<>();
+
+  /**
+   * Map, keyed by call ID, used to track the day group for a call. As call log entries are put into
    * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are
    * also assigned a secondary "day group". This map tracks the day group assigned to all calls in
    * the call log. This information is used to trigger the display of a day group header above the
@@ -491,7 +501,7 @@
    * previous day group without having to reverse the cursor to the start of the previous day call
    * log entry.
    */
-  private Map<Long, Integer> mDayGroups = new ArrayMap<>();
+  private final Map<Long, Integer> mDayGroups = new ArrayMap<>();
 
   private boolean mLoading = true;
   private ContactsPreferences mContactsPreferences;
@@ -688,7 +698,7 @@
 
   @Override
   protected void addGroups(Cursor cursor) {
-    mCallLogGroupBuilder.addGroups(cursor);
+    mCallLogGroupBuilder.addGroups(cursor, mActivity);
   }
 
   @Override
@@ -865,10 +875,11 @@
           protected void onPostExecute(Boolean success) {
             views.isLoaded = true;
             if (success) {
-              int currentGroup = getDayGroupForCall(views.rowId);
-              if (currentGroup != details.previousGroup) {
+              views.callbackAction = getCallbackAction(views.rowId);
+              int currentDayGroup = getDayGroup(views.rowId);
+              if (currentDayGroup != details.previousGroup) {
                 views.dayGroupHeaderVisibility = View.VISIBLE;
-                views.dayGroupHeaderText = getGroupDescription(currentGroup);
+                views.dayGroupHeaderText = getGroupDescription(currentDayGroup);
               } else {
                 views.dayGroupHeaderVisibility = View.GONE;
               }
@@ -1226,7 +1237,7 @@
       cursor.moveToPosition(startingPosition);
       return CallLogGroupBuilder.DAY_GROUP_NONE;
     }
-    int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID));
+    int result = getDayGroup(cursor.getLong(CallLogQuery.ID));
     cursor.moveToPosition(startingPosition);
     return result;
   }
@@ -1236,14 +1247,30 @@
   }
 
   /**
-   * Given a call Id, look up the day group that the call belongs to. The day group data is
-   * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
+   * Given a call ID, look up its callback action. Callback action data are populated in {@link
+   * com.android.dialer.app.calllog.CallLogGroupBuilder}.
    *
-   * @param callId The call to retrieve the day group for.
+   * @param callId The call ID to retrieve the callback action.
+   * @return The callback action for the call.
+   */
+  @MainThread
+  private int getCallbackAction(long callId) {
+    Integer result = mCallbackActions.get(callId);
+    if (result != null) {
+      return result;
+    }
+    return CallbackAction.NONE;
+  }
+
+  /**
+   * Given a call ID, look up the day group the call belongs to. Day group data are populated in
+   * {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
+   *
+   * @param callId The call ID to retrieve the day group.
    * @return The day group for the call.
    */
   @MainThread
-  private int getDayGroupForCall(long callId) {
+  private int getDayGroup(long callId) {
     Integer result = mDayGroups.get(callId);
     if (result != null) {
       return result;
@@ -1306,17 +1333,27 @@
   }
 
   /**
+   * Stores the callback action associated with a call in the call log.
+   *
+   * @param rowId The row ID of the current call.
+   * @param callbackAction The current call's callback action.
+   */
+  @Override
+  @MainThread
+  public void setCallbackAction(long rowId, @CallbackAction int callbackAction) {
+    mCallbackActions.put(rowId, callbackAction);
+  }
+
+  /**
    * Stores the day group associated with a call in the call log.
    *
-   * @param rowId The row Id of the current call.
+   * @param rowId The row ID of the current call.
    * @param dayGroup The day group the call belongs in.
    */
   @Override
   @MainThread
   public void setDayGroup(long rowId, int dayGroup) {
-    if (!mDayGroups.containsKey(rowId)) {
-      mDayGroups.put(rowId, dayGroup);
-    }
+    mDayGroups.put(rowId, dayGroup);
   }
 
   /** Clears the day group associations on re-bind of the call log. */
diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
index 45ff378..57a8be7 100644
--- a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
+++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.app.calllog;
 
+import android.content.Context;
 import android.database.Cursor;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
@@ -25,6 +26,8 @@
 import android.text.TextUtils;
 import android.text.format.Time;
 import com.android.contacts.common.util.DateUtils;
+import com.android.dialer.calllogutils.CallbackActionHelper;
+import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
 import com.android.dialer.compat.AppCompatConstants;
 import com.android.dialer.phonenumbercache.CallLogQuery;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
@@ -71,7 +74,7 @@
    *
    * @see GroupingListAdapter#addGroups(Cursor)
    */
-  public void addGroups(Cursor cursor) {
+  public void addGroups(Cursor cursor, Context context) {
     final int count = cursor.getCount();
     if (count == 0) {
       return;
@@ -90,23 +93,32 @@
     int groupDayGroup = getDayGroup(firstDate, currentTime);
     mGroupCreator.setDayGroup(firstRowId, groupDayGroup);
 
-    // Instantiate the group values to those of the first call in the cursor.
+    // Determine the callback action for the first call in the cursor.
     String groupNumber = cursor.getString(CallLogQuery.NUMBER);
+    String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+    int groupFeatures = cursor.getInt(CallLogQuery.FEATURES);
+    int groupCallbackAction =
+        CallbackActionHelper.getCallbackAction(
+            groupNumber, groupFeatures, groupAccountComponentName, context);
+    mGroupCreator.setCallbackAction(firstRowId, groupCallbackAction);
+
+    // Instantiate other group values to those of the first call in the cursor.
+    String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
     String groupPostDialDigits =
         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
     String groupViaNumbers =
         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
     int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
-    String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
-    String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
     int groupSize = 1;
 
     String number;
     String numberPostDialDigits;
     String numberViaNumbers;
     int callType;
+    int features;
     String accountComponentName;
     String accountId;
+    int callbackAction;
 
     while (cursor.moveToNext()) {
       // Obtain the values for the current call to group.
@@ -118,21 +130,28 @@
       numberViaNumbers =
           (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
       callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+      features = cursor.getInt(CallLogQuery.FEATURES);
       accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
       accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+      callbackAction =
+          CallbackActionHelper.getCallbackAction(number, features, accountComponentName, context);
 
       final boolean isSameNumber = equalNumbers(groupNumber, number);
       final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits);
       final boolean isSameViaNumbers = groupViaNumbers.equals(numberViaNumbers);
       final boolean isSameAccount =
           isSameAccount(groupAccountComponentName, accountComponentName, groupAccountId, accountId);
+      final boolean isSameCallbackAction = (groupCallbackAction == callbackAction);
 
-      // Group with the same number and account. Never group voicemails. Only group blocked
-      // calls with other blocked calls.
+      // Group calls with the following criteria:
+      // (1) Calls with the same number, account, and callback action should be in the same group;
+      // (2) Never group voice mails; and
+      // (3) Only group blocked calls with other blocked calls.
       if (isSameNumber
           && isSameAccount
           && isSamePostDialDigits
           && isSameViaNumbers
+          && isSameCallbackAction
           && areBothNotVoicemail(callType, groupCallType)
           && (areBothNotBlocked(callType, groupCallType)
               || areBothBlocked(callType, groupCallType))) {
@@ -158,10 +177,12 @@
         groupCallType = callType;
         groupAccountComponentName = accountComponentName;
         groupAccountId = accountId;
+        groupCallbackAction = callbackAction;
       }
 
-      // Save the day group associated with the current call.
+      // Save the callback action and the day group associated with the current call.
       final long currentCallId = cursor.getLong(CallLogQuery.ID);
+      mGroupCreator.setCallbackAction(currentCallId, groupCallbackAction);
       mGroupCreator.setDayGroup(currentCallId, groupDayGroup);
     }
 
@@ -259,12 +280,22 @@
     void addGroup(int cursorPosition, int size);
 
     /**
+     * Defines the interface for tracking each call's callback action. Calls in a call group are
+     * associated with the same callback action as the first call in the group. The value of a
+     * callback action should be one of the categories in {@link CallbackAction}.
+     *
+     * @param rowId The row ID of the current call.
+     * @param callbackAction The current call's callback action.
+     */
+    void setCallbackAction(long rowId, @CallbackAction int callbackAction);
+
+    /**
      * Defines the interface for tracking the day group each call belongs to. Calls in a call group
      * are assigned the same day group as the first call in the group. The day group assigns calls
      * to the buckets: Today, Yesterday, Last week, and Other
      *
-     * @param rowId The row Id of the current call.
-     * @param dayGroup The day group the call belongs in.
+     * @param rowId The row ID of the current call.
+     * @param dayGroup The day group the call belongs to.
      */
     void setDayGroup(long rowId, int dayGroup);
 
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index f0eee11..225b652 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -66,6 +66,7 @@
 import com.android.dialer.calldetails.CallDetailsActivity;
 import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
@@ -228,6 +229,7 @@
   private final View.OnLongClickListener longPressListener;
   private boolean mVoicemailPrimaryActionButtonClicked;
 
+  public int callbackAction;
   public int dayGroupHeaderVisibility;
   public CharSequence dayGroupHeaderText;
   public boolean isAttachedToWindow;
@@ -511,36 +513,53 @@
       } else {
         primaryActionButtonView.setVisibility(View.GONE);
       }
-    } else {
-      // Treat as normal list item; show call button, if possible.
-      if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) {
-        boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
+      return;
+    }
 
-        if (!isVoicemailNumber && showLightbringerPrimaryButton()) {
+    // Treat as normal list item; show call button, if possible.
+    if (!PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) {
+      primaryActionButtonView.setTag(null);
+      primaryActionButtonView.setVisibility(View.GONE);
+      return;
+    }
+
+    switch (callbackAction) {
+      case CallbackAction.IMS_VIDEO:
+        primaryActionButtonView.setTag(
+            IntentProvider.getReturnVideoCallIntentProvider(number, accountHandle));
+        primaryActionButtonView.setContentDescription(
+            TextUtils.expandTemplate(
+                mContext.getString(R.string.description_video_call_action), validNameOrNumber));
+        primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24);
+        primaryActionButtonView.setVisibility(View.VISIBLE);
+        break;
+      case CallbackAction.LIGHTBRINGER:
+        if (showLightbringerPrimaryButton()) {
           CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount();
           primaryActionButtonView.setTag(IntentProvider.getLightbringerIntentProvider(number));
-          primaryActionButtonView.setContentDescription(
-              TextUtils.expandTemplate(
-                  mContext.getString(R.string.description_video_call_action), validNameOrNumber));
-          primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24);
-          primaryActionButtonView.setVisibility(View.VISIBLE);
-          return;
-        }
-
-        if (isVoicemailNumber) {
-          // Call to generic voicemail number, in case there are multiple accounts.
-          primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider());
         } else {
-          if (this.info != null && this.info.lookupKey != null) {
-            primaryActionButtonView.setTag(
-                IntentProvider.getAssistedDialIntentProvider(
-                    number + postDialDigits,
-                    mContext,
-                    mContext.getSystemService(TelephonyManager.class)));
-          } else {
-            primaryActionButtonView.setTag(
-                IntentProvider.getReturnCallIntentProvider(number + postDialDigits));
-          }
+          primaryActionButtonView.setTag(
+              IntentProvider.getReturnVideoCallIntentProvider(number, accountHandle));
+        }
+        primaryActionButtonView.setContentDescription(
+            TextUtils.expandTemplate(
+                mContext.getString(R.string.description_video_call_action), validNameOrNumber));
+        primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24);
+        primaryActionButtonView.setVisibility(View.VISIBLE);
+        break;
+      case CallbackAction.VOICE:
+        if (mCallLogCache.isVoicemailNumber(accountHandle, number)) {
+          // Call to generic voicemail number, in case there are multiple accounts
+          primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider());
+        } else if (this.info != null && this.info.lookupKey != null) {
+          primaryActionButtonView.setTag(
+              IntentProvider.getAssistedDialIntentProvider(
+                  number + postDialDigits,
+                  mContext,
+                  mContext.getSystemService(TelephonyManager.class)));
+        } else {
+          primaryActionButtonView.setTag(
+              IntentProvider.getReturnCallIntentProvider(number + postDialDigits));
         }
 
         primaryActionButtonView.setContentDescription(
@@ -548,10 +567,10 @@
                 mContext.getString(R.string.description_call_action), validNameOrNumber));
         primaryActionButtonView.setImageResource(R.drawable.quantum_ic_call_vd_theme_24);
         primaryActionButtonView.setVisibility(View.VISIBLE);
-      } else {
+        break;
+      default:
         primaryActionButtonView.setTag(null);
         primaryActionButtonView.setVisibility(View.GONE);
-      }
     }
   }
 
diff --git a/java/com/android/dialer/calllogutils/CallbackActionHelper.java b/java/com/android/dialer/calllogutils/CallbackActionHelper.java
new file mode 100644
index 0000000..297d5e6
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallbackActionHelper.java
@@ -0,0 +1,95 @@
+/*
+ * 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.calllogutils;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import com.android.dialer.lightbringer.Lightbringer;
+import com.android.dialer.lightbringer.LightbringerComponent;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Helper class to determine the callback action associated with a call in the call log. */
+public class CallbackActionHelper {
+
+  /** Specifies the action a user can take to make a callback. */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    CallbackAction.NONE,
+    CallbackAction.IMS_VIDEO,
+    CallbackAction.LIGHTBRINGER,
+    CallbackAction.VOICE
+  })
+  public @interface CallbackAction {
+    int NONE = 0;
+    int IMS_VIDEO = 1;
+    int LIGHTBRINGER = 2;
+    int VOICE = 3;
+  }
+
+  /**
+   * Returns the {@link CallbackAction} that can be associated with a call.
+   *
+   * @param number The phone number in column {@link android.provider.CallLog.Calls#NUMBER}.
+   * @param features Value of features in column {@link android.provider.CallLog.Calls#FEATURES}.
+   * @param phoneAccountComponentName Account name in column {@link
+   *     android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}.
+   * @param context The context in which the method is called.
+   * @return One of the values in {@link CallbackAction}
+   */
+  public static @CallbackAction int getCallbackAction(
+      String number, int features, String phoneAccountComponentName, Context context) {
+    return getCallbackAction(
+        number, features, isLightbringerCall(phoneAccountComponentName, context));
+  }
+
+  /**
+   * Returns the {@link CallbackAction} that can be associated with a call.
+   *
+   * @param number The phone number in column {@link android.provider.CallLog.Calls#NUMBER}.
+   * @param features Value of features in column {@link android.provider.CallLog.Calls#FEATURES}.
+   * @param isLightbringerCall Whether the call is a Lightbringer call.
+   * @return One of the values in {@link CallbackAction}
+   */
+  public static @CallbackAction int getCallbackAction(
+      String number, int features, boolean isLightbringerCall) {
+    if (TextUtils.isEmpty(number)) {
+      return CallbackAction.NONE;
+    }
+    if (isLightbringerCall) {
+      return CallbackAction.LIGHTBRINGER;
+    }
+
+    boolean isVideoCall = (features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO;
+    if (isVideoCall) {
+      return CallbackAction.IMS_VIDEO;
+    }
+
+    return CallbackAction.VOICE;
+  }
+
+  private static boolean isLightbringerCall(String phoneAccountComponentName, Context context) {
+    Lightbringer lightBringer = LightbringerComponent.get(context).getLightbringer();
+    return lightBringer.getPhoneAccountComponentName() != null
+        && lightBringer
+            .getPhoneAccountComponentName()
+            .flattenToString()
+            .equals(phoneAccountComponentName);
+  }
+}