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