• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.allapps;
17 
18 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN;
19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END;
20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.graphics.Rect;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.WindowInsets;
37 import android.widget.ImageButton;
38 import android.widget.ImageView;
39 import android.widget.LinearLayout;
40 import android.widget.TextView;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.VisibleForTesting;
44 import androidx.core.graphics.Insets;
45 import androidx.core.view.WindowInsetsCompat;
46 
47 import com.android.app.animation.Interpolators;
48 import com.android.launcher3.DeviceProfile;
49 import com.android.launcher3.Flags;
50 import com.android.launcher3.Insettable;
51 import com.android.launcher3.R;
52 import com.android.launcher3.Utilities;
53 import com.android.launcher3.anim.AnimatedPropertySetter;
54 import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
55 import com.android.launcher3.logging.StatsLogManager;
56 import com.android.launcher3.model.StringCache;
57 import com.android.launcher3.views.ActivityContext;
58 
59 import java.util.ArrayList;
60 
61 /**
62  * Work profile utility ViewGroup that is shown at the bottom of AllApps work tab
63  */
64 public class WorkUtilityView extends LinearLayout implements Insettable,
65         KeyboardInsetAnimationCallback.KeyboardInsetListener {
66 
67     private static final String TAG = "WorkUtilityView";
68     private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
69     private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
70     private static final int EXPAND_COLLAPSE_DURATION = 300;
71     private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
72     private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
73     private static final int WORK_SCHEDULER_OPACITY_DURATION =
74             (int) (EXPAND_COLLAPSE_DURATION * 0.75f);
75     private static final int FLAG_FADE_ONGOING = 1 << 1;
76     private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
77     private static final int FLAG_IS_EXPAND = 1 << 3;
78     private static final int SCROLL_THRESHOLD_DP = 10;
79     private static final float WORK_SCHEDULER_SCALE_MIN = 0.25f;
80     private static final float WORK_SCHEDULER_SCALE_MAX = 1f;
81 
82     private final Rect mInsets = new Rect();
83     private final Rect mImeInsets = new Rect();
84     private int mFlags;
85     private final ActivityContext mActivityContext;
86     private final Context mContext;
87     private final int mTextMarginStart;
88     private final int mTextMarginEnd;
89     private final int mIconMarginStart;
90     private final String mWorkSchedulerIntentAction;
91 
92     // Threshold when user scrolls up/down to determine when should button extend/collapse
93     private final int mScrollThreshold;
94     private ValueAnimator mPauseFABAnim;
95     private View mWorkFAB;
96     private TextView mPauseText;
97     private ImageView mWorkIcon;
98     private ImageButton mSchedulerButton;
99     private final StatsLogManager mStatsLogManager;
100     private LinearLayout mWorkUtilityView;
101 
WorkUtilityView(@onNull Context context)102     public WorkUtilityView(@NonNull Context context) {
103         this(context, null, 0);
104     }
105 
WorkUtilityView(@onNull Context context, @NonNull AttributeSet attrs)106     public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs) {
107         this(context, attrs, 0);
108     }
109 
WorkUtilityView(@onNull Context context, @NonNull AttributeSet attrs, int defStyleAttr)110     public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs,
111             int defStyleAttr) {
112         super(context, attrs, defStyleAttr);
113         mContext = context;
114         mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
115         mActivityContext = ActivityContext.lookupContext(getContext());
116         mTextMarginStart = mContext.getResources().getDimensionPixelSize(
117                 R.dimen.work_fab_text_start_margin);
118         mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
119                 R.dimen.work_fab_text_end_margin);
120         mIconMarginStart = mContext.getResources().getDimensionPixelSize(
121                 R.dimen.work_fab_icon_start_margin_expanded);
122         mWorkSchedulerIntentAction = mContext.getResources().getString(
123                 R.string.work_profile_scheduler_intent);
124         mStatsLogManager = mActivityContext.getStatsLogManager();
125     }
126 
127     @Override
onFinishInflate()128     protected void onFinishInflate() {
129         super.onFinishInflate();
130 
131         mPauseText = findViewById(R.id.pause_text);
132         mWorkIcon = findViewById(R.id.work_icon);
133         mWorkFAB = findViewById(R.id.work_mode_toggle);
134         mSchedulerButton = findViewById(R.id.work_scheduler);
135         mWorkUtilityView = findViewById(R.id.work_utility_view);
136         setSelected(true);
137         KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
138                 new KeyboardInsetAnimationCallback(this);
139         setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
140         // Expand is the default state upon initialization.
141         addFlag(FLAG_IS_EXPAND);
142         setInsets(mActivityContext.getDeviceProfile().getInsets());
143         updateStringFromCache();
144         mSchedulerButton.setVisibility(GONE);
145         mSchedulerButton.setOnClickListener(null);
146         if (shouldUseScheduler()) {
147             mSchedulerButton.setVisibility(VISIBLE);
148             mSchedulerButton.setOnClickListener(view -> {
149                 Log.d(TAG, "WorkScheduler button clicked.");
150                 mActivityContext.startActivitySafely(view,
151                         new Intent(mWorkSchedulerIntentAction), null /* itemInfo */);
152             });
153         }
154     }
155 
156     @Override
setInsets(Rect insets)157     public void setInsets(Rect insets) {
158         mInsets.set(insets);
159         updateTranslationY();
160         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
161         if (lp != null) {
162             int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
163             DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
164             if (mActivityContext.getAppsView().isSearchBarFloating()) {
165                 bottomMargin += dp.hotseatQsbHeight;
166             }
167 
168             if (!dp.isGestureMode && dp.isTaskbarPresent) {
169                 bottomMargin += dp.taskbarHeight;
170             }
171 
172             lp.bottomMargin = bottomMargin;
173         }
174     }
175 
176     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)177     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
178         super.onLayout(changed, left, top, right, bottom);
179         boolean isRtl = Utilities.isRtl(getResources());
180         int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
181         setTranslationX(isRtl ? shift : -shift);
182     }
183 
184     @Override
isEnabled()185     public boolean isEnabled() {
186         return super.isEnabled() && getVisibility() == VISIBLE;
187     }
188 
animateVisibility(boolean visible)189     public void animateVisibility(boolean visible) {
190         clearAnimation();
191         if (visible) {
192             addFlag(FLAG_FADE_ONGOING);
193             setVisibility(VISIBLE);
194             extend();
195             animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
196         } else if (getVisibility() != GONE) {
197             addFlag(FLAG_FADE_ONGOING);
198             animate().alpha(0).withEndAction(() -> {
199                 removeFlag(FLAG_FADE_ONGOING);
200                 setVisibility(GONE);
201             }).start();
202         }
203     }
204 
205     @Override
onApplyWindowInsets(WindowInsets insets)206     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
207         WindowInsetsCompat windowInsetsCompat =
208                 WindowInsetsCompat.toWindowInsetsCompat(insets, this);
209         if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
210             setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
211             shrink();
212         } else {
213             mImeInsets.setEmpty();
214             extend();
215         }
216         updateTranslationY();
217         return super.onApplyWindowInsets(insets);
218     }
219 
updateTranslationY()220     void updateTranslationY() {
221         setTranslationY(-mImeInsets.bottom);
222     }
223 
224     @Override
setTranslationY(float translationY)225     public void setTranslationY(float translationY) {
226         // Always translate at least enough for nav bar insets.
227         super.setTranslationY(Math.min(translationY, -mInsets.bottom));
228     }
229 
animateSchedulerScale(boolean isExpanding)230     private ValueAnimator animateSchedulerScale(boolean isExpanding) {
231         float scaleFrom = isExpanding ? WORK_SCHEDULER_SCALE_MIN : WORK_SCHEDULER_SCALE_MAX;
232         float scaleTo = isExpanding ? WORK_SCHEDULER_SCALE_MAX : WORK_SCHEDULER_SCALE_MIN;
233         ValueAnimator schedulerScaleAnim = ObjectAnimator.ofFloat(scaleFrom, scaleTo);
234         schedulerScaleAnim.setDuration(EXPAND_COLLAPSE_DURATION);
235         schedulerScaleAnim.setInterpolator(Interpolators.STANDARD);
236         schedulerScaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
237             @Override
238             public void onAnimationUpdate(ValueAnimator valueAnimator) {
239                 float scale = (float) valueAnimator.getAnimatedValue();
240                 mSchedulerButton.setScaleX(scale);
241                 mSchedulerButton.setScaleY(scale);
242             }
243         });
244         schedulerScaleAnim.addListener(new AnimatorListenerAdapter() {
245             @Override
246             public void onAnimationStart(Animator animation) {
247                 if (isExpanding) {
248                     mSchedulerButton.setVisibility(VISIBLE);
249                 }
250             }
251 
252             @Override
253             public void onAnimationEnd(Animator animation) {
254                 if (!isExpanding) {
255                     mSchedulerButton.setVisibility(GONE);
256                 }
257             }
258         });
259         return schedulerScaleAnim;
260     }
261 
animateSchedulerAlpha(boolean isExpanding)262     private ValueAnimator animateSchedulerAlpha(boolean isExpanding) {
263         float alphaFrom = isExpanding ? 0 : 1;
264         float alphaTo = isExpanding ? 1 : 0;
265         ValueAnimator schedulerAlphaAnim = ObjectAnimator.ofFloat(alphaFrom, alphaTo);
266         schedulerAlphaAnim.setDuration(WORK_SCHEDULER_OPACITY_DURATION);
267         schedulerAlphaAnim.setStartDelay(isExpanding ? 0 :
268                 EXPAND_COLLAPSE_DURATION - WORK_SCHEDULER_OPACITY_DURATION);
269         schedulerAlphaAnim.setInterpolator(Interpolators.STANDARD);
270         schedulerAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
271             @Override
272             public void onAnimationUpdate(ValueAnimator valueAnimator) {
273                 mSchedulerButton.setAlpha((float) valueAnimator.getAnimatedValue());
274             }
275         });
276         return schedulerAlphaAnim;
277     }
278 
animateWorkUtilityViews(boolean isExpanding)279     private void animateWorkUtilityViews(boolean isExpanding) {
280         if (!shouldAnimate(isExpanding)) {
281             return;
282         }
283         AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
284         mPauseText.measure(0,0);
285         int currentWidth = mPauseText.getWidth();
286         int fullWidth = mPauseText.getMeasuredWidth();
287         float from = isExpanding ? 0 : currentWidth;
288         float to = isExpanding ? fullWidth : 0;
289         mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
290         mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
291         mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
292         mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
293             @Override
294             public void onAnimationUpdate(ValueAnimator valueAnimator) {
295                 float translation = (float) valueAnimator.getAnimatedValue();
296                 float translationFraction = translation / fullWidth;
297                 ViewGroup.MarginLayoutParams textViewLayoutParams =
298                         (ViewGroup.MarginLayoutParams) mPauseText.getLayoutParams();
299                 textViewLayoutParams.width = (int) translation;
300                 textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
301                 textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
302                 mPauseText.setLayoutParams(textViewLayoutParams);
303                 ViewGroup.MarginLayoutParams iconLayoutParams =
304                         (ViewGroup.MarginLayoutParams) mWorkIcon.getLayoutParams();
305                 iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
306                 mWorkIcon.setLayoutParams(iconLayoutParams);
307             }
308         });
309         mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
310             @Override
311             public void onAnimationEnd(Animator animator) {
312                 if (isExpanding) {
313                     addFlag(FLAG_IS_EXPAND);
314                 } else {
315                     mPauseText.setVisibility(GONE);
316                     removeFlag(FLAG_IS_EXPAND);
317                 }
318                 mPauseText.setHorizontallyScrolling(false);
319                 mPauseText.setEllipsize(TextUtils.TruncateAt.END);
320             }
321 
322             @Override
323             public void onAnimationStart(Animator animator) {
324                 mPauseText.setHorizontallyScrolling(true);
325                 mPauseText.setVisibility(VISIBLE);
326                 mPauseText.setEllipsize(null);
327             }
328         });
329         ArrayList<Animator> animatorList = new ArrayList<>();
330         animatorList.add(mPauseFABAnim);
331         animatorList.add(updatePauseTextAlpha(isExpanding));
332         if (shouldUseScheduler()) {
333             animatorList.add(animateSchedulerScale(isExpanding));
334             animatorList.add(animateSchedulerAlpha(isExpanding));
335         }
336         animatorSet.addListener(new AnimatorListenerAdapter() {
337             @Override
338             public void onAnimationStart(Animator animation) {
339                 mStatsLogManager.logger().sendToInteractionJankMonitor(
340                         isExpanding ? LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN
341                                 : LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN,
342                         mWorkUtilityView);
343             }
344 
345             @Override
346             public void onAnimationEnd(Animator animation) {
347                 mStatsLogManager.logger().sendToInteractionJankMonitor(
348                         isExpanding ? LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END
349                                 : LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END,
350                         mWorkUtilityView);
351             }
352         });
353         animatorSet.playTogether(animatorList);
354         animatorSet.start();
355     }
356 
357 
updatePauseTextAlpha(boolean expand)358     private ValueAnimator updatePauseTextAlpha(boolean expand) {
359         float from = expand ? 0 : 1;
360         float to = expand ? 1 : 0;
361         ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
362         alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
363                 : TEXT_COLLAPSE_OPACITY_DURATION);
364         alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
365         alphaAnim.setInterpolator(Interpolators.LINEAR);
366         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
367             @Override
368             public void onAnimationUpdate(ValueAnimator valueAnimator) {
369                 mPauseText.setAlpha((float) valueAnimator.getAnimatedValue());
370             }
371         });
372         return alphaAnim;
373     }
374 
setInsets(Rect rect, Insets insets)375     private void setInsets(Rect rect, Insets insets) {
376         rect.set(insets.left, insets.top, insets.right, insets.bottom);
377     }
378 
getImeInsets()379     public Rect getImeInsets() {
380         return mImeInsets;
381     }
382 
383     @Override
onTranslationStart()384     public void onTranslationStart() {
385         addFlag(FLAG_TRANSLATION_ONGOING);
386     }
387 
388     @Override
onTranslationEnd()389     public void onTranslationEnd() {
390         removeFlag(FLAG_TRANSLATION_ONGOING);
391     }
392 
addFlag(int flag)393     private void addFlag(int flag) {
394         mFlags |= flag;
395     }
396 
removeFlag(int flag)397     private void removeFlag(int flag) {
398         mFlags &= ~flag;
399     }
400 
containsFlag(int flag)401     private boolean containsFlag(int flag) {
402         return (mFlags & flag) == flag;
403     }
404 
extend()405     public void extend() {
406         animateWorkUtilityViews(true);
407     }
408 
shrink()409     public void shrink() {
410         animateWorkUtilityViews(false);
411     }
412 
413     /**
414      * Determines if the button should animate based on current state. It should animate the button
415      * only if it is not in the same state it is animating to.
416      */
shouldAnimate(boolean expanding)417     private boolean shouldAnimate(boolean expanding) {
418         return expanding != containsFlag(FLAG_IS_EXPAND)
419                 && (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
420     }
421 
getScrollThreshold()422     public int getScrollThreshold() {
423         return mScrollThreshold;
424     }
425 
getWorkFAB()426     public View getWorkFAB() {
427         return mWorkFAB;
428     }
429 
updateStringFromCache()430     public void updateStringFromCache(){
431         StringCache cache = mActivityContext.getStringCache();
432         if (cache != null) {
433             mPauseText.setText(cache.workProfilePauseButton);
434         }
435     }
436 
437     @VisibleForTesting
shouldUseScheduler()438     boolean shouldUseScheduler() {
439         return Flags.workSchedulerInWorkProfile() && !mWorkSchedulerIntentAction.isEmpty();
440     }
441 
442     @VisibleForTesting
getSchedulerButton()443     ImageButton getSchedulerButton() {
444         return mSchedulerButton;
445     }
446 }
447