• 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 
17 package com.android.wm.shell.onehanded;
18 
19 import static android.view.View.LAYER_TYPE_HARDWARE;
20 import static android.view.View.LAYER_TYPE_NONE;
21 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
22 
23 import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE;
24 import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING;
25 import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING;
26 import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE;
27 
28 import android.animation.ValueAnimator;
29 import android.annotation.Nullable;
30 import android.content.Context;
31 import android.content.res.ColorStateList;
32 import android.content.res.TypedArray;
33 import android.graphics.PixelFormat;
34 import android.graphics.Rect;
35 import android.view.Gravity;
36 import android.view.LayoutInflater;
37 import android.view.SurfaceControl;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.WindowManager;
41 import android.view.animation.LinearInterpolator;
42 import android.widget.FrameLayout;
43 import android.widget.ImageView;
44 import android.widget.TextView;
45 
46 import androidx.annotation.NonNull;
47 import androidx.appcompat.view.ContextThemeWrapper;
48 
49 import com.android.internal.annotations.VisibleForTesting;
50 import com.android.wm.shell.R;
51 import com.android.wm.shell.common.DisplayLayout;
52 
53 import java.io.PrintWriter;
54 
55 /**
56  * Handles tutorial visibility and synchronized transition for One Handed operations,
57  * TargetViewContainer only be created and always attach to window,
58  * detach TargetViewContainer from window after exiting one handed mode.
59  */
60 public class OneHandedTutorialHandler implements OneHandedTransitionCallback,
61         OneHandedState.OnStateChangedListener, OneHandedAnimationCallback {
62     private static final String TAG = "OneHandedTutorialHandler";
63     private static final float START_TRANSITION_FRACTION = 0.6f;
64 
65     private final float mTutorialHeightRatio;
66     private final WindowManager mWindowManager;
67 
68     private @OneHandedState.State int mCurrentState;
69     private int mTutorialAreaHeight;
70 
71     private Context mContext;
72     private Rect mDisplayBounds;
73     private ValueAnimator mAlphaAnimator;
74     private @Nullable View mTutorialView;
75     private @Nullable ViewGroup mTargetViewContainer;
76 
77     private float mAlphaTransitionStart;
78     private int mAlphaAnimationDurationMs;
79 
OneHandedTutorialHandler(Context context, OneHandedSettingsUtil settingsUtil, WindowManager windowManager)80     public OneHandedTutorialHandler(Context context, OneHandedSettingsUtil settingsUtil,
81             WindowManager windowManager) {
82         mContext = context;
83         mWindowManager = windowManager;
84         mTutorialHeightRatio = settingsUtil.getTranslationFraction(context);
85         mAlphaAnimationDurationMs = settingsUtil.getTransitionDuration(context);
86     }
87 
88     @Override
onOneHandedAnimationCancel( OneHandedAnimationController.OneHandedTransitionAnimator animator)89     public void onOneHandedAnimationCancel(
90             OneHandedAnimationController.OneHandedTransitionAnimator animator) {
91         if (mAlphaAnimator != null) {
92             mAlphaAnimator.cancel();
93         }
94     }
95 
96     @Override
onAnimationUpdate(SurfaceControl.Transaction tx, float xPos, float yPos)97     public void onAnimationUpdate(SurfaceControl.Transaction tx, float xPos, float yPos) {
98         if (!isAttached()) {
99             return;
100         }
101         if (yPos < mAlphaTransitionStart) {
102             checkTransitionEnd();
103             return;
104         }
105         if (mAlphaAnimator == null || mAlphaAnimator.isStarted() || mAlphaAnimator.isRunning()) {
106             return;
107         }
108         mAlphaAnimator.start();
109     }
110 
111     @Override
onStateChanged(int newState)112     public void onStateChanged(int newState) {
113         mCurrentState = newState;
114         switch (newState) {
115             case STATE_ENTERING:
116                 createViewAndAttachToWindow(mContext);
117                 updateThemeColor();
118                 setupAlphaTransition(true /* isEntering */);
119                 break;
120             case STATE_ACTIVE:
121                 checkTransitionEnd();
122                 setupAlphaTransition(false /* isEntering */);
123                 break;
124             case STATE_EXITING:
125             case STATE_NONE:
126                 checkTransitionEnd();
127                 removeTutorialFromWindowManager();
128                 break;
129             default:
130                 break;
131         }
132     }
133 
134     /**
135      * Called when onDisplayAdded() or onDisplayRemoved() callback.
136      *
137      * @param displayLayout The latest {@link DisplayLayout} representing current displayId
138      */
onDisplayChanged(DisplayLayout displayLayout)139     public void onDisplayChanged(DisplayLayout displayLayout) {
140         // Ensure the mDisplayBounds is portrait, due to OHM only support on portrait
141         if (displayLayout.height() > displayLayout.width()) {
142             mDisplayBounds = new Rect(0, 0, displayLayout.width(), displayLayout.height());
143         } else {
144             mDisplayBounds = new Rect(0, 0, displayLayout.height(), displayLayout.width());
145         }
146         mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio);
147         mAlphaTransitionStart = mTutorialAreaHeight * START_TRANSITION_FRACTION;
148     }
149 
150     @VisibleForTesting
createViewAndAttachToWindow(Context context)151     void createViewAndAttachToWindow(Context context) {
152         if (isAttached()) {
153             return;
154         }
155         mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, null);
156         mTargetViewContainer = new FrameLayout(context);
157         mTargetViewContainer.setClipChildren(false);
158         mTargetViewContainer.setAlpha(mCurrentState == STATE_ACTIVE ? 1.0f : 0.0f);
159         mTargetViewContainer.addView(mTutorialView);
160         mTargetViewContainer.setLayerType(LAYER_TYPE_HARDWARE, null);
161 
162         attachTargetToWindow();
163     }
164 
165     /**
166      * Adds the tutorial target view to the WindowManager and update its layout.
167      */
attachTargetToWindow()168     private void attachTargetToWindow() {
169         try {
170             mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams());
171         } catch (IllegalStateException e) {
172             // This shouldn't happen, but if the target is already added, just update its
173             // layout params.
174             mWindowManager.updateViewLayout(mTargetViewContainer, getTutorialTargetLayoutParams());
175         }
176     }
177 
178     @VisibleForTesting
removeTutorialFromWindowManager()179     void removeTutorialFromWindowManager() {
180         if (!isAttached()) {
181             return;
182         }
183         mTargetViewContainer.setLayerType(LAYER_TYPE_NONE, null);
184         mWindowManager.removeViewImmediate(mTargetViewContainer);
185         mTargetViewContainer = null;
186     }
187 
188     /**
189      * Returns layout params for the dismiss target, using the latest display metrics.
190      */
getTutorialTargetLayoutParams()191     private WindowManager.LayoutParams getTutorialTargetLayoutParams() {
192         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
193                 mDisplayBounds.width(), mTutorialAreaHeight, 0, 0,
194                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
195                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
196                     | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
197                     PixelFormat.TRANSLUCENT);
198         lp.gravity = Gravity.TOP | Gravity.LEFT;
199         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
200         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
201         lp.setFitInsetsTypes(0 /* types */);
202         lp.setTitle("one-handed-tutorial-overlay");
203         return lp;
204     }
205 
206     @VisibleForTesting
isAttached()207     boolean isAttached() {
208         return mTargetViewContainer != null && mTargetViewContainer.isAttachedToWindow();
209     }
210 
211     /**
212      * onConfigurationChanged events for updating tutorial text.
213      */
onConfigurationChanged()214     public void onConfigurationChanged() {
215         removeTutorialFromWindowManager();
216         if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) {
217             createViewAndAttachToWindow(mContext);
218             updateThemeColor();
219             checkTransitionEnd();
220         }
221     }
222 
updateThemeColor()223     private void updateThemeColor() {
224         if (mTutorialView == null) {
225             return;
226         }
227 
228         final Context themedContext = new ContextThemeWrapper(mTutorialView.getContext(),
229                 com.android.internal.R.style.Theme_DeviceDefault_DayNight);
230         final int textColorPrimary;
231         final int themedTextColorSecondary;
232         TypedArray ta = themedContext.obtainStyledAttributes(new int[]{
233                 com.android.internal.R.attr.textColorPrimary,
234                 com.android.internal.R.attr.textColorSecondary});
235         textColorPrimary = ta.getColor(0, 0);
236         themedTextColorSecondary = ta.getColor(1, 0);
237         ta.recycle();
238 
239         final ImageView iconView = mTutorialView.findViewById(R.id.one_handed_tutorial_image);
240         iconView.setImageTintList(ColorStateList.valueOf(textColorPrimary));
241 
242         final TextView tutorialTitle = mTutorialView.findViewById(R.id.one_handed_tutorial_title);
243         final TextView tutorialDesc = mTutorialView.findViewById(
244                 R.id.one_handed_tutorial_description);
245         tutorialTitle.setTextColor(textColorPrimary);
246         tutorialDesc.setTextColor(themedTextColorSecondary);
247     }
248 
setupAlphaTransition(boolean isEntering)249     private void setupAlphaTransition(boolean isEntering) {
250         final float start = isEntering ? 0.0f : 1.0f;
251         final float end = isEntering ? 1.0f : 0.0f;
252         final int duration = isEntering ? mAlphaAnimationDurationMs : Math.round(
253                 mAlphaAnimationDurationMs * (1.0f - mTutorialHeightRatio));
254         mAlphaAnimator = ValueAnimator.ofFloat(start, end);
255         mAlphaAnimator.setInterpolator(new LinearInterpolator());
256         mAlphaAnimator.setDuration(duration);
257         mAlphaAnimator.addUpdateListener(
258                 animator -> mTargetViewContainer.setAlpha((float) animator.getAnimatedValue()));
259     }
260 
checkTransitionEnd()261     private void checkTransitionEnd() {
262         if (mAlphaAnimator != null && (mAlphaAnimator.isRunning() || mAlphaAnimator.isStarted())) {
263             mAlphaAnimator.end();
264             mAlphaAnimator.removeAllUpdateListeners();
265             mAlphaAnimator = null;
266         }
267     }
268 
dump(@onNull PrintWriter pw)269     void dump(@NonNull PrintWriter pw) {
270         final String innerPrefix = "  ";
271         pw.println(TAG);
272         pw.print(innerPrefix + "isAttached=");
273         pw.println(isAttached());
274         pw.print(innerPrefix + "mCurrentState=");
275         pw.println(mCurrentState);
276         pw.print(innerPrefix + "mDisplayBounds=");
277         pw.println(mDisplayBounds);
278         pw.print(innerPrefix + "mTutorialAreaHeight=");
279         pw.println(mTutorialAreaHeight);
280         pw.print(innerPrefix + "mAlphaTransitionStart=");
281         pw.println(mAlphaTransitionStart);
282         pw.print(innerPrefix + "mAlphaAnimationDurationMs=");
283         pw.println(mAlphaAnimationDurationMs);
284     }
285 }
286