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