blob: 653471d8a220cdb12a8eb469df412bc75303f714 [file] [log] [blame]
Selim Cinek281c2022016-10-13 19:14:43 -07001/*
2 * Copyright (C) 2016 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
17package com.android.systemui.statusbar.phone;
18
Selim Cinek332c23f2018-03-16 17:37:50 -070019import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DURATION;
20import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DELAY;
21
Selim Cinek281c2022016-10-13 19:14:43 -070022import android.content.Context;
Selim Cinek49014f82016-11-04 14:55:30 -070023import android.content.res.Configuration;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.Paint;
Selim Cinekaa9db1f2018-02-27 17:35:47 -080027import android.graphics.Rect;
Selim Cinek72fc8db2017-06-06 18:07:47 -070028import android.graphics.drawable.Icon;
Aurimas Liutikasa14377a2018-04-17 09:50:46 -070029import androidx.collection.ArrayMap;
Selim Cinek281c2022016-10-13 19:14:43 -070030import android.util.AttributeSet;
31import android.view.View;
32
Selim Cinek72fc8db2017-06-06 18:07:47 -070033import com.android.internal.statusbar.StatusBarIcon;
Selim Cinek061d9072016-12-20 13:17:03 +010034import com.android.systemui.Interpolators;
Selim Cinek49014f82016-11-04 14:55:30 -070035import com.android.systemui.R;
Selim Cinek281c2022016-10-13 19:14:43 -070036import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
Selim Cinek49014f82016-11-04 14:55:30 -070037import com.android.systemui.statusbar.StatusBarIconView;
38import com.android.systemui.statusbar.stack.AnimationFilter;
39import com.android.systemui.statusbar.stack.AnimationProperties;
Selim Cinek281c2022016-10-13 19:14:43 -070040import com.android.systemui.statusbar.stack.ViewState;
41
Selim Cinek72fc8db2017-06-06 18:07:47 -070042import java.util.ArrayList;
Selim Cinek65d418e2016-11-29 15:42:34 -080043import java.util.HashMap;
Selim Cinek281c2022016-10-13 19:14:43 -070044
45/**
46 * A container for notification icons. It handles overflowing icons properly and positions them
47 * correctly on the screen.
48 */
49public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
Selim Cinek932005d2016-12-05 17:12:09 -080050 /**
51 * A float value indicating how much before the overflow start the icons should transform into
52 * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
53 * 1 icon width early.
54 */
55 public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
56 private static final int NO_VALUE = Integer.MIN_VALUE;
Selim Cinek281c2022016-10-13 19:14:43 -070057 private static final String TAG = "NotificationIconContainer";
Selim Cinek49014f82016-11-04 14:55:30 -070058 private static final boolean DEBUG = false;
Evan Laird8cf0de42018-02-06 18:34:55 -050059 private static final boolean DEBUG_OVERFLOW = false;
Selim Cinek2b549f42016-11-22 16:38:51 -080060 private static final int CANNED_ANIMATION_DURATION = 100;
Selim Cinek49014f82016-11-04 14:55:30 -070061 private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
Selim Cinek5b5beb012016-11-08 18:11:58 -080062 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
63
Selim Cinek49014f82016-11-04 14:55:30 -070064 @Override
65 public AnimationFilter getAnimationFilter() {
Selim Cinek5b5beb012016-11-08 18:11:58 -080066 return mAnimationFilter;
Selim Cinek49014f82016-11-04 14:55:30 -070067 }
68 }.setDuration(200);
Selim Cinekb3dadcc2016-11-21 17:21:13 -080069
Selim Cinek2b549f42016-11-22 16:38:51 -080070 private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
Selim Cinekf082fe22016-12-20 14:32:19 +010071 private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha()
72 .animateScale();
Selim Cinek2b549f42016-11-22 16:38:51 -080073
74 @Override
75 public AnimationFilter getAnimationFilter() {
76 return mAnimationFilter;
77 }
Selim Cinek061d9072016-12-20 13:17:03 +010078
79 }.setDuration(CANNED_ANIMATION_DURATION)
80 .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT);
Selim Cinek2b549f42016-11-22 16:38:51 -080081
Lucas Dupin8274aa92017-09-13 17:24:09 -070082 /**
83 * Temporary AnimationProperties to avoid unnecessary allocations.
84 */
85 private static final AnimationProperties sTempProperties = new AnimationProperties() {
Selim Cinek2b549f42016-11-22 16:38:51 -080086 private AnimationFilter mAnimationFilter = new AnimationFilter();
Selim Cinek2b549f42016-11-22 16:38:51 -080087
88 @Override
89 public AnimationFilter getAnimationFilter() {
90 return mAnimationFilter;
91 }
Selim Cinekf082fe22016-12-20 14:32:19 +010092 };
Selim Cinek2b549f42016-11-22 16:38:51 -080093
Selim Cinek5b5beb012016-11-08 18:11:58 -080094 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
95 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
96
97 @Override
98 public AnimationFilter getAnimationFilter() {
99 return mAnimationFilter;
100 }
101 }.setDuration(200).setDelay(50);
Adrian Roos28f90c72017-05-08 17:24:26 -0700102
Selim Cinekd03518c2018-03-15 12:13:51 -0700103 /**
104 * The animation property used for all icons that were not isolated, when the isolation ends.
105 * This just fades the alpha and doesn't affect the movement and has a delay.
106 */
107 private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS
108 = new AnimationProperties() {
109 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
110
111 @Override
112 public AnimationFilter getAnimationFilter() {
113 return mAnimationFilter;
114 }
Selim Cinek332c23f2018-03-16 17:37:50 -0700115 }.setDuration(CONTENT_FADE_DURATION);
Selim Cinekd03518c2018-03-15 12:13:51 -0700116
117 /**
118 * The animation property used for the icon when its isolation ends.
119 * This animates the translation back to the right position.
120 */
121 private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() {
122 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
123
124 @Override
125 public AnimationFilter getAnimationFilter() {
126 return mAnimationFilter;
127 }
Selim Cinek332c23f2018-03-16 17:37:50 -0700128 }.setDuration(CONTENT_FADE_DURATION);
Selim Cinekd03518c2018-03-15 12:13:51 -0700129
Adrian Roos14d70cb2017-04-25 16:46:49 -0700130 public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5;
Evan Lairdc987fc72017-12-15 10:14:22 -0500131 public static final int MAX_STATIC_ICONS = 4;
Evan Laird6f9e8562018-05-22 19:14:50 -0400132 private static final int MAX_DOTS = 1;
Selim Cinek281c2022016-10-13 19:14:43 -0700133
Evan Lairdc987fc72017-12-15 10:14:22 -0500134 private boolean mIsStaticLayout = true;
Selim Cinek65d418e2016-11-29 15:42:34 -0800135 private final HashMap<View, IconState> mIconStates = new HashMap<>();
Selim Cinek49014f82016-11-04 14:55:30 -0700136 private int mDotPadding;
137 private int mStaticDotRadius;
Evan Laird8cf0de42018-02-06 18:34:55 -0500138 private int mStaticDotDiameter;
Evan Laird6f9e8562018-05-22 19:14:50 -0400139 private int mOverflowWidth;
Selim Cinek932005d2016-12-05 17:12:09 -0800140 private int mActualLayoutWidth = NO_VALUE;
141 private float mActualPaddingEnd = NO_VALUE;
142 private float mActualPaddingStart = NO_VALUE;
Adrian Roos456e0052017-04-04 16:44:29 -0700143 private boolean mDark;
Selim Cinek5b5beb012016-11-08 18:11:58 -0800144 private boolean mChangingViewPositions;
Selim Cinek2b549f42016-11-22 16:38:51 -0800145 private int mAddAnimationStartIndex = -1;
146 private int mCannedAnimationStartIndex = -1;
Selim Cinek17e1b692016-12-02 18:19:11 -0800147 private int mSpeedBumpIndex = -1;
148 private int mIconSize;
149 private float mOpenedAmount = 0.0f;
Adrian Roosff160ed2017-01-13 17:18:29 -0800150 private boolean mDisallowNextAnimation;
Selim Cinek09bd29d2017-02-03 15:30:28 -0800151 private boolean mAnimationsEnabled = true;
Selim Cinek72fc8db2017-06-06 18:07:47 -0700152 private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
Evan Lairdc987fc72017-12-15 10:14:22 -0500153 // Keep track of the last visible icon so collapsed container can report on its location
154 private IconState mLastVisibleIconState;
Lucas Dupin15475702018-04-04 14:32:53 -0700155 private IconState mFirstVisibleIconState;
Evan Laird8cf0de42018-02-06 18:34:55 -0500156 private float mVisualOverflowStart;
157 // Keep track of overflow in range [0, 3]
158 private int mNumDots;
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800159 private StatusBarIconView mIsolatedIcon;
160 private Rect mIsolatedIconLocation;
161 private int[] mAbsolutePosition = new int[2];
Selim Cinekd03518c2018-03-15 12:13:51 -0700162 private View mIsolatedIconForAnimation;
Evan Lairdc987fc72017-12-15 10:14:22 -0500163
Selim Cinek281c2022016-10-13 19:14:43 -0700164 public NotificationIconContainer(Context context, AttributeSet attrs) {
165 super(context, attrs);
Selim Cinek49014f82016-11-04 14:55:30 -0700166 initDimens();
Evan Laird8cf0de42018-02-06 18:34:55 -0500167 setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW));
Selim Cinek281c2022016-10-13 19:14:43 -0700168 }
169
Selim Cinek49014f82016-11-04 14:55:30 -0700170 private void initDimens() {
171 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
172 mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
Evan Laird8cf0de42018-02-06 18:34:55 -0500173 mStaticDotDiameter = 2 * mStaticDotRadius;
Selim Cinek49014f82016-11-04 14:55:30 -0700174 }
175
176 @Override
177 protected void onDraw(Canvas canvas) {
178 super.onDraw(canvas);
179 Paint paint = new Paint();
180 paint.setColor(Color.RED);
181 paint.setStyle(Paint.Style.STROKE);
182 canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
Evan Laird8cf0de42018-02-06 18:34:55 -0500183
184 if (DEBUG_OVERFLOW) {
185 if (mLastVisibleIconState == null) {
186 return;
187 }
188
189 int height = getHeight();
190 int end = getFinalTranslationX();
191
192 // Visualize the "end" of the layout
193 paint.setColor(Color.BLUE);
194 canvas.drawLine(end, 0, end, height, paint);
195
Lucas Dupin15475702018-04-04 14:32:53 -0700196 paint.setColor(Color.GREEN);
Evan Laird8cf0de42018-02-06 18:34:55 -0500197 int lastIcon = (int) mLastVisibleIconState.xTranslation;
198 canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
199
Lucas Dupin15475702018-04-04 14:32:53 -0700200 if (mFirstVisibleIconState != null) {
201 int firstIcon = (int) mFirstVisibleIconState.xTranslation;
202 canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
203 }
204
Evan Laird8cf0de42018-02-06 18:34:55 -0500205 paint.setColor(Color.RED);
206 canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
207
208 paint.setColor(Color.YELLOW);
209 float overflow = getMaxOverflowStart();
210 canvas.drawLine(overflow, 0, overflow, height, paint);
211 }
Selim Cinek49014f82016-11-04 14:55:30 -0700212 }
213
214 @Override
215 protected void onConfigurationChanged(Configuration newConfig) {
216 super.onConfigurationChanged(newConfig);
217 initDimens();
218 }
Lucas Dupin15475702018-04-04 14:32:53 -0700219
Selim Cinek281c2022016-10-13 19:14:43 -0700220 @Override
221 protected void onLayout(boolean changed, int l, int t, int r, int b) {
222 float centerY = getHeight() / 2.0f;
223 // we layout all our children on the left at the top
Selim Cinek17e1b692016-12-02 18:19:11 -0800224 mIconSize = 0;
Selim Cinek281c2022016-10-13 19:14:43 -0700225 for (int i = 0; i < getChildCount(); i++) {
226 View child = getChildAt(i);
227 // We need to layout all children even the GONE ones, such that the heights are
228 // calculated correctly as they are used to calculate how many we can fit on the screen
229 int width = child.getMeasuredWidth();
230 int height = child.getMeasuredHeight();
231 int top = (int) (centerY - height / 2.0f);
232 child.layout(0, top, width, top + height);
Selim Cinek17e1b692016-12-02 18:19:11 -0800233 if (i == 0) {
Evan Laird6f9e8562018-05-22 19:14:50 -0400234 setIconSize(child.getWidth());
Selim Cinek17e1b692016-12-02 18:19:11 -0800235 }
Selim Cinek281c2022016-10-13 19:14:43 -0700236 }
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800237 getLocationOnScreen(mAbsolutePosition);
Evan Lairdc987fc72017-12-15 10:14:22 -0500238 if (mIsStaticLayout) {
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800239 updateState();
Selim Cinek281c2022016-10-13 19:14:43 -0700240 }
241 }
242
Evan Laird6f9e8562018-05-22 19:14:50 -0400243 private void setIconSize(int size) {
244 mIconSize = size;
245 mOverflowWidth = mIconSize + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
246 }
247
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800248 private void updateState() {
249 resetViewStates();
250 calculateIconTranslations();
251 applyIconStates();
252 }
253
Selim Cinekc383fd02016-10-21 15:31:26 -0700254 public void applyIconStates() {
Selim Cinek281c2022016-10-13 19:14:43 -0700255 for (int i = 0; i < getChildCount(); i++) {
256 View child = getChildAt(i);
Selim Cinekc383fd02016-10-21 15:31:26 -0700257 ViewState childState = mIconStates.get(child);
Selim Cinek281c2022016-10-13 19:14:43 -0700258 if (childState != null) {
259 childState.applyToView(child);
260 }
261 }
Selim Cinek2b549f42016-11-22 16:38:51 -0800262 mAddAnimationStartIndex = -1;
263 mCannedAnimationStartIndex = -1;
Adrian Roosff160ed2017-01-13 17:18:29 -0800264 mDisallowNextAnimation = false;
Selim Cinekd03518c2018-03-15 12:13:51 -0700265 mIsolatedIconForAnimation = null;
Selim Cinek281c2022016-10-13 19:14:43 -0700266 }
267
268 @Override
269 public void onViewAdded(View child) {
270 super.onViewAdded(child);
Selim Cinek72fc8db2017-06-06 18:07:47 -0700271 boolean isReplacingIcon = isReplacingIcon(child);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800272 if (!mChangingViewPositions) {
Selim Cinek72fc8db2017-06-06 18:07:47 -0700273 IconState v = new IconState();
274 if (isReplacingIcon) {
275 v.justAdded = false;
276 v.justReplaced = true;
277 }
278 mIconStates.put(child, v);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800279 }
280 int childIndex = indexOfChild(child);
Selim Cinek72fc8db2017-06-06 18:07:47 -0700281 if (childIndex < getChildCount() - 1 && !isReplacingIcon
Selim Cinek5b5beb012016-11-08 18:11:58 -0800282 && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
Selim Cinek2b549f42016-11-22 16:38:51 -0800283 if (mAddAnimationStartIndex < 0) {
284 mAddAnimationStartIndex = childIndex;
Selim Cinek5b5beb012016-11-08 18:11:58 -0800285 } else {
Selim Cinek2b549f42016-11-22 16:38:51 -0800286 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800287 }
288 }
Selim Cinek04adab52017-11-28 18:22:12 +0100289 if (child instanceof StatusBarIconView) {
Adrian Roos456e0052017-04-04 16:44:29 -0700290 ((StatusBarIconView) child).setDark(mDark, false, 0);
291 }
Selim Cinek281c2022016-10-13 19:14:43 -0700292 }
293
Selim Cinek72fc8db2017-06-06 18:07:47 -0700294 private boolean isReplacingIcon(View child) {
295 if (mReplacingIcons == null) {
296 return false;
297 }
298 if (!(child instanceof StatusBarIconView)) {
299 return false;
300 }
301 StatusBarIconView iconView = (StatusBarIconView) child;
302 Icon sourceIcon = iconView.getSourceIcon();
303 String groupKey = iconView.getNotification().getGroupKey();
304 ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
305 if (statusBarIcons != null) {
306 StatusBarIcon replacedIcon = statusBarIcons.get(0);
307 if (sourceIcon.sameAs(replacedIcon.icon)) {
308 return true;
309 }
310 }
311 return false;
312 }
313
Selim Cinek281c2022016-10-13 19:14:43 -0700314 @Override
315 public void onViewRemoved(View child) {
316 super.onViewRemoved(child);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800317 if (child instanceof StatusBarIconView) {
Selim Cinek72fc8db2017-06-06 18:07:47 -0700318 boolean isReplacingIcon = isReplacingIcon(child);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800319 final StatusBarIconView icon = (StatusBarIconView) child;
320 if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
Selim Cinek72fc8db2017-06-06 18:07:47 -0700321 && child.getVisibility() == VISIBLE && isReplacingIcon) {
Selim Cinek5b5beb012016-11-08 18:11:58 -0800322 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
Selim Cinek2b549f42016-11-22 16:38:51 -0800323 if (mAddAnimationStartIndex < 0) {
324 mAddAnimationStartIndex = animationStartIndex;
Selim Cinek5b5beb012016-11-08 18:11:58 -0800325 } else {
Selim Cinek2b549f42016-11-22 16:38:51 -0800326 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
Selim Cinek5b5beb012016-11-08 18:11:58 -0800327 }
328 }
329 if (!mChangingViewPositions) {
330 mIconStates.remove(child);
Selim Cinek72fc8db2017-06-06 18:07:47 -0700331 if (!isReplacingIcon) {
332 addTransientView(icon, 0);
Selim Cinekd03518c2018-03-15 12:13:51 -0700333 boolean isIsolatedIcon = child == mIsolatedIcon;
Selim Cinek72fc8db2017-06-06 18:07:47 -0700334 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
Selim Cinekd03518c2018-03-15 12:13:51 -0700335 () -> removeTransientView(icon),
Selim Cinek332c23f2018-03-16 17:37:50 -0700336 isIsolatedIcon ? CONTENT_FADE_DURATION : 0);
Selim Cinek72fc8db2017-06-06 18:07:47 -0700337 }
Selim Cinek5b5beb012016-11-08 18:11:58 -0800338 }
339 }
340 }
341
342 /**
343 * Finds the first view with a translation bigger then a given value
344 */
345 private int findFirstViewIndexAfter(float translationX) {
346 for (int i = 0; i < getChildCount(); i++) {
347 View view = getChildAt(i);
348 if (view.getTranslationX() > translationX) {
349 return i;
350 }
351 }
352 return getChildCount();
Selim Cinek281c2022016-10-13 19:14:43 -0700353 }
354
Selim Cinek65d418e2016-11-29 15:42:34 -0800355 public void resetViewStates() {
Selim Cinek281c2022016-10-13 19:14:43 -0700356 for (int i = 0; i < getChildCount(); i++) {
357 View view = getChildAt(i);
358 ViewState iconState = mIconStates.get(view);
359 iconState.initFrom(view);
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800360 iconState.alpha = mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f;
Selim Cinek414ad332017-02-24 19:06:12 -0800361 iconState.hidden = false;
Selim Cinek281c2022016-10-13 19:14:43 -0700362 }
Selim Cinek281c2022016-10-13 19:14:43 -0700363 }
364
Selim Cinekc383fd02016-10-21 15:31:26 -0700365 /**
Evan Laird8cf0de42018-02-06 18:34:55 -0500366 * Calculate the horizontal translations for each notification based on how much the icons
Selim Cinekc383fd02016-10-21 15:31:26 -0700367 * are inserted into the notification container.
368 * If this is not a whole number, the fraction means by how much the icon is appearing.
369 */
Selim Cinek49014f82016-11-04 14:55:30 -0700370 public void calculateIconTranslations() {
371 float translationX = getActualPaddingStart();
Selim Cinek17e1b692016-12-02 18:19:11 -0800372 int firstOverflowIndex = -1;
Selim Cinekc383fd02016-10-21 15:31:26 -0700373 int childCount = getChildCount();
Evan Lairdc987fc72017-12-15 10:14:22 -0500374 int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK :
375 mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
Selim Cinek17e1b692016-12-02 18:19:11 -0800376 float layoutEnd = getLayoutEnd();
Evan Laird8cf0de42018-02-06 18:34:55 -0500377 float overflowStart = getMaxOverflowStart();
378 mVisualOverflowStart = 0;
Lucas Dupine87bf772018-04-05 12:48:43 -0700379 mFirstVisibleIconState = null;
Selim Cinek17e1b692016-12-02 18:19:11 -0800380 boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
Selim Cinekc383fd02016-10-21 15:31:26 -0700381 for (int i = 0; i < childCount; i++) {
382 View view = getChildAt(i);
383 IconState iconState = mIconStates.get(view);
384 iconState.xTranslation = translationX;
Lucas Dupine87bf772018-04-05 12:48:43 -0700385 if (mFirstVisibleIconState == null) {
386 mFirstVisibleIconState = iconState;
387 }
Adrian Roos14d70cb2017-04-25 16:46:49 -0700388 boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
389 && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
Selim Cinek17e1b692016-12-02 18:19:11 -0800390 boolean noOverflowAfter = i == childCount - 1;
Adrian Roosceac4a02017-05-30 20:25:52 -0700391 float drawingScale = mDark && view instanceof StatusBarIconView
392 ? ((StatusBarIconView) view).getIconScaleFullyDark()
393 : 1f;
Selim Cinek17e1b692016-12-02 18:19:11 -0800394 if (mOpenedAmount != 0.0f) {
Adrian Roos14d70cb2017-04-25 16:46:49 -0700395 noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
Selim Cinek49014f82016-11-04 14:55:30 -0700396 }
Selim Cinek17e1b692016-12-02 18:19:11 -0800397 iconState.visibleState = StatusBarIconView.STATE_ICON;
Evan Laird8cf0de42018-02-06 18:34:55 -0500398
399 boolean isOverflowing =
Evan Laird6f9e8562018-05-22 19:14:50 -0400400 (translationX > (noOverflowAfter ? layoutEnd - mIconSize
401 : overflowStart - mIconSize));
Evan Laird8cf0de42018-02-06 18:34:55 -0500402 if (firstOverflowIndex == -1 && (forceOverflow || isOverflowing)) {
Adrian Roos14d70cb2017-04-25 16:46:49 -0700403 firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
Evan Laird6f9e8562018-05-22 19:14:50 -0400404 mVisualOverflowStart = layoutEnd - mOverflowWidth;
405 if (forceOverflow || mIsStaticLayout) {
Evan Laird8cf0de42018-02-06 18:34:55 -0500406 mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart);
Selim Cinek17e1b692016-12-02 18:19:11 -0800407 }
408 }
Adrian Roosceac4a02017-05-30 20:25:52 -0700409 translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
Selim Cinekc383fd02016-10-21 15:31:26 -0700410 }
Evan Laird8cf0de42018-02-06 18:34:55 -0500411 mNumDots = 0;
Selim Cinek17e1b692016-12-02 18:19:11 -0800412 if (firstOverflowIndex != -1) {
Evan Laird8cf0de42018-02-06 18:34:55 -0500413 translationX = mVisualOverflowStart;
Selim Cinek17e1b692016-12-02 18:19:11 -0800414 for (int i = firstOverflowIndex; i < childCount; i++) {
Selim Cinek49014f82016-11-04 14:55:30 -0700415 View view = getChildAt(i);
416 IconState iconState = mIconStates.get(view);
Evan Laird6f9e8562018-05-22 19:14:50 -0400417 int dotWidth = mStaticDotDiameter + mDotPadding;
Selim Cinek49014f82016-11-04 14:55:30 -0700418 iconState.xTranslation = translationX;
Evan Laird8cf0de42018-02-06 18:34:55 -0500419 if (mNumDots < MAX_DOTS) {
420 if (mNumDots == 0 && iconState.iconAppearAmount < 0.8f) {
Selim Cinek17e1b692016-12-02 18:19:11 -0800421 iconState.visibleState = StatusBarIconView.STATE_ICON;
Selim Cinek17e1b692016-12-02 18:19:11 -0800422 } else {
423 iconState.visibleState = StatusBarIconView.STATE_DOT;
Evan Laird8cf0de42018-02-06 18:34:55 -0500424 mNumDots++;
Selim Cinek17e1b692016-12-02 18:19:11 -0800425 }
Evan Laird8cf0de42018-02-06 18:34:55 -0500426 translationX += (mNumDots == MAX_DOTS ? MAX_DOTS * dotWidth : dotWidth)
Selim Cinek17e1b692016-12-02 18:19:11 -0800427 * iconState.iconAppearAmount;
Evan Lairdc987fc72017-12-15 10:14:22 -0500428 mLastVisibleIconState = iconState;
Selim Cinek49014f82016-11-04 14:55:30 -0700429 } else {
430 iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
431 }
Selim Cinek49014f82016-11-04 14:55:30 -0700432 }
Evan Lairdc987fc72017-12-15 10:14:22 -0500433 } else if (childCount > 0) {
434 View lastChild = getChildAt(childCount - 1);
435 mLastVisibleIconState = mIconStates.get(lastChild);
Lucas Dupin15475702018-04-04 14:32:53 -0700436 mFirstVisibleIconState = mIconStates.get(getChildAt(0));
Selim Cinek49014f82016-11-04 14:55:30 -0700437 }
Adrian Roos456e0052017-04-04 16:44:29 -0700438 boolean center = mDark;
439 if (center && translationX < getLayoutEnd()) {
Lucas Dupin15475702018-04-04 14:32:53 -0700440 float initialTranslation =
441 mFirstVisibleIconState == null ? 0 : mFirstVisibleIconState.xTranslation;
442 float contentWidth = getFinalTranslationX() - initialTranslation;
443 float availableSpace = getLayoutEnd() - getActualPaddingStart();
444 float delta = (availableSpace - contentWidth) / 2;
445
Adrian Roos14d70cb2017-04-25 16:46:49 -0700446 if (firstOverflowIndex != -1) {
447 // If we have an overflow, only count those half for centering because the dots
448 // don't have a lot of visual weight.
Evan Laird8cf0de42018-02-06 18:34:55 -0500449 float deltaIgnoringOverflow = (getLayoutEnd() - mVisualOverflowStart) / 2;
Adrian Roos14d70cb2017-04-25 16:46:49 -0700450 delta = (deltaIgnoringOverflow + delta) / 2;
451 }
Adrian Roos7a9551a2017-01-11 12:27:49 -0800452 for (int i = 0; i < childCount; i++) {
453 View view = getChildAt(i);
454 IconState iconState = mIconStates.get(view);
455 iconState.xTranslation += delta;
456 }
457 }
458
Selim Cinek49014f82016-11-04 14:55:30 -0700459 if (isLayoutRtl()) {
460 for (int i = 0; i < childCount; i++) {
461 View view = getChildAt(i);
462 IconState iconState = mIconStates.get(view);
463 iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
464 }
465 }
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800466 if (mIsolatedIcon != null) {
467 IconState iconState = mIconStates.get(mIsolatedIcon);
468 if (iconState != null) {
469 // Most of the time the icon isn't yet added when this is called but only happening
470 // later
471 iconState.xTranslation = mIsolatedIconLocation.left - mAbsolutePosition[0]
472 - (1 - mIsolatedIcon.getIconScale()) * mIsolatedIcon.getWidth() / 2.0f;
473 iconState.visibleState = StatusBarIconView.STATE_ICON;
474 }
475 }
Selim Cinek49014f82016-11-04 14:55:30 -0700476 }
477
478 private float getLayoutEnd() {
479 return getActualWidth() - getActualPaddingEnd();
480 }
481
482 private float getActualPaddingEnd() {
Selim Cinek932005d2016-12-05 17:12:09 -0800483 if (mActualPaddingEnd == NO_VALUE) {
Selim Cinek49014f82016-11-04 14:55:30 -0700484 return getPaddingEnd();
485 }
486 return mActualPaddingEnd;
487 }
488
489 private float getActualPaddingStart() {
Selim Cinek932005d2016-12-05 17:12:09 -0800490 if (mActualPaddingStart == NO_VALUE) {
Selim Cinek49014f82016-11-04 14:55:30 -0700491 return getPaddingStart();
492 }
493 return mActualPaddingStart;
Selim Cinek281c2022016-10-13 19:14:43 -0700494 }
495
496 /**
Evan Lairdc987fc72017-12-15 10:14:22 -0500497 * Sets whether the layout should always show the same number of icons.
Selim Cinek281c2022016-10-13 19:14:43 -0700498 * If this is true, the icon positions will be updated on layout.
499 * If this if false, the layout is managed from the outside and layouting won't trigger a
500 * repositioning of the icons.
501 */
Evan Lairdc987fc72017-12-15 10:14:22 -0500502 public void setIsStaticLayout(boolean isStaticLayout) {
503 mIsStaticLayout = isStaticLayout;
Selim Cinek281c2022016-10-13 19:14:43 -0700504 }
Selim Cinekc383fd02016-10-21 15:31:26 -0700505
Selim Cinek49014f82016-11-04 14:55:30 -0700506 public void setActualLayoutWidth(int actualLayoutWidth) {
507 mActualLayoutWidth = actualLayoutWidth;
508 if (DEBUG) {
509 invalidate();
510 }
511 }
512
513 public void setActualPaddingEnd(float paddingEnd) {
514 mActualPaddingEnd = paddingEnd;
515 if (DEBUG) {
516 invalidate();
517 }
518 }
519
520 public void setActualPaddingStart(float paddingStart) {
521 mActualPaddingStart = paddingStart;
522 if (DEBUG) {
523 invalidate();
524 }
525 }
526
527 public int getActualWidth() {
Selim Cinek932005d2016-12-05 17:12:09 -0800528 if (mActualLayoutWidth == NO_VALUE) {
Selim Cinek49014f82016-11-04 14:55:30 -0700529 return getWidth();
530 }
531 return mActualLayoutWidth;
532 }
533
Evan Lairdc987fc72017-12-15 10:14:22 -0500534 public int getFinalTranslationX() {
535 if (mLastVisibleIconState == null) {
536 return 0;
537 }
538
Evan Laird8cf0de42018-02-06 18:34:55 -0500539 int translation = (int) (mLastVisibleIconState.xTranslation + mIconSize);
540 // There's a chance that last translation goes beyond the edge maybe
541 return Math.min(getWidth(), translation);
542 }
543
544 private float getMaxOverflowStart() {
Evan Laird6f9e8562018-05-22 19:14:50 -0400545 return getLayoutEnd() - mOverflowWidth;
Evan Lairdc987fc72017-12-15 10:14:22 -0500546 }
547
Selim Cinek5b5beb012016-11-08 18:11:58 -0800548 public void setChangingViewPositions(boolean changingViewPositions) {
549 mChangingViewPositions = changingViewPositions;
550 }
551
Adrian Roos456e0052017-04-04 16:44:29 -0700552 public void setDark(boolean dark, boolean fade, long delay) {
553 mDark = dark;
Adrian Roos28f90c72017-05-08 17:24:26 -0700554 mDisallowNextAnimation |= !fade;
Adrian Roos456e0052017-04-04 16:44:29 -0700555 for (int i = 0; i < getChildCount(); i++) {
556 View view = getChildAt(i);
557 if (view instanceof StatusBarIconView) {
558 ((StatusBarIconView) view).setDark(dark, fade, delay);
559 }
560 }
Adrian Roos7a9551a2017-01-11 12:27:49 -0800561 }
562
Selim Cinek2b549f42016-11-22 16:38:51 -0800563 public IconState getIconState(StatusBarIconView icon) {
564 return mIconStates.get(icon);
565 }
566
Selim Cinek17e1b692016-12-02 18:19:11 -0800567 public void setSpeedBumpIndex(int speedBumpIndex) {
568 mSpeedBumpIndex = speedBumpIndex;
569 }
570
571 public void setOpenedAmount(float expandAmount) {
572 mOpenedAmount = expandAmount;
573 }
574
Selim Cinek932005d2016-12-05 17:12:09 -0800575 public boolean hasOverflow() {
Evan Laird8cf0de42018-02-06 18:34:55 -0500576 return mNumDots > 0;
Selim Cinek932005d2016-12-05 17:12:09 -0800577 }
578
Evan Lairdc987fc72017-12-15 10:14:22 -0500579 /**
580 * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
581 * extra padding will have to be accounted for
582 *
583 * This method has no meaning for non-static containers
584 */
585 public boolean hasPartialOverflow() {
Evan Laird8cf0de42018-02-06 18:34:55 -0500586 return mNumDots > 0 && mNumDots < MAX_DOTS;
Evan Lairdc987fc72017-12-15 10:14:22 -0500587 }
588
589 /**
590 * Get padding that can account for extra dots up to the max. The only valid values for
591 * this method are for 1 or 2 dots.
592 * @return only extraDotPadding or extraDotPadding * 2
593 */
594 public int getPartialOverflowExtraPadding() {
595 if (!hasPartialOverflow()) {
596 return 0;
597 }
598
Evan Laird43c09a92018-03-06 09:46:49 -0500599 int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding);
Evan Laird8cf0de42018-02-06 18:34:55 -0500600
601 int adjustedWidth = getFinalTranslationX() + partialOverflowAmount;
602 // In case we actually give too much padding...
603 if (adjustedWidth > getWidth()) {
604 partialOverflowAmount = getWidth() - getFinalTranslationX();
605 }
606
607 return partialOverflowAmount;
608 }
609
610 // Give some extra room for btw notifications if we can
611 public int getNoOverflowExtraPadding() {
612 if (mNumDots != 0) {
613 return 0;
614 }
615
Evan Laird6f9e8562018-05-22 19:14:50 -0400616 int collapsedPadding = mOverflowWidth;
Evan Laird8cf0de42018-02-06 18:34:55 -0500617
618 if (collapsedPadding + getFinalTranslationX() > getWidth()) {
619 collapsedPadding = getWidth() - getFinalTranslationX();
620 }
621
622 return collapsedPadding;
Evan Lairdc987fc72017-12-15 10:14:22 -0500623 }
624
Selim Cinek932005d2016-12-05 17:12:09 -0800625 public int getIconSize() {
626 return mIconSize;
627 }
628
Selim Cinek09bd29d2017-02-03 15:30:28 -0800629 public void setAnimationsEnabled(boolean enabled) {
630 if (!enabled && mAnimationsEnabled) {
631 for (int i = 0; i < getChildCount(); i++) {
632 View child = getChildAt(i);
633 ViewState childState = mIconStates.get(child);
634 if (childState != null) {
635 childState.cancelAnimations(child);
636 childState.applyToView(child);
637 }
638 }
639 }
640 mAnimationsEnabled = enabled;
641 }
642
Selim Cinek72fc8db2017-06-06 18:07:47 -0700643 public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
644 mReplacingIcons = replacingIcons;
645 }
646
Selim Cinek332c23f2018-03-16 17:37:50 -0700647 public void showIconIsolated(StatusBarIconView icon, boolean animated) {
Selim Cinekd03518c2018-03-15 12:13:51 -0700648 if (animated) {
Selim Cinek332c23f2018-03-16 17:37:50 -0700649 mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
Selim Cinekd03518c2018-03-15 12:13:51 -0700650 }
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800651 mIsolatedIcon = icon;
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800652 updateState();
653 }
654
Selim Cinek332c23f2018-03-16 17:37:50 -0700655 public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) {
656 mIsolatedIconLocation = isolatedIconLocation;
657 if (requireUpdate) {
658 updateState();
659 }
660 }
661
Selim Cinek5b5beb012016-11-08 18:11:58 -0800662 public class IconState extends ViewState {
Selim Cinek1f624952017-06-08 19:11:50 -0700663 public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
Selim Cinekc383fd02016-10-21 15:31:26 -0700664 public float iconAppearAmount = 1.0f;
Selim Cinek2b549f42016-11-22 16:38:51 -0800665 public float clampedAppearAmount = 1.0f;
Selim Cinek49014f82016-11-04 14:55:30 -0700666 public int visibleState;
Selim Cinek5b5beb012016-11-08 18:11:58 -0800667 public boolean justAdded = true;
Selim Cinek72fc8db2017-06-06 18:07:47 -0700668 private boolean justReplaced;
Selim Cinek2b549f42016-11-22 16:38:51 -0800669 public boolean needsCannedAnimation;
Selim Cinek2b549f42016-11-22 16:38:51 -0800670 public boolean useFullTransitionAmount;
Selim Cineka1d97902016-12-14 16:31:40 -0800671 public boolean useLinearTransitionAmount;
Selim Cinek01a73f92016-12-06 16:13:42 -0800672 public boolean translateContent;
Selim Cinek875ba9b2017-02-13 16:20:17 -0800673 public int iconColor = StatusBarIconView.NO_COLOR;
Selim Cinek44d81a62017-05-08 19:45:40 -0700674 public boolean noAnimations;
Selim Cinek1f624952017-06-08 19:11:50 -0700675 public boolean isLastExpandIcon;
676 public int customTransformHeight = NO_VALUE;
Selim Cinekc383fd02016-10-21 15:31:26 -0700677
678 @Override
679 public void applyToView(View view) {
Selim Cinek49014f82016-11-04 14:55:30 -0700680 if (view instanceof StatusBarIconView) {
681 StatusBarIconView icon = (StatusBarIconView) view;
Selim Cinek2b549f42016-11-22 16:38:51 -0800682 boolean animate = false;
683 AnimationProperties animationProperties = null;
Lucas Dupin8274aa92017-09-13 17:24:09 -0700684 boolean animationsAllowed = mAnimationsEnabled && !mDisallowNextAnimation
Selim Cinek44d81a62017-05-08 19:45:40 -0700685 && !noAnimations;
Selim Cinek09bd29d2017-02-03 15:30:28 -0800686 if (animationsAllowed) {
Selim Cinek72fc8db2017-06-06 18:07:47 -0700687 if (justAdded || justReplaced) {
Selim Cinek09bd29d2017-02-03 15:30:28 -0800688 super.applyToView(icon);
Selim Cinek72fc8db2017-06-06 18:07:47 -0700689 if (justAdded && iconAppearAmount != 0.0f) {
Selim Cinek09bd29d2017-02-03 15:30:28 -0800690 icon.setAlpha(0.0f);
691 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
692 false /* animate */);
693 animationProperties = ADD_ICON_PROPERTIES;
694 animate = true;
695 }
696 } else if (visibleState != icon.getVisibleState()) {
697 animationProperties = DOT_ANIMATION_PROPERTIES;
Selim Cinekedebced2017-02-03 12:34:16 -0800698 animate = true;
699 }
Selim Cinek09bd29d2017-02-03 15:30:28 -0800700 if (!animate && mAddAnimationStartIndex >= 0
701 && indexOfChild(view) >= mAddAnimationStartIndex
702 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
Selim Cinek5b5beb012016-11-08 18:11:58 -0800703 || visibleState != StatusBarIconView.STATE_HIDDEN)) {
Selim Cinek09bd29d2017-02-03 15:30:28 -0800704 animationProperties = DOT_ANIMATION_PROPERTIES;
705 animate = true;
Selim Cinek2b549f42016-11-22 16:38:51 -0800706 }
Selim Cinek09bd29d2017-02-03 15:30:28 -0800707 if (needsCannedAnimation) {
Lucas Dupin8274aa92017-09-13 17:24:09 -0700708 AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
Selim Cinek09bd29d2017-02-03 15:30:28 -0800709 animationFilter.reset();
710 animationFilter.combineFilter(
711 ICON_ANIMATION_PROPERTIES.getAnimationFilter());
Lucas Dupin8274aa92017-09-13 17:24:09 -0700712 sTempProperties.resetCustomInterpolators();
713 sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
Selim Cinek09bd29d2017-02-03 15:30:28 -0800714 if (animationProperties != null) {
715 animationFilter.combineFilter(animationProperties.getAnimationFilter());
Lucas Dupin8274aa92017-09-13 17:24:09 -0700716 sTempProperties.combineCustomInterpolators(animationProperties);
Selim Cinek09bd29d2017-02-03 15:30:28 -0800717 }
Lucas Dupin8274aa92017-09-13 17:24:09 -0700718 animationProperties = sTempProperties;
Selim Cinek09bd29d2017-02-03 15:30:28 -0800719 animationProperties.setDuration(CANNED_ANIMATION_DURATION);
720 animate = true;
721 mCannedAnimationStartIndex = indexOfChild(view);
722 }
723 if (!animate && mCannedAnimationStartIndex >= 0
724 && indexOfChild(view) > mCannedAnimationStartIndex
725 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
726 || visibleState != StatusBarIconView.STATE_HIDDEN)) {
Lucas Dupin8274aa92017-09-13 17:24:09 -0700727 AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
Selim Cinek09bd29d2017-02-03 15:30:28 -0800728 animationFilter.reset();
729 animationFilter.animateX();
Lucas Dupin8274aa92017-09-13 17:24:09 -0700730 sTempProperties.resetCustomInterpolators();
731 animationProperties = sTempProperties;
Selim Cinek09bd29d2017-02-03 15:30:28 -0800732 animationProperties.setDuration(CANNED_ANIMATION_DURATION);
733 animate = true;
734 }
Selim Cinekd03518c2018-03-15 12:13:51 -0700735 if (mIsolatedIconForAnimation != null) {
736 if (view == mIsolatedIconForAnimation) {
737 animationProperties = UNISOLATION_PROPERTY;
Selim Cinek332c23f2018-03-16 17:37:50 -0700738 animationProperties.setDelay(
739 mIsolatedIcon != null ? CONTENT_FADE_DELAY : 0);
Selim Cinekd03518c2018-03-15 12:13:51 -0700740 } else {
741 animationProperties = UNISOLATION_PROPERTY_OTHERS;
Selim Cinek332c23f2018-03-16 17:37:50 -0700742 animationProperties.setDelay(
743 mIsolatedIcon == null ? CONTENT_FADE_DELAY : 0);
Selim Cinekd03518c2018-03-15 12:13:51 -0700744 }
745 animate = true;
746 }
Selim Cinek2b549f42016-11-22 16:38:51 -0800747 }
Selim Cinek09bd29d2017-02-03 15:30:28 -0800748 icon.setVisibleState(visibleState, animationsAllowed);
Selim Cinek875ba9b2017-02-13 16:20:17 -0800749 icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
Selim Cinek09bd29d2017-02-03 15:30:28 -0800750 if (animate) {
Selim Cinek5b5beb012016-11-08 18:11:58 -0800751 animateTo(icon, animationProperties);
Selim Cinek49014f82016-11-04 14:55:30 -0700752 } else {
753 super.applyToView(view);
754 }
Selim Cinekd95ca7c2017-07-26 12:20:38 -0700755 boolean inShelf = iconAppearAmount == 1.0f;
756 icon.setIsInShelf(inShelf);
Selim Cinek49014f82016-11-04 14:55:30 -0700757 }
Selim Cinek5b5beb012016-11-08 18:11:58 -0800758 justAdded = false;
Selim Cinek72fc8db2017-06-06 18:07:47 -0700759 justReplaced = false;
Selim Cinek2b549f42016-11-22 16:38:51 -0800760 needsCannedAnimation = false;
761 }
762
Selim Cinek1f624952017-06-08 19:11:50 -0700763 public boolean hasCustomTransformHeight() {
764 return isLastExpandIcon && customTransformHeight != NO_VALUE;
765 }
766
Selim Cinek875ba9b2017-02-13 16:20:17 -0800767 @Override
768 public void initFrom(View view) {
769 super.initFrom(view);
770 if (view instanceof StatusBarIconView) {
771 iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
772 }
773 }
Selim Cinekc383fd02016-10-21 15:31:26 -0700774 }
Selim Cinek281c2022016-10-13 19:14:43 -0700775}