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