• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW;
20 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
21 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
22 
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.graphics.Color;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.util.TypedValue;
31 import android.view.KeyEvent;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.TextView;
39 
40 import androidx.annotation.Nullable;
41 import androidx.recyclerview.widget.GridLayoutManager;
42 import androidx.recyclerview.widget.RecyclerView;
43 
44 import com.android.internal.util.ContrastColorUtil;
45 import com.android.wm.shell.R;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.function.Consumer;
50 
51 /**
52  * Container view for showing aged out bubbles.
53  */
54 public class BubbleOverflowContainerView extends LinearLayout {
55     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES;
56 
57     private LinearLayout mEmptyState;
58     private TextView mEmptyStateTitle;
59     private TextView mEmptyStateSubtitle;
60     private ImageView mEmptyStateImage;
61     private BubbleController mController;
62     private BubbleOverflowAdapter mAdapter;
63     private RecyclerView mRecyclerView;
64     private List<Bubble> mOverflowBubbles = new ArrayList<>();
65 
66     private View.OnKeyListener mKeyListener = (view, i, keyEvent) -> {
67         if (keyEvent.getAction() == KeyEvent.ACTION_UP
68                 && keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK) {
69             mController.collapseStack();
70             return true;
71         }
72         return false;
73     };
74 
75     private class OverflowGridLayoutManager extends GridLayoutManager {
OverflowGridLayoutManager(Context context, int columns)76         OverflowGridLayoutManager(Context context, int columns) {
77             super(context, columns);
78         }
79 
80 //        @Override
81 //        public boolean canScrollVertically() {
82 //            // TODO (b/162006693): this should be based on items in the list & available height
83 //            return true;
84 //        }
85 
86         @Override
getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)87         public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
88                 RecyclerView.State state) {
89             int bubbleCount = state.getItemCount();
90             int columnCount = super.getColumnCountForAccessibility(recycler, state);
91             if (bubbleCount < columnCount) {
92                 // If there are 4 columns and bubbles <= 3,
93                 // TalkBack says "AppName 1 of 4 in list 4 items"
94                 // This is a workaround until TalkBack bug is fixed for GridLayoutManager
95                 return bubbleCount;
96             }
97             return columnCount;
98         }
99     }
100 
BubbleOverflowContainerView(Context context)101     public BubbleOverflowContainerView(Context context) {
102         this(context, null);
103     }
104 
BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs)105     public BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs) {
106         this(context, attrs, 0);
107     }
108 
BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)109     public BubbleOverflowContainerView(Context context, @Nullable AttributeSet attrs,
110             int defStyleAttr) {
111         this(context, attrs, defStyleAttr, 0);
112     }
113 
BubbleOverflowContainerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)114     public BubbleOverflowContainerView(Context context, AttributeSet attrs, int defStyleAttr,
115             int defStyleRes) {
116         super(context, attrs, defStyleAttr, defStyleRes);
117         setFocusableInTouchMode(true);
118     }
119 
setBubbleController(BubbleController controller)120     public void setBubbleController(BubbleController controller) {
121         mController = controller;
122     }
123 
show()124     public void show() {
125         requestFocus();
126         updateOverflow();
127     }
128 
129     @Override
onFinishInflate()130     protected void onFinishInflate() {
131         super.onFinishInflate();
132 
133         mRecyclerView = findViewById(R.id.bubble_overflow_recycler);
134         mEmptyState = findViewById(R.id.bubble_overflow_empty_state);
135         mEmptyStateTitle = findViewById(R.id.bubble_overflow_empty_title);
136         mEmptyStateSubtitle = findViewById(R.id.bubble_overflow_empty_subtitle);
137         mEmptyStateImage = findViewById(R.id.bubble_overflow_empty_state_image);
138     }
139 
140     @Override
onAttachedToWindow()141     protected void onAttachedToWindow() {
142         super.onAttachedToWindow();
143         if (mController != null) {
144             // For the overflow to get key events (e.g. back press) we need to adjust the flags
145             mController.updateWindowFlagsForOverflow(true);
146         }
147         setOnKeyListener(mKeyListener);
148     }
149 
150     @Override
onDetachedFromWindow()151     protected void onDetachedFromWindow() {
152         super.onDetachedFromWindow();
153         if (mController != null) {
154             mController.updateWindowFlagsForOverflow(false);
155         }
156         setOnKeyListener(null);
157     }
158 
updateOverflow()159     void updateOverflow() {
160         Resources res = getResources();
161         final int columns = res.getInteger(R.integer.bubbles_overflow_columns);
162         mRecyclerView.setLayoutManager(
163                 new OverflowGridLayoutManager(getContext(), columns));
164         mAdapter = new BubbleOverflowAdapter(getContext(), mOverflowBubbles,
165                 mController::promoteBubbleFromOverflow,
166                 mController.getPositioner());
167         mRecyclerView.setAdapter(mAdapter);
168 
169         mOverflowBubbles.clear();
170         mOverflowBubbles.addAll(mController.getOverflowBubbles());
171         mAdapter.notifyDataSetChanged();
172 
173         mController.setOverflowListener(mDataListener);
174         updateEmptyStateVisibility();
175         updateTheme();
176     }
177 
updateEmptyStateVisibility()178     void updateEmptyStateVisibility() {
179         mEmptyState.setVisibility(mOverflowBubbles.isEmpty() ? View.VISIBLE : View.GONE);
180         mRecyclerView.setVisibility(mOverflowBubbles.isEmpty() ? View.GONE : View.VISIBLE);
181     }
182 
183     /**
184      * Handle theme changes.
185      */
updateTheme()186     void updateTheme() {
187         Resources res = getResources();
188         final int mode = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
189         final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES);
190 
191         mEmptyStateImage.setImageDrawable(isNightMode
192                 ? res.getDrawable(R.drawable.bubble_ic_empty_overflow_dark)
193                 : res.getDrawable(R.drawable.bubble_ic_empty_overflow_light));
194 
195         findViewById(R.id.bubble_overflow_container)
196                 .setBackgroundColor(isNightMode
197                         ? res.getColor(R.color.bubbles_dark)
198                         : res.getColor(R.color.bubbles_light));
199 
200         final TypedArray typedArray = getContext().obtainStyledAttributes(new int[] {
201                 android.R.attr.colorBackgroundFloating,
202                 android.R.attr.textColorSecondary});
203         int bgColor = typedArray.getColor(0, isNightMode ? Color.BLACK : Color.WHITE);
204         int textColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK);
205         textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, isNightMode);
206         typedArray.recycle();
207         setBackgroundColor(bgColor);
208         mEmptyStateTitle.setTextColor(textColor);
209         mEmptyStateSubtitle.setTextColor(textColor);
210     }
211 
updateFontSize()212     public void updateFontSize() {
213         final float fontSize = mContext.getResources()
214                 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material);
215         mEmptyStateTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
216         mEmptyStateSubtitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
217     }
218 
219     private final BubbleData.Listener mDataListener = new BubbleData.Listener() {
220 
221         @Override
222         public void applyUpdate(BubbleData.Update update) {
223 
224             Bubble toRemove = update.removedOverflowBubble;
225             if (toRemove != null) {
226                 if (DEBUG_OVERFLOW) {
227                     Log.d(TAG, "remove: " + toRemove);
228                 }
229                 toRemove.cleanupViews();
230                 final int indexToRemove = mOverflowBubbles.indexOf(toRemove);
231                 mOverflowBubbles.remove(toRemove);
232                 mAdapter.notifyItemRemoved(indexToRemove);
233             }
234 
235             Bubble toAdd = update.addedOverflowBubble;
236             if (toAdd != null) {
237                 final int indexToAdd = mOverflowBubbles.indexOf(toAdd);
238                 if (DEBUG_OVERFLOW) {
239                     Log.d(TAG, "add: " + toAdd + " prevIndex: " + indexToAdd);
240                 }
241                 if (indexToAdd > 0) {
242                     mOverflowBubbles.remove(toAdd);
243                     mOverflowBubbles.add(0, toAdd);
244                     mAdapter.notifyItemMoved(indexToAdd, 0);
245                 } else {
246                     mOverflowBubbles.add(0, toAdd);
247                     mAdapter.notifyItemInserted(0);
248                 }
249             }
250 
251             updateEmptyStateVisibility();
252 
253             if (DEBUG_OVERFLOW) {
254                 Log.d(TAG, BubbleDebugConfig.formatBubblesString(
255                         mController.getOverflowBubbles(), null));
256             }
257         }
258     };
259 }
260 
261 class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> {
262     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowAdapter" : TAG_BUBBLES;
263 
264     private Context mContext;
265     private Consumer<Bubble> mPromoteBubbleFromOverflow;
266     private BubblePositioner mPositioner;
267     private List<Bubble> mBubbles;
268 
BubbleOverflowAdapter(Context context, List<Bubble> list, Consumer<Bubble> promoteBubble, BubblePositioner positioner)269     BubbleOverflowAdapter(Context context,
270             List<Bubble> list,
271             Consumer<Bubble> promoteBubble,
272             BubblePositioner positioner) {
273         mContext = context;
274         mBubbles = list;
275         mPromoteBubbleFromOverflow = promoteBubble;
276         mPositioner = positioner;
277     }
278 
279     @Override
onCreateViewHolder(ViewGroup parent, int viewType)280     public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
281             int viewType) {
282 
283         // Set layout for overflow bubble view.
284         LinearLayout overflowView = (LinearLayout) LayoutInflater.from(parent.getContext())
285                 .inflate(R.layout.bubble_overflow_view, parent, false);
286         LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
287                 LinearLayout.LayoutParams.WRAP_CONTENT,
288                 LinearLayout.LayoutParams.WRAP_CONTENT);
289         overflowView.setLayoutParams(params);
290 
291         // Ensure name has enough contrast.
292         final TypedArray ta = mContext.obtainStyledAttributes(
293                 new int[]{android.R.attr.colorBackgroundFloating, android.R.attr.textColorPrimary});
294         final int bgColor = ta.getColor(0, Color.WHITE);
295         int textColor = ta.getColor(1, Color.BLACK);
296         textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
297         ta.recycle();
298 
299         TextView viewName = overflowView.findViewById(R.id.bubble_view_name);
300         viewName.setTextColor(textColor);
301 
302         return new ViewHolder(overflowView, mPositioner);
303     }
304 
305     @Override
onBindViewHolder(ViewHolder vh, int index)306     public void onBindViewHolder(ViewHolder vh, int index) {
307         Bubble b = mBubbles.get(index);
308 
309         vh.iconView.setRenderedBubble(b);
310         vh.iconView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
311         vh.iconView.setOnClickListener(view -> {
312             mBubbles.remove(b);
313             notifyDataSetChanged();
314             mPromoteBubbleFromOverflow.accept(b);
315         });
316 
317         String titleStr = b.getTitle();
318         if (titleStr == null) {
319             titleStr = mContext.getResources().getString(R.string.notification_bubble_title);
320         }
321         vh.iconView.setContentDescription(mContext.getResources().getString(
322                 R.string.bubble_content_description_single, titleStr, b.getAppName()));
323 
324         vh.iconView.setAccessibilityDelegate(
325                 new View.AccessibilityDelegate() {
326                     @Override
327                     public void onInitializeAccessibilityNodeInfo(View host,
328                             AccessibilityNodeInfo info) {
329                         super.onInitializeAccessibilityNodeInfo(host, info);
330                         // Talkback prompts "Double tap to add back to stack"
331                         // instead of the default "Double tap to activate"
332                         info.addAction(
333                                 new AccessibilityNodeInfo.AccessibilityAction(
334                                         AccessibilityNodeInfo.ACTION_CLICK,
335                                         mContext.getResources().getString(
336                                                 R.string.bubble_accessibility_action_add_back)));
337                     }
338                 });
339 
340         CharSequence label = b.getShortcutInfo() != null
341                 ? b.getShortcutInfo().getLabel()
342                 : b.getAppName();
343         vh.textView.setText(label);
344     }
345 
346     @Override
getItemCount()347     public int getItemCount() {
348         return mBubbles.size();
349     }
350 
351     public static class ViewHolder extends RecyclerView.ViewHolder {
352         public BadgedImageView iconView;
353         public TextView textView;
354 
ViewHolder(LinearLayout v, BubblePositioner positioner)355         ViewHolder(LinearLayout v, BubblePositioner positioner) {
356             super(v);
357             iconView = v.findViewById(R.id.bubble_view);
358             iconView.initialize(positioner);
359             textView = v.findViewById(R.id.bubble_view_name);
360         }
361     }
362 }