Ensure notif-based bubbles don't occur once and for all!! & improve logging

* Move filtering of intent into NotificationManagerService
* Add Log.w messages when an activity isn't configured correctly
* Updates tests so that they will still pass
* Add tests for badly-configured bubbles

Test: atest NotificationManagerTest (other CL)
Test: manual - send a badly configured bubble and note the logs & that
      it doesn't appear
Fixes: 132913407
Change-Id: I3b3e2d63b34fe51f1aea178a92f567b6b3e0b927
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index dcc0419..7e016bb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bubbles;
 
+import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
@@ -41,7 +42,9 @@
 import android.app.IActivityTaskManager;
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.pm.ParceledListSlice;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -112,7 +115,7 @@
     public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
 
     // Enables some subset of notifs to automatically become bubbles
-    private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
+    public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
 
     /** Flag to enable or disable the entire feature */
     private static final String ENABLE_BUBBLES = "experiment_enable_bubbles";
@@ -450,7 +453,8 @@
             if (!areBubblesEnabled(mContext)) {
                 return;
             }
-            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) {
+            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
+                    && canLaunchInActivityView(mContext, entry)) {
                 updateShowInShadeForSuppressNotification(entry);
             }
         }
@@ -460,7 +464,8 @@
             if (!areBubblesEnabled(mContext)) {
                 return;
             }
-            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) {
+            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
+                    && canLaunchInActivityView(mContext, entry)) {
                 updateBubble(entry);
             }
         }
@@ -470,7 +475,8 @@
             if (!areBubblesEnabled(mContext)) {
                 return;
             }
-            boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry);
+            boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
+                    && canLaunchInActivityView(mContext, entry);
             if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) {
                 // It was previously a bubble but no longer a bubble -- lets remove it
                 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE);
@@ -657,12 +663,6 @@
                 || autoBubbleAll;
     }
 
-    private boolean shouldAutoExpand(NotificationEntry entry) {
-        Notification.BubbleMetadata metadata = entry.getBubbleMetadata();
-        return metadata != null && metadata.getAutoExpandBubble()
-                && isForegroundApp(mContext, entry.notification.getPackageName());
-    }
-
     private void updateShowInShadeForSuppressNotification(NotificationEntry entry) {
         boolean suppressNotification = entry.getBubbleMetadata() != null
                 && entry.getBubbleMetadata().isNotificationSuppressed()
@@ -771,6 +771,48 @@
                 (int) (defaultBounciness * 100)) / 100f;
     }
 
+    /**
+     * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
+     *
+     * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically
+     * that should filter out any invalid bubbles, but should protect SysUI side just in case.
+     *
+     * @param context the context to use.
+     * @param entry the entry to bubble.
+     */
+    static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
+        PendingIntent intent = entry.getBubbleMetadata() != null
+                ? entry.getBubbleMetadata().getIntent()
+                : null;
+        if (intent == null) {
+            Log.w(TAG, "Unable to create bubble -- no intent");
+            return false;
+        }
+        ActivityInfo info =
+                intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
+        if (info == null) {
+            Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
+                    + intent);
+            return false;
+        }
+        if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
+            Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
+                    + intent);
+            return false;
+        }
+        if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
+            Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always "
+                    + "for intent: " + intent);
+            return false;
+        }
+        if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) {
+            Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: "
+                    + intent);
+            return false;
+        }
+        return true;
+    }
+
     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
     private class BubblesImeListener extends IPinnedStackListener.Stub {
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index e7948b5..6add4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -16,12 +16,10 @@
 
 package com.android.systemui.bubbles;
 
-import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
-import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING;
-import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE;
-import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS;
 import static android.view.Display.INVALID_DISPLAY;
 
+import static com.android.systemui.bubbles.BubbleController.DEBUG_ENABLE_AUTO_BUBBLE;
+
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
 import android.app.ActivityView;
@@ -30,7 +28,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -337,11 +334,10 @@
                 mNotifRow = null;
             }
             mActivityView.setVisibility(VISIBLE);
-        } else {
+        } else if (DEBUG_ENABLE_AUTO_BUBBLE) {
             // Hide activity view if we had it previously
             mActivityView.setVisibility(GONE);
             mNotifRow = mEntry.getRow();
-
         }
         updateView();
     }
@@ -532,59 +528,14 @@
     @Nullable
     private PendingIntent getBubbleIntent(NotificationEntry entry) {
         Notification notif = entry.notification.getNotification();
-        String packageName = entry.notification.getPackageName();
         Notification.BubbleMetadata data = notif.getBubbleMetadata();
-        if (data != null && canLaunchInActivityView(data.getIntent(), true /* enableLogging */,
-                packageName)) {
+        if (BubbleController.canLaunchInActivityView(mContext, entry) && data != null) {
             return data.getIntent();
-        } else if (BubbleController.shouldUseContentIntent(mContext)
-                && canLaunchInActivityView(notif.contentIntent, false /* enableLogging */,
-                packageName)) {
-            return notif.contentIntent;
         }
         return null;
     }
 
     /**
-     * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
-     *
-     * @param intent the pending intent of the bubble.
-     * @param enableLogging whether bubble developer error should be logged.
-     * @param packageName the notification package name for this bubble.
-     * @return
-     */
-    private boolean canLaunchInActivityView(PendingIntent intent, boolean enableLogging,
-            String packageName) {
-        if (intent == null) {
-            return false;
-        }
-        ActivityInfo info =
-                intent.getIntent().resolveActivityInfo(mContext.getPackageManager(), 0);
-        if (info == null) {
-            if (enableLogging) {
-                StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
-                        BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
-            }
-            return false;
-        }
-        if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
-            if (enableLogging) {
-                StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
-                        BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
-            }
-            return false;
-        }
-        if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
-            if (enableLogging) {
-                StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
-                        BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
-            }
-            return false;
-        }
-        return (info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
-    }
-
-    /**
      * Listener that is notified when a bubble is blocked.
      */
     public interface OnBubbleBlockedListener {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 46d6886..8c58247 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -46,6 +46,7 @@
 import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_FOREGROUND_SERVICE;
+import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.content.pm.PackageManager.MATCH_ALL;
@@ -82,6 +83,9 @@
 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
 import static android.service.notification.NotificationListenerService.TRIM_FULL;
 import static android.service.notification.NotificationListenerService.TRIM_LIGHT;
+import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING;
+import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE;
+import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 
 import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
@@ -131,6 +135,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
@@ -197,6 +202,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.StatsLog;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 import android.view.accessibility.AccessibilityEvent;
@@ -4821,9 +4827,12 @@
     private boolean isNotificationAppropriateToBubble(NotificationRecord r, String pkg, int userId,
             NotificationRecord oldRecord) {
         Notification notification = r.getNotification();
+        Notification.BubbleMetadata metadata = notification.getBubbleMetadata();
+        boolean intentCanBubble = metadata != null
+                && canLaunchInActivityView(getContext(), metadata.getIntent(), pkg);
 
         // Does the app want to bubble & is able to bubble
-        boolean canBubble = notification.getBubbleMetadata() != null
+        boolean canBubble = intentCanBubble
                 && mPreferencesHelper.areBubblesAllowed(pkg, userId)
                 && mPreferencesHelper.bubblesEnabled(r.sbn.getUser())
                 && r.getChannel().canBubble()
@@ -4869,6 +4878,63 @@
         return false;
     }
 
+    /**
+     * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
+     *
+     * @param context       the context to use.
+     * @param pendingIntent the pending intent of the bubble.
+     * @param packageName   the notification package name for this bubble.
+     */
+    // Keep checks in sync with BubbleController#canLaunchInActivityView.
+    @VisibleForTesting
+    protected boolean canLaunchInActivityView(Context context, PendingIntent pendingIntent,
+            String packageName) {
+        if (pendingIntent == null) {
+            Log.w(TAG, "Unable to create bubble -- no intent");
+            return false;
+        }
+
+        // Need escalated privileges to get the intent.
+        final long token = Binder.clearCallingIdentity();
+        Intent intent;
+        try {
+            intent = pendingIntent.getIntent();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        ActivityInfo info = intent != null
+                ? intent.resolveActivityInfo(context.getPackageManager(), 0)
+                : null;
+        if (info == null) {
+            StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
+                    BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
+            Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
+                    + intent);
+            return false;
+        }
+        if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
+            StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
+                    BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
+            Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
+                    + intent);
+            return false;
+        }
+        if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
+            StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
+                    BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
+            Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always "
+                    + "for intent: " + intent);
+            return false;
+        }
+        if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) {
+            Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: "
+                    + intent);
+            return false;
+        }
+        return true;
+    }
+
     private void doChannelWarningToast(CharSequence toastText) {
         Binder.withCleanCallingIdentity(() -> {
             final int defaultWarningEnabled = Build.IS_DEBUGGABLE ? 1 : 0;
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index 3ff85c8..7453c48 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -30,12 +30,14 @@
     <uses-permission android:name="android.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" />
+    <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/>
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
 
         <provider android:name=".DummyProvider"
             android:authorities="com.android.services.uitests" />
+
     </application>
 
     <instrumentation
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 9db8753..87f221a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -304,6 +304,12 @@
             void onGranted(ComponentName assistant, int userId, boolean granted);
         }
 
+        @Override
+        protected boolean canLaunchInActivityView(Context context, PendingIntent pendingIntent,
+                String packageName) {
+            // Tests for this not being true are in CTS NotificationManagerTest
+            return true;
+        }
     }
 
     private class TestableToastCallback extends ITransientNotification.Stub {