Talkback for focus on collapsed bubble stack

Bug: 131610000
Test: manual
Change-Id: I6aa79dfea751c47c86e93f56f7916dd56dbc003f
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 8aad0f8..f60e95e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -18,6 +18,9 @@
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.view.LayoutInflater;
 
@@ -37,6 +40,7 @@
 
     private final String mKey;
     private final String mGroupId;
+    private String mAppName;
     private final BubbleExpandedView.OnBubbleBlockedListener mListener;
 
     private boolean mInflated;
@@ -45,6 +49,7 @@
     BubbleExpandedView expandedView;
     private long mLastUpdated;
     private long mLastAccessed;
+    private PackageManager mPm;
 
     public static String groupId(NotificationEntry entry) {
         UserHandle user = entry.notification.getUser();
@@ -53,16 +58,33 @@
 
     /** Used in tests when no UI is required. */
     @VisibleForTesting(visibility = PRIVATE)
-    Bubble(NotificationEntry e) {
-        this (e, null);
+    Bubble(Context context, NotificationEntry e) {
+        this (context, e, null);
     }
 
-    Bubble(NotificationEntry e, BubbleExpandedView.OnBubbleBlockedListener listener) {
+    Bubble(Context context, NotificationEntry e,
+            BubbleExpandedView.OnBubbleBlockedListener listener) {
         entry = e;
         mKey = e.key;
         mLastUpdated = e.notification.getPostTime();
         mGroupId = groupId(e);
         mListener = listener;
+
+        mPm = context.getPackageManager();
+        ApplicationInfo info;
+        try {
+            info = mPm.getApplicationInfo(
+                entry.notification.getPackageName(),
+                PackageManager.MATCH_UNINSTALLED_PACKAGES
+                    | PackageManager.MATCH_DISABLED_COMPONENTS
+                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                    | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+            if (info != null) {
+                mAppName = String.valueOf(mPm.getApplicationLabel(info));
+            }
+        } catch (PackageManager.NameNotFoundException unused) {
+            mAppName = entry.notification.getPackageName();
+        }
     }
 
     public String getKey() {
@@ -77,6 +99,10 @@
         return entry.notification.getPackageName();
     }
 
+    public String getAppName() {
+        return mAppName;
+    }
+
     boolean isInflated() {
         return mInflated;
     }
@@ -97,9 +123,9 @@
 
         expandedView = (BubbleExpandedView) inflater.inflate(
                 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
-        expandedView.setEntry(entry, stackView);
-
+        expandedView.setEntry(entry, stackView, mAppName);
         expandedView.setOnBlockedListener(mListener);
+
         mInflated = true;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 48edf67..aed117b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -193,7 +193,7 @@
             if (shouldCollapse) {
                 collapseStack();
             }
-            updateVisibility();
+            updateStack();
         }
     }
 
@@ -534,10 +534,11 @@
             }
         }
 
+        // Runs on state change.
         @Override
         public void apply() {
             mNotificationEntryManager.updateNotifications();
-            updateVisibility();
+            updateStack();
 
             if (DEBUG) {
                 Log.d(TAG, "[BubbleData]");
@@ -554,34 +555,31 @@
     };
 
     /**
-     * Lets any listeners know if bubble state has changed.
+     * Updates the visibility of the bubbles based on current state.
+     * Does not un-bubble, just hides or un-hides. Notifies any
+     * {@link BubbleStateChangeListener}s of visibility changes.
+     * Updates stack description for TalkBack focus.
      */
-    private void updateBubblesShowing() {
+    public void updateStack() {
         if (mStackView == null) {
             return;
         }
-
-        boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
-        boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
-        mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
-        if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
-            mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
-        }
-    }
-
-    /**
-     * Updates the visibility of the bubbles based on current state.
-     * Does not un-bubble, just hides or un-hides. Will notify any
-     * {@link BubbleStateChangeListener}s if visibility changes.
-     */
-    public void updateVisibility() {
         if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) {
             // Bubbles only appear in unlocked shade
             mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE);
         } else if (mStackView != null) {
             mStackView.setVisibility(INVISIBLE);
         }
-        updateBubblesShowing();
+
+        // Let listeners know if bubble state changed.
+        boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
+        boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
+        mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
+        if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
+            mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
+        }
+
+        mStackView.updateContentDescription();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 9156e06..1858244 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -178,7 +178,7 @@
         Bubble bubble = getBubbleWithKey(entry.key);
         if (bubble == null) {
             // Create a new bubble
-            bubble = new Bubble(entry, this::onBubbleBlocked);
+            bubble = new Bubble(mContext, entry, this::onBubbleBlocked);
             doAdd(bubble);
             trim();
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 39867c3..fa137a1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -244,9 +244,10 @@
     /**
      * Sets the notification entry used to populate this view.
      */
-    public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
+    public void setEntry(NotificationEntry entry, BubbleStackView stackView, String appName) {
         mStackView = stackView;
         mEntry = entry;
+        mAppName = appName;
 
         ApplicationInfo info;
         try {
@@ -257,12 +258,10 @@
                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
             if (info != null) {
-                mAppName = String.valueOf(mPm.getApplicationLabel(info));
                 mAppIcon = mPm.getApplicationIcon(info);
             }
         } catch (PackageManager.NameNotFoundException e) {
-            // Ahh... just use package name
-            mAppName = entry.notification.getPackageName();
+            // Do nothing.
         }
         if (mAppIcon == null) {
             mAppIcon = mPm.getDefaultActivityIcon();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 4fef157..4ad3a33 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -23,6 +23,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.app.Notification;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.ColorMatrix;
@@ -553,6 +554,43 @@
         return false;
     }
 
+    /**
+     * Update content description for a11y TalkBack.
+     */
+    public void updateContentDescription() {
+        if (mBubbleData.getBubbles().isEmpty()) {
+            return;
+        }
+        Bubble topBubble = mBubbleData.getBubbles().get(0);
+        String appName = topBubble.getAppName();
+        Notification notification = topBubble.entry.notification.getNotification();
+        CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+        String titleStr = getResources().getString(R.string.stream_notification);
+        if (titleCharSeq != null) {
+            titleStr = titleCharSeq.toString();
+        }
+        int moreCount = mBubbleContainer.getChildCount() - 1;
+
+        // Example: Title from app name.
+        String singleDescription = getResources().getString(
+                R.string.bubble_content_description_single, titleStr, appName);
+
+        // Example: Title from app name and 4 more.
+        String stackDescription = getResources().getString(
+                R.string.bubble_content_description_stack, titleStr, appName, moreCount);
+
+        if (mIsExpanded) {
+            // TODO(b/129522932) - update content description for each bubble in expanded view.
+        } else {
+            // Collapsed stack.
+            if (moreCount > 0) {
+                mBubbleContainer.setContentDescription(stackDescription);
+            } else {
+                mBubbleContainer.setContentDescription(singleDescription);
+            }
+        }
+    }
+
     private void updateSystemGestureExcludeRects() {
         // Exclude the region occupied by the first BubbleView in the stack
         Rect excludeZone = mSystemGestureExclusionRects.get(0);