Merge "Improve the latency of NotificationChannelSlice" into qt-dev
diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java
index 07fc899..a262191 100644
--- a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java
+++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java
@@ -66,7 +66,12 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 public class NotificationChannelSlice implements CustomSliceable {
@@ -98,6 +103,7 @@
     private static final String PACKAGE_NAME = "package_name";
     private static final String PACKAGE_UID = "package_uid";
     private static final String CHANNEL_ID = "channel_id";
+    private static final long TASK_TIMEOUT_MS = 100;
 
     /**
      * Sort notification channel with weekly average sent count by descending.
@@ -129,6 +135,7 @@
             };
 
     protected final Context mContext;
+    private final ExecutorService mExecutorService;
     @VisibleForTesting
     NotificationBackend mNotificationBackend;
     private NotificationBackend.AppRow mAppRow;
@@ -138,6 +145,7 @@
     public NotificationChannelSlice(Context context) {
         mContext = context;
         mNotificationBackend = new NotificationBackend();
+        mExecutorService = Executors.newCachedThreadPool();
     }
 
     @Override
@@ -151,10 +159,7 @@
          * 2. Multiple channels.
          * 3. Sent at least ~10 notifications.
          */
-        // TODO(b/123065955): Review latency of NotificationChannelSlice
-        final List<PackageInfo> multiChannelPackages = getMultiChannelPackages(
-                getRecentlyInstalledPackages());
-        mPackageName = getMaxSentNotificationsPackage(multiChannelPackages);
+        mPackageName = getEligibleNotificationsPackage(getRecentlyInstalledPackages());
         if (mPackageName == null) {
             // Return a header with IsError flag, if package is not found.
             return listBuilder.setHeader(getNoSuggestedAppHeader())
@@ -306,25 +311,6 @@
         return PendingIntent.getBroadcast(mContext, intent.hashCode(), intent, 0);
     }
 
-    private List<PackageInfo> getMultiChannelPackages(List<PackageInfo> packageInfoList) {
-        final List<PackageInfo> multiChannelPackages = new ArrayList<>();
-
-        if (packageInfoList.isEmpty()) {
-            return multiChannelPackages;
-        }
-
-        for (PackageInfo packageInfo : packageInfoList) {
-            final int channelCount = mNotificationBackend.getChannelCount(packageInfo.packageName,
-                    getApplicationUid(packageInfo.packageName));
-            if (channelCount > 1) {
-                multiChannelPackages.add(packageInfo);
-            }
-        }
-
-        // TODO(b/119831690): Filter the packages which doesn't have any configurable channel.
-        return multiChannelPackages;
-    }
-
     private List<PackageInfo> getRecentlyInstalledPackages() {
         final long startTime = System.currentTimeMillis() - DURATION_START_DAYS;
         final long endTime = System.currentTimeMillis() - DURATION_END_DAYS;
@@ -383,19 +369,33 @@
                 .collect(Collectors.toList());
     }
 
-    private String getMaxSentNotificationsPackage(List<PackageInfo> packageInfoList) {
+    private String getEligibleNotificationsPackage(List<PackageInfo> packageInfoList) {
         if (packageInfoList.isEmpty()) {
             return null;
         }
 
+        // Create tasks to get notification data for multi-channel packages.
+        final List<Future<NotificationBackend.AppRow>> appRowTasks = new ArrayList<>();
+        for (PackageInfo packageInfo : packageInfoList) {
+            final NotificationMultiChannelAppRow future = new NotificationMultiChannelAppRow(
+                    mContext, mNotificationBackend, packageInfo);
+            appRowTasks.add(mExecutorService.submit(future));
+        }
+
         // Get the package which has sent at least ~10 notifications and not turn off channels.
         int maxSentCount = 0;
         String maxSentCountPackage = null;
-        for (PackageInfo packageInfo : packageInfoList) {
-            final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext,
-                    mContext.getPackageManager(), packageInfo);
+        for (Future<NotificationBackend.AppRow> appRowTask : appRowTasks) {
+            NotificationBackend.AppRow appRow = null;
+            try {
+                appRow = appRowTask.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (ExecutionException | InterruptedException | TimeoutException e) {
+                Log.w(TAG, "Failed to get notification data.", e);
+            }
+
             // Ignore packages which are banned notifications or block all displayable channels.
-            if (appRow.banned || isAllChannelsBlocked(getDisplayableChannels(appRow))) {
+            if (appRow == null || appRow.banned || isAllChannelsBlocked(
+                    getDisplayableChannels(appRow))) {
                 continue;
             }
 
@@ -403,7 +403,7 @@
             final int sentCount = appRow.sentByApp.sentCount;
             if (sentCount >= MIN_NOTIFICATION_SENT_COUNT && sentCount > maxSentCount) {
                 maxSentCount = sentCount;
-                maxSentCountPackage = packageInfo.packageName;
+                maxSentCountPackage = appRow.pkg;
                 mAppRow = appRow;
             }
         }
diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java
new file mode 100644
index 0000000..4edce14
--- /dev/null
+++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.settings.homepage.contextualcards.slices;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+
+import com.android.settings.notification.NotificationBackend;
+
+import java.util.concurrent.Callable;
+
+/**
+ * This class is responsible for getting notification app row from package which has multiple
+ * notification channels.{@link NotificationChannelSlice} uses it to improve latency.
+ */
+class NotificationMultiChannelAppRow implements Callable<NotificationBackend.AppRow> {
+
+    private final Context mContext;
+    private final NotificationBackend mNotificationBackend;
+    private final PackageInfo mPackageInfo;
+
+    public NotificationMultiChannelAppRow(Context context, NotificationBackend notificationBackend,
+            PackageInfo packageInfo) {
+        mContext = context;
+        mNotificationBackend = notificationBackend;
+        mPackageInfo = packageInfo;
+    }
+
+    @Override
+    public NotificationBackend.AppRow call() throws Exception {
+        final int channelCount = mNotificationBackend.getChannelCount(
+                mPackageInfo.applicationInfo.packageName, mPackageInfo.applicationInfo.uid);
+        if (channelCount > 1) {
+            return mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(),
+                    mPackageInfo);
+        }
+        return null;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java
index 6c020f8..60a6b42 100644
--- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java
+++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSliceTest.java
@@ -319,6 +319,7 @@
         applicationInfo.name = APP_LABEL;
         applicationInfo.uid = UID;
         applicationInfo.flags = flags;
+        applicationInfo.packageName = PACKAGE_NAME;
 
         final PackageInfo packageInfo = new PackageInfo();
         packageInfo.packageName = PACKAGE_NAME;
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRowTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRowTest.java
new file mode 100644
index 0000000..d722af6
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRowTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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.settings.homepage.contextualcards.slices;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import com.android.settings.notification.NotificationBackend;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class NotificationMultiChannelAppRowTest {
+
+    @Mock
+    private NotificationBackend mNotificationBackend;
+    private Context mContext;
+    private NotificationMultiChannelAppRow mNotificationMultiChannelAppRow;
+    private PackageInfo mPackageInfo;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+        mPackageInfo = new PackageInfo();
+        mPackageInfo.applicationInfo = new ApplicationInfo();
+        mPackageInfo.applicationInfo.packageName = "com.android.test";
+        mNotificationMultiChannelAppRow = new NotificationMultiChannelAppRow(mContext,
+                mNotificationBackend, mPackageInfo);
+    }
+
+    @Test
+    public void call_isMultiChannel_shouldLoadAppRow() throws Exception {
+        doReturn(3).when(mNotificationBackend).getChannelCount(any(String.class),
+                any(int.class));
+
+        mNotificationMultiChannelAppRow.call();
+
+        verify(mNotificationBackend).loadAppRow(any(Context.class), any(PackageManager.class),
+                any(PackageInfo.class));
+    }
+
+    @Test
+    public void call_isNotMultiChannel_shouldNotLoadAppRow() throws Exception {
+        doReturn(1).when(mNotificationBackend).getChannelCount(any(String.class),
+                any(int.class));
+
+        mNotificationMultiChannelAppRow.call();
+
+        verify(mNotificationBackend, never()).loadAppRow(any(Context.class),
+                any(PackageManager.class), any(PackageInfo.class));
+    }
+}