1 /*
2  * Copyright (C) 2021 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.taskbar;
17 
18 import static android.view.Display.DEFAULT_DISPLAY;
19 
20 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.content.SharedPreferences;
27 import android.content.res.Resources;
28 import android.graphics.Outline;
29 import android.graphics.Rect;
30 import android.view.View;
31 import android.view.ViewOutlineProvider;
32 
33 import com.android.launcher3.DeviceProfile;
34 import com.android.launcher3.LauncherPrefs;
35 import com.android.launcher3.R;
36 import com.android.launcher3.anim.AnimatedFloat;
37 import com.android.launcher3.anim.RevealOutlineAnimation;
38 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
39 import com.android.launcher3.util.DisplayController;
40 import com.android.launcher3.util.Executors;
41 import com.android.launcher3.util.MultiPropertyFactory;
42 import com.android.launcher3.util.MultiValueAlpha;
43 import com.android.quickstep.NavHandle;
44 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
45 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
46 
47 import java.io.PrintWriter;
48 
49 /**
50  * Handles properties/data collection, then passes the results to our stashed handle View to render.
51  */
52 public class StashedHandleViewController implements TaskbarControllers.LoggableTaskbarController,
53         NavHandle {
54 
55     public static final int ALPHA_INDEX_STASHED = 0;
56     public static final int ALPHA_INDEX_HOME_DISABLED = 1;
57     public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 2;
58     public static final int ALPHA_INDEX_HIDDEN_WHILE_DREAMING = 3;
59     private static final int NUM_ALPHA_CHANNELS = 4;
60 
61     // Values for long press animations, picked to most closely match navbar spec.
62     private static final float SCALE_TOUCH_ANIMATION_SHRINK = 0.85f;
63     private static final float SCALE_TOUCH_ANIMATION_EXPAND = 1.18f;
64 
65     /**
66      * The SharedPreferences key for whether the stashed handle region is dark.
67      */
68     private static final String SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY =
69             "stashed_handle_region_is_dark";
70 
71     private final TaskbarActivityContext mActivity;
72     private final SharedPreferences mPrefs;
73     private final StashedHandleView mStashedHandleView;
74     private int mStashedHandleWidth;
75     private final int mStashedHandleHeight;
76     private RegionSamplingHelper mRegionSamplingHelper;
77     private final MultiValueAlpha mTaskbarStashedHandleAlpha;
78     private final AnimatedFloat mTaskbarStashedHandleHintScale = new AnimatedFloat(
79             this::updateStashedHandleHintScale);
80 
81     // Initialized in init.
82     private TaskbarControllers mControllers;
83     private int mTaskbarSize;
84 
85     // The bounds we want to clip to in the settled state when showing the stashed handle.
86     private final Rect mStashedHandleBounds = new Rect();
87     private float mStashedHandleRadius;
88 
89     // When the reveal animation is cancelled, we can assume it's about to create a new animation,
90     // which should start off at the same point the cancelled one left off.
91     private float mStartProgressForNextRevealAnim;
92     private boolean mWasLastRevealAnimReversed;
93 
94     // States that affect whether region sampling is enabled or not
95     private boolean mIsStashed;
96     private boolean mIsLumaSamplingEnabled;
97     private boolean mTaskbarHidden;
98 
99     private float mTranslationYForSwipe;
100     private float mTranslationYForStash;
101 
StashedHandleViewController(TaskbarActivityContext activity, StashedHandleView stashedHandleView)102     public StashedHandleViewController(TaskbarActivityContext activity,
103             StashedHandleView stashedHandleView) {
104         mActivity = activity;
105         mPrefs = LauncherPrefs.getPrefs(mActivity);
106         mStashedHandleView = stashedHandleView;
107         mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, NUM_ALPHA_CHANNELS);
108         mTaskbarStashedHandleAlpha.setUpdateVisibility(true);
109         mStashedHandleView.updateHandleColor(
110                 mPrefs.getBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, false),
111                 false /* animate */);
112         final Resources resources = mActivity.getResources();
113         mStashedHandleHeight = resources.getDimensionPixelSize(
114                 R.dimen.taskbar_stashed_handle_height);
115     }
116 
init(TaskbarControllers controllers)117     public void init(TaskbarControllers controllers) {
118         mControllers = controllers;
119         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
120         Resources resources = mActivity.getResources();
121         if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()) {
122             mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
123             mStashedHandleWidth =
124                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
125         } else {
126             mTaskbarSize = deviceProfile.taskbarHeight;
127             mStashedHandleWidth = resources
128                     .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
129         }
130         int taskbarBottomMargin = deviceProfile.taskbarBottomMargin;
131         mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin;
132 
133         mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue(
134                 mActivity.isPhoneGestureNavMode() ? 1 : 0);
135         mTaskbarStashedHandleHintScale.updateValue(1f);
136 
137         final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
138         mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
139             @Override
140             public void getOutline(View view, Outline outline) {
141                 final int stashedCenterX = view.getWidth() / 2;
142                 final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
143                 mStashedHandleBounds.set(
144                         stashedCenterX - mStashedHandleWidth / 2,
145                         stashedCenterY - mStashedHandleHeight / 2,
146                         stashedCenterX + mStashedHandleWidth / 2,
147                         stashedCenterY + mStashedHandleHeight / 2);
148                 mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
149                 mStashedHandleRadius = view.getHeight() / 2f;
150                 outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
151             }
152         });
153 
154         mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
155             final int stashedCenterX = view.getWidth() / 2;
156             final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
157 
158             view.setPivotX(stashedCenterX);
159             view.setPivotY(stashedCenterY);
160         });
161         initRegionSampler();
162         if (mActivity.isPhoneGestureNavMode()) {
163             onIsStashedChanged(true);
164         }
165     }
166 
167     /**
168      * Returns the stashed handle bounds.
169      * @param out The destination rect.
170      */
getStashedHandleBounds(Rect out)171     public void getStashedHandleBounds(Rect out) {
172         out.set(mStashedHandleBounds);
173     }
174 
initRegionSampler()175     private void initRegionSampler() {
176         mRegionSamplingHelper = new RegionSamplingHelper(mStashedHandleView,
177                 new RegionSamplingHelper.SamplingCallback() {
178                     @Override
179                     public void onRegionDarknessChanged(boolean isRegionDark) {
180                         mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */);
181                         mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY,
182                                 isRegionDark).apply();
183                     }
184 
185                     @Override
186                     public Rect getSampledRegion(View sampledView) {
187                         return mStashedHandleView.getSampledRegion();
188                     }
189                 }, Executors.UI_HELPER_EXECUTOR);
190     }
191 
192 
onDestroy()193     public void onDestroy() {
194         mRegionSamplingHelper.stopAndDestroy();
195         mRegionSamplingHelper = null;
196     }
197 
getStashedHandleAlpha()198     public MultiPropertyFactory<View> getStashedHandleAlpha() {
199         return mTaskbarStashedHandleAlpha;
200     }
201 
getStashedHandleHintScale()202     public AnimatedFloat getStashedHandleHintScale() {
203         return mTaskbarStashedHandleHintScale;
204     }
205 
206     /**
207      * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
208      * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
209      * morphs into the size of where the taskbar icons will be.
210      */
createRevealAnimToIsStashed(boolean isStashed)211     public Animator createRevealAnimToIsStashed(boolean isStashed) {
212         Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
213         float startRadius = mStashedHandleRadius;
214 
215         if (DisplayController.isTransientTaskbar(mActivity)) {
216             // Account for the full visual height of the transient taskbar.
217             int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
218             visualBounds.top -= heightDiff;
219             visualBounds.bottom += heightDiff;
220 
221             startRadius = visualBounds.height() / 2f;
222         }
223 
224         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
225                 startRadius, mStashedHandleRadius, visualBounds, mStashedHandleBounds);
226 
227         boolean isReversed = !isStashed;
228         boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
229         mWasLastRevealAnimReversed = isReversed;
230         if (changingDirection) {
231             mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
232         }
233 
234         ValueAnimator revealAnim = handleRevealProvider.createRevealAnimator(mStashedHandleView,
235                 isReversed, mStartProgressForNextRevealAnim);
236         revealAnim.addListener(new AnimatorListenerAdapter() {
237             @Override
238             public void onAnimationEnd(Animator animation) {
239                 mStartProgressForNextRevealAnim = ((ValueAnimator) animation).getAnimatedFraction();
240             }
241         });
242         return revealAnim;
243     }
244 
245     /** Called when taskbar is stashed or unstashed. */
onIsStashedChanged(boolean isStashed)246     public void onIsStashedChanged(boolean isStashed) {
247         mIsStashed = isStashed;
248         updateSamplingState();
249     }
250 
onNavigationBarLumaSamplingEnabled(int displayId, boolean enable)251     public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
252         if (DEFAULT_DISPLAY != displayId) {
253             return;
254         }
255 
256         mIsLumaSamplingEnabled = enable;
257         updateSamplingState();
258     }
259 
updateSamplingState()260     private void updateSamplingState() {
261         updateRegionSamplingWindowVisibility();
262         if (shouldSample()) {
263             mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
264             mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
265         } else {
266             mRegionSamplingHelper.stop();
267         }
268     }
269 
shouldSample()270     private boolean shouldSample() {
271         return mIsStashed && mIsLumaSamplingEnabled;
272     }
273 
updateStashedHandleHintScale()274     protected void updateStashedHandleHintScale() {
275         mStashedHandleView.setScaleX(mTaskbarStashedHandleHintScale.value);
276         mStashedHandleView.setScaleY(mTaskbarStashedHandleHintScale.value);
277     }
278 
279     /**
280      * Sets the translation of the stashed handle during the swipe up gesture.
281      */
setTranslationYForSwipe(float transY)282     protected void setTranslationYForSwipe(float transY) {
283         mTranslationYForSwipe = transY;
284         updateTranslationY();
285     }
286 
287     /**
288      * Sets the translation of the stashed handle during the spring on stash animation.
289      */
setTranslationYForStash(float transY)290     protected void setTranslationYForStash(float transY) {
291         mTranslationYForStash = transY;
292         updateTranslationY();
293     }
294 
updateTranslationY()295     private void updateTranslationY() {
296         mStashedHandleView.setTranslationY(mTranslationYForSwipe + mTranslationYForStash);
297     }
298 
299     /**
300      * Should be called when the home button is disabled, so we can hide this handle as well.
301      */
setIsHomeButtonDisabled(boolean homeDisabled)302     public void setIsHomeButtonDisabled(boolean homeDisabled) {
303         mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_HOME_DISABLED).setValue(
304                 homeDisabled ? 0 : 1);
305     }
306 
updateStateForSysuiFlags(@ystemUiStateFlags long systemUiStateFlags)307     public void updateStateForSysuiFlags(@SystemUiStateFlags long systemUiStateFlags) {
308         mTaskbarHidden = (systemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
309         updateRegionSamplingWindowVisibility();
310     }
311 
updateRegionSamplingWindowVisibility()312     private void updateRegionSamplingWindowVisibility() {
313         mRegionSamplingHelper.setWindowVisible(shouldSample() && !mTaskbarHidden);
314     }
315 
isStashedHandleVisible()316     public boolean isStashedHandleVisible() {
317         return mStashedHandleView.getVisibility() == View.VISIBLE;
318     }
319 
320     @Override
dumpLogs(String prefix, PrintWriter pw)321     public void dumpLogs(String prefix, PrintWriter pw) {
322         pw.println(prefix + "StashedHandleViewController:");
323 
324         pw.println(prefix + "\tisStashedHandleVisible=" + isStashedHandleVisible());
325         pw.println(prefix + "\tmStashedHandleWidth=" + mStashedHandleWidth);
326         pw.println(prefix + "\tmStashedHandleHeight=" + mStashedHandleHeight);
327         mRegionSamplingHelper.dump(prefix, pw);
328     }
329 
330     @Override
animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs)331     public void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {
332         float targetScale;
333         if (isTouchDown) {
334             targetScale = shrink ? SCALE_TOUCH_ANIMATION_SHRINK : SCALE_TOUCH_ANIMATION_EXPAND;
335         } else {
336             targetScale = 1f;
337         }
338         mStashedHandleView.animateScale(targetScale, durationMs);
339     }
340 
341     @Override
isNavHandleStashedTaskbar()342     public boolean isNavHandleStashedTaskbar() {
343         return true;
344     }
345 
346     @Override
canNavHandleBeLongPressed()347     public boolean canNavHandleBeLongPressed() {
348         return isStashedHandleVisible();
349     }
350 
351     @Override
getNavHandleWidth(Context context)352     public int getNavHandleWidth(Context context) {
353         return mStashedHandleWidth;
354     }
355 }
356