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