1 /*
2  * Copyright (C) 2017 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.systemui.statusbar.phone;
18 
19 import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT;
20 import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
21 import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON;
22 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.pm.ActivityInfo;
26 import android.content.res.Configuration;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Paint.Style;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.view.View;
34 
35 import com.android.keyguard.AlphaOptimizedLinearLayout;
36 import com.android.systemui.res.R;
37 import com.android.systemui.statusbar.StatusIconDisplayable;
38 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
39 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
40 import com.android.systemui.statusbar.notification.stack.ViewState;
41 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
42 
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 /**
47  * A container for Status bar system icons. Limits the number of system icons and handles overflow
48  * similar to {@link NotificationIconContainer}.
49  *
50  * Children are expected to implement {@link StatusIconDisplayable}
51  */
52 public class StatusIconContainer extends AlphaOptimizedLinearLayout {
53 
54     private static final String TAG = "StatusIconContainer";
55     private static final boolean DEBUG = false;
56     private static final boolean DEBUG_OVERFLOW = false;
57     // Max 8 status icons including battery
58     private static final int MAX_ICONS = 7;
59     private static final int MAX_DOTS = 1;
60 
61     private int mDotPadding;
62     private int mIconSpacing;
63     private int mStaticDotDiameter;
64     private int mUnderflowWidth;
65     private int mUnderflowStart = 0;
66     // Whether or not we can draw into the underflow space
67     private boolean mNeedsUnderflow;
68     // Individual StatusBarIconViews draw their etc dots centered in this width
69     private int mIconDotFrameWidth;
70     private boolean mQsExpansionTransitioning;
71     private boolean mShouldRestrictIcons = true;
72     // Used to count which states want to be visible during layout
73     private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
74     // So we can count and measure properly
75     private ArrayList<View> mMeasureViews = new ArrayList<>();
76     // Any ignored icon will never be added as a child
77     private ArrayList<String> mIgnoredSlots = new ArrayList<>();
78 
79     private Configuration mConfiguration;
80 
StatusIconContainer(Context context)81     public StatusIconContainer(Context context) {
82         this(context, null);
83     }
84 
StatusIconContainer(Context context, AttributeSet attrs)85     public StatusIconContainer(Context context, AttributeSet attrs) {
86         super(context, attrs);
87         mConfiguration = new Configuration(context.getResources().getConfiguration());
88         reloadDimens();
89         setWillNotDraw(!DEBUG_OVERFLOW);
90     }
91 
92     @Override
onFinishInflate()93     protected void onFinishInflate() {
94         super.onFinishInflate();
95     }
96 
setQsExpansionTransitioning(boolean expansionTransitioning)97     public void setQsExpansionTransitioning(boolean expansionTransitioning) {
98         mQsExpansionTransitioning = expansionTransitioning;
99     }
100 
setShouldRestrictIcons(boolean should)101     public void setShouldRestrictIcons(boolean should) {
102         mShouldRestrictIcons = should;
103     }
104 
isRestrictingIcons()105     public boolean isRestrictingIcons() {
106         return mShouldRestrictIcons;
107     }
108 
reloadDimens()109     private void reloadDimens() {
110         // This is the same value that StatusBarIconView uses
111         mIconDotFrameWidth = getResources().getDimensionPixelSize(
112                 com.android.internal.R.dimen.status_bar_icon_size_sp);
113         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
114         mIconSpacing = getResources().getDimensionPixelSize(R.dimen.status_bar_system_icon_spacing);
115         int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
116         mStaticDotDiameter = 2 * radius;
117         mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
118     }
119 
120     @Override
onLayout(boolean changed, int l, int t, int r, int b)121     protected void onLayout(boolean changed, int l, int t, int r, int b) {
122         float midY = getHeight() / 2.0f;
123 
124         // Layout all child views so that we can move them around later
125         for (int i = 0; i < getChildCount(); i++) {
126             View child = getChildAt(i);
127             int width = child.getMeasuredWidth();
128             int height = child.getMeasuredHeight();
129             int top = (int) (midY - height / 2.0f);
130             child.layout(0, top, width, top + height);
131         }
132 
133         resetViewStates();
134         calculateIconTranslations();
135         applyIconStates();
136     }
137 
138     @Override
onDraw(Canvas canvas)139     protected void onDraw(Canvas canvas) {
140         super.onDraw(canvas);
141         if (DEBUG_OVERFLOW) {
142             Paint paint = new Paint();
143             paint.setStyle(Style.STROKE);
144             paint.setColor(Color.RED);
145 
146             // Show bounding box
147             canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
148 
149             // Show etc box
150             paint.setColor(Color.GREEN);
151             canvas.drawRect(
152                     mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
153         }
154     }
155 
156     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)157     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
158         mMeasureViews.clear();
159         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
160         final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
161         final int count = getChildCount();
162         // Collect all of the views which want to be laid out
163         for (int i = 0; i < count; i++) {
164             StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
165             if (icon.isIconVisible() && !icon.isIconBlocked()
166                     && !mIgnoredSlots.contains(icon.getSlot())) {
167                 mMeasureViews.add((View) icon);
168             }
169         }
170 
171         int visibleCount = mMeasureViews.size();
172         int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
173         int totalWidth = mPaddingLeft + mPaddingRight;
174         boolean trackWidth = true;
175 
176         // Measure all children so that they report the correct width
177         int childWidthSpec = MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.UNSPECIFIED);
178         mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
179         for (int i = 0; i < visibleCount; i++) {
180             // Walking backwards
181             View child = mMeasureViews.get(visibleCount - i - 1);
182             measureChild(child, childWidthSpec, heightMeasureSpec);
183             int spacing = i == visibleCount - 1 ? 0 : mIconSpacing;
184             if (mShouldRestrictIcons) {
185                 if (i < maxVisible && trackWidth) {
186                     totalWidth += getViewTotalMeasuredWidth(child) + spacing;
187                 } else if (trackWidth) {
188                     // We've hit the icon limit; add space for dots
189                     totalWidth += mUnderflowWidth;
190                     trackWidth = false;
191                 }
192             } else {
193                 totalWidth += getViewTotalMeasuredWidth(child) + spacing;
194             }
195         }
196         setMeasuredDimension(
197                 getMeasuredWidth(widthMode, specWidth, totalWidth),
198                 getMeasuredHeight(heightMeasureSpec, mMeasureViews));
199     }
200 
getMeasuredHeight(int heightMeasureSpec, List<View> measuredChildren)201     private int getMeasuredHeight(int heightMeasureSpec, List<View> measuredChildren) {
202         if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
203             return MeasureSpec.getSize(heightMeasureSpec);
204         } else {
205             int highest = 0;
206             for (View child : measuredChildren) {
207                 highest = Math.max(child.getMeasuredHeight(), highest);
208             }
209             return highest + getPaddingTop() + getPaddingBottom();
210         }
211     }
212 
getMeasuredWidth(int widthMode, int specWidth, int totalWidth)213     private int getMeasuredWidth(int widthMode, int specWidth, int totalWidth) {
214         if (widthMode == MeasureSpec.EXACTLY) {
215             if (!mNeedsUnderflow && totalWidth > specWidth) {
216                 mNeedsUnderflow = true;
217             }
218             return specWidth;
219         } else {
220             if (widthMode == MeasureSpec.AT_MOST && totalWidth > specWidth) {
221                 mNeedsUnderflow = true;
222                 totalWidth = specWidth;
223             }
224             return totalWidth;
225         }
226     }
227 
228     @Override
onViewAdded(View child)229     public void onViewAdded(View child) {
230         super.onViewAdded(child);
231         StatusIconState vs = new StatusIconState();
232         vs.justAdded = true;
233         child.setTag(R.id.status_bar_view_state_tag, vs);
234     }
235 
236     @Override
onViewRemoved(View child)237     public void onViewRemoved(View child) {
238         super.onViewRemoved(child);
239         child.setTag(R.id.status_bar_view_state_tag, null);
240     }
241 
242     @Override
onConfigurationChanged(Configuration newConfig)243     protected void onConfigurationChanged(Configuration newConfig) {
244         super.onConfigurationChanged(newConfig);
245         final int configDiff = newConfig.diff(mConfiguration);
246         mConfiguration.setTo(newConfig);
247         if ((configDiff & (ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_FONT_SCALE)) != 0) {
248             reloadDimens();
249         }
250     }
251 
252     /**
253      * Add a name of an icon slot to be ignored. It will not show up nor be measured
254      * @param slotName name of the icon as it exists in
255      * frameworks/base/core/res/res/values/config.xml
256      */
addIgnoredSlot(String slotName)257     public void addIgnoredSlot(String slotName) {
258         boolean added = addIgnoredSlotInternal(slotName);
259         if (added) {
260             requestLayout();
261         }
262     }
263 
264     /**
265      * Add a list of slots to be ignored
266      * @param slots names of the icons to ignore
267      */
addIgnoredSlots(List<String> slots)268     public void addIgnoredSlots(List<String> slots) {
269         boolean willAddAny = false;
270         for (String slot : slots) {
271             willAddAny |= addIgnoredSlotInternal(slot);
272         }
273 
274         if (willAddAny) {
275             requestLayout();
276         }
277     }
278 
279     /**
280      *
281      * @param slotName
282      * @return
283      */
addIgnoredSlotInternal(String slotName)284     private boolean addIgnoredSlotInternal(String slotName) {
285         if (mIgnoredSlots.contains(slotName)) {
286             return false;
287         }
288         mIgnoredSlots.add(slotName);
289         return true;
290     }
291 
292     /**
293      * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible
294      * by the {@link StatusBarIconController}.
295      * @param slotName name of the icon slot to remove from the ignored list
296      */
removeIgnoredSlot(String slotName)297     public void removeIgnoredSlot(String slotName) {
298         boolean removed = mIgnoredSlots.remove(slotName);
299         if (removed) {
300             requestLayout();
301         }
302     }
303 
304     /**
305      * Remove a list of slots from the list of ignored icon slots.
306      * It will then be shown when set to visible by the {@link StatusBarIconController}.
307      * @param slots name of the icon slots to remove from the ignored list
308      */
removeIgnoredSlots(List<String> slots)309     public void removeIgnoredSlots(List<String> slots) {
310         boolean removedAny = false;
311         for (String slot : slots) {
312             removedAny |= mIgnoredSlots.remove(slot);
313         }
314 
315         if (removedAny) {
316             requestLayout();
317         }
318     }
319 
320     /**
321      * Layout is happening from end -> start
322      */
calculateIconTranslations()323     private void calculateIconTranslations() {
324         mLayoutStates.clear();
325         float width = getWidth();
326         float translationX = width - getPaddingEnd();
327         float contentStart = getPaddingStart();
328         int childCount = getChildCount();
329         // Underflow === don't show content until that index
330         if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX
331                 + " width=" + width + " underflow=" + mNeedsUnderflow);
332 
333         // Collect all of the states which want to be visible
334         for (int i = childCount - 1; i >= 0; i--) {
335             View child = getChildAt(i);
336             StatusIconDisplayable iconView = (StatusIconDisplayable) child;
337             StatusIconState childState = getViewStateFromChild(child);
338 
339             if (!iconView.isIconVisible() || iconView.isIconBlocked()
340                     || mIgnoredSlots.contains(iconView.getSlot())) {
341                 childState.visibleState = STATE_HIDDEN;
342                 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
343                 continue;
344             }
345 
346             // Move translationX to the spot within StatusIconContainer's layout to add the view
347             // without cutting off the child view.
348             translationX -= getViewTotalWidth(child);
349             childState.visibleState = STATE_ICON;
350             childState.setXTranslation(translationX);
351             mLayoutStates.add(0, childState);
352 
353             // Shift translationX over by mIconSpacing for the next view.
354             translationX -= mIconSpacing;
355         }
356 
357         // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
358         int totalVisible = mLayoutStates.size();
359         int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
360 
361         // Init mUnderflowStart value with the offset to let the dot be placed next to battery icon.
362         // This is to prevent if the underflow happens at rightest(totalVisible - 1) child then
363         // break the for loop with mUnderflowStart staying 0(initial value), causing the dot be
364         // placed at the leftest side.
365         mUnderflowStart = (int) Math.max(contentStart, width - getPaddingEnd() - mUnderflowWidth);
366         int visible = 0;
367         int firstUnderflowIndex = -1;
368         for (int i = totalVisible - 1; i >= 0; i--) {
369             StatusIconState state = mLayoutStates.get(i);
370             // Allow room for underflow if we found we need it in onMeasure
371             if ((mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth)))
372                     || (mShouldRestrictIcons && (visible >= maxVisible))) {
373                 firstUnderflowIndex = i;
374                 break;
375             }
376             mUnderflowStart = (int) Math.max(
377                     contentStart, state.getXTranslation() - mUnderflowWidth - mIconSpacing);
378             visible++;
379         }
380 
381         if (firstUnderflowIndex != -1) {
382             int totalDots = 0;
383             int dotWidth = mStaticDotDiameter + mDotPadding;
384             int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
385             for (int i = firstUnderflowIndex; i >= 0; i--) {
386                 StatusIconState state = mLayoutStates.get(i);
387                 if (totalDots < MAX_DOTS) {
388                     state.setXTranslation(dotOffset);
389                     state.visibleState = STATE_DOT;
390                     dotOffset -= dotWidth;
391                     totalDots++;
392                 } else {
393                     state.visibleState = STATE_HIDDEN;
394                 }
395             }
396         }
397 
398         // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
399         if (isLayoutRtl()) {
400             for (int i = 0; i < childCount; i++) {
401                 View child = getChildAt(i);
402                 StatusIconState state = getViewStateFromChild(child);
403                 state.setXTranslation(width - state.getXTranslation() - child.getWidth());
404             }
405         }
406     }
407 
applyIconStates()408     private void applyIconStates() {
409         for (int i = 0; i < getChildCount(); i++) {
410             View child = getChildAt(i);
411             StatusIconState vs = getViewStateFromChild(child);
412             if (vs != null) {
413                 vs.applyToView(child);
414                 vs.qsExpansionTransitioning = mQsExpansionTransitioning;
415             }
416         }
417     }
418 
resetViewStates()419     private void resetViewStates() {
420         for (int i = 0; i < getChildCount(); i++) {
421             View child = getChildAt(i);
422             StatusIconState vs = getViewStateFromChild(child);
423             if (vs == null) {
424                 continue;
425             }
426 
427             vs.initFrom(child);
428             vs.setAlpha(1.0f);
429             vs.hidden = false;
430         }
431     }
432 
getViewStateFromChild(View child)433     private static @Nullable StatusIconState getViewStateFromChild(View child) {
434         return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
435     }
436 
getViewTotalMeasuredWidth(View child)437     private static int getViewTotalMeasuredWidth(View child) {
438         return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
439     }
440 
getViewTotalWidth(View child)441     private static int getViewTotalWidth(View child) {
442         return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
443     }
444 
445     public static class StatusIconState extends ViewState {
446         /// StatusBarIconView.STATE_*
447         public int visibleState = STATE_ICON;
448         public boolean justAdded = true;
449         public boolean qsExpansionTransitioning = false;
450 
451         // How far we are from the end of the view actually is the most relevant for animation
452         float distanceToViewEnd = -1;
453 
454         @Override
applyToView(View view)455         public void applyToView(View view) {
456             float parentWidth = 0;
457             if (view.getParent() instanceof View) {
458                 parentWidth = ((View) view.getParent()).getWidth();
459             }
460 
461             float currentDistanceToEnd = parentWidth - getXTranslation();
462 
463             if (!(view instanceof StatusIconDisplayable)) {
464                 return;
465             }
466             StatusIconDisplayable icon = (StatusIconDisplayable) view;
467             AnimationProperties animationProperties = null;
468             boolean animateVisibility = true;
469 
470             // Figure out which properties of the state transition (if any) we need to animate
471             if (justAdded
472                     || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
473                 // Icon is appearing, fade it in by putting it where it will be and animating alpha
474                 super.applyToView(view);
475                 view.setAlpha(0.f);
476                 icon.setVisibleState(STATE_HIDDEN);
477                 animationProperties = ADD_ICON_PROPERTIES;
478             } else if (icon.getVisibleState() != visibleState) {
479                 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) {
480                     // Disappearing, don't do anything fancy
481                     animateVisibility = false;
482                 } else {
483                     // all other transitions (to/from dot, etc)
484                     animationProperties = ANIMATE_ALL_PROPERTIES;
485                 }
486             } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) {
487                 // Visibility isn't changing, just animate position
488                 animationProperties = X_ANIMATION_PROPERTIES;
489             }
490 
491             icon.setVisibleState(visibleState, animateVisibility);
492             if (animationProperties != null && !qsExpansionTransitioning) {
493                 animateTo(view, animationProperties);
494             } else {
495                 super.applyToView(view);
496             }
497 
498             qsExpansionTransitioning = false;
499             justAdded = false;
500             distanceToViewEnd = currentDistanceToEnd;
501 
502         }
503     }
504 
505     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
506         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
507 
508         @Override
509         public AnimationFilter getAnimationFilter() {
510             return mAnimationFilter;
511         }
512     }.setDuration(200).setDelay(50);
513 
514     private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() {
515         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
516 
517         @Override
518         public AnimationFilter getAnimationFilter() {
519             return mAnimationFilter;
520         }
521     }.setDuration(200);
522 
523     private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() {
524         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY()
525                 .animateAlpha().animateScale();
526 
527         @Override
528         public AnimationFilter getAnimationFilter() {
529             return mAnimationFilter;
530         }
531     }.setDuration(200);
532 }
533