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 com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_ENTER_TRANSITION; 20 import static com.android.internal.jank.InteractionJankMonitor.CUJ_ONE_HANDED_EXIT_TRANSITION; 21 import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; 22 import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; 23 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.os.SystemProperties; 27 import android.text.TextUtils; 28 import android.util.ArrayMap; 29 import android.view.SurfaceControl; 30 import android.window.DisplayAreaAppearedInfo; 31 import android.window.DisplayAreaInfo; 32 import android.window.DisplayAreaOrganizer; 33 import android.window.WindowContainerToken; 34 import android.window.WindowContainerTransaction; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.internal.jank.InteractionJankMonitor; 41 import com.android.wm.shell.R; 42 import com.android.wm.shell.common.DisplayLayout; 43 import com.android.wm.shell.common.ShellExecutor; 44 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Map; 49 50 /** 51 * Manages OneHanded display areas such as offset. 52 * 53 * This class listens on {@link DisplayAreaOrganizer} callbacks for windowing mode change 54 * both to and from OneHanded and issues corresponding animation if applicable. 55 * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running 56 * and files a final {@link WindowContainerTransaction} at the end of the transition. 57 * 58 * This class is also responsible for translating one handed operations within SysUI component 59 */ 60 public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { 61 private static final String TAG = "OneHandedDisplayAreaOrganizer"; 62 private static final String ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION = 63 "persist.debug.one_handed_translate_animation_duration"; 64 65 private DisplayLayout mDisplayLayout = new DisplayLayout(); 66 67 private final Rect mLastVisualDisplayBounds = new Rect(); 68 private final Rect mDefaultDisplayBounds = new Rect(); 69 private final OneHandedSettingsUtil mOneHandedSettingsUtil; 70 private final InteractionJankMonitor mJankMonitor; 71 private final Context mContext; 72 73 private boolean mIsReady; 74 private float mLastVisualOffset = 0; 75 private int mEnterExitAnimationDurationMs; 76 77 private ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = new ArrayMap(); 78 private OneHandedAnimationController mAnimationController; 79 private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory 80 mSurfaceControlTransactionFactory; 81 private OneHandedTutorialHandler mTutorialHandler; 82 private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>(); 83 84 @VisibleForTesting 85 OneHandedAnimationCallback mOneHandedAnimationCallback = 86 new OneHandedAnimationCallback() { 87 @Override 88 public void onOneHandedAnimationStart( 89 OneHandedAnimationController.OneHandedTransitionAnimator animator) { 90 final boolean isEntering = animator.getTransitionDirection() 91 == TRANSITION_DIRECTION_TRIGGER; 92 if (!mTransitionCallbacks.isEmpty()) { 93 for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { 94 final OneHandedTransitionCallback cb = mTransitionCallbacks.get(i); 95 cb.onStartTransition(isEntering); 96 } 97 } 98 } 99 100 @Override 101 public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx, 102 OneHandedAnimationController.OneHandedTransitionAnimator animator) { 103 mAnimationController.removeAnimator(animator.getToken()); 104 final boolean isEntering = animator.getTransitionDirection() 105 == TRANSITION_DIRECTION_TRIGGER; 106 if (mAnimationController.isAnimatorsConsumed()) { 107 endCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION 108 : CUJ_ONE_HANDED_EXIT_TRANSITION); 109 finishOffset((int) animator.getDestinationOffset(), 110 animator.getTransitionDirection()); 111 } 112 } 113 114 @Override 115 public void onOneHandedAnimationCancel( 116 OneHandedAnimationController.OneHandedTransitionAnimator animator) { 117 mAnimationController.removeAnimator(animator.getToken()); 118 final boolean isEntering = animator.getTransitionDirection() 119 == TRANSITION_DIRECTION_TRIGGER; 120 if (mAnimationController.isAnimatorsConsumed()) { 121 cancelCUJTracing(isEntering ? CUJ_ONE_HANDED_ENTER_TRANSITION 122 : CUJ_ONE_HANDED_EXIT_TRANSITION); 123 finishOffset((int) animator.getDestinationOffset(), 124 animator.getTransitionDirection()); 125 } 126 } 127 }; 128 129 /** 130 * Constructor of OneHandedDisplayAreaOrganizer 131 */ OneHandedDisplayAreaOrganizer(Context context, DisplayLayout displayLayout, OneHandedSettingsUtil oneHandedSettingsUtil, OneHandedAnimationController animationController, OneHandedTutorialHandler tutorialHandler, InteractionJankMonitor jankMonitor, ShellExecutor mainExecutor)132 public OneHandedDisplayAreaOrganizer(Context context, 133 DisplayLayout displayLayout, 134 OneHandedSettingsUtil oneHandedSettingsUtil, 135 OneHandedAnimationController animationController, 136 OneHandedTutorialHandler tutorialHandler, 137 InteractionJankMonitor jankMonitor, 138 ShellExecutor mainExecutor) { 139 super(mainExecutor); 140 mContext = context; 141 setDisplayLayout(displayLayout); 142 mOneHandedSettingsUtil = oneHandedSettingsUtil; 143 mAnimationController = animationController; 144 mJankMonitor = jankMonitor; 145 final int animationDurationConfig = context.getResources().getInteger( 146 R.integer.config_one_handed_translate_animation_duration); 147 mEnterExitAnimationDurationMs = 148 SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, 149 animationDurationConfig); 150 mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; 151 mTutorialHandler = tutorialHandler; 152 } 153 154 @Override onDisplayAreaAppeared(@onNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash)155 public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, 156 @NonNull SurfaceControl leash) { 157 leash.setUnreleasedWarningCallSite( 158 "OneHandedSiaplyAreaOrganizer.onDisplayAreaAppeared"); 159 mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); 160 } 161 162 @Override onDisplayAreaVanished(@onNull DisplayAreaInfo displayAreaInfo)163 public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { 164 final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); 165 if (leash != null) { 166 leash.release(); 167 } 168 mDisplayAreaTokenMap.remove(displayAreaInfo.token); 169 } 170 171 @Override registerOrganizer(int displayAreaFeature)172 public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) { 173 final List<DisplayAreaAppearedInfo> displayAreaInfos = 174 super.registerOrganizer(displayAreaFeature); 175 for (int i = 0; i < displayAreaInfos.size(); i++) { 176 final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); 177 onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); 178 } 179 mIsReady = true; 180 updateDisplayBounds(); 181 return displayAreaInfos; 182 } 183 184 @Override unregisterOrganizer()185 public void unregisterOrganizer() { 186 super.unregisterOrganizer(); 187 mIsReady = false; 188 resetWindowsOffset(); 189 } 190 isReady()191 boolean isReady() { 192 return mIsReady; 193 } 194 195 /** 196 * Handler for display rotation changes by {@link DisplayLayout} 197 * 198 * @param context Any context 199 * @param toRotation target rotation of the display (after rotating). 200 * @param wct A task transaction {@link WindowContainerTransaction} from 201 * {@link DisplayChangeController} to populate. 202 */ onRotateDisplay(Context context, int toRotation, WindowContainerTransaction wct)203 public void onRotateDisplay(Context context, int toRotation, WindowContainerTransaction wct) { 204 if (mDisplayLayout.rotation() == toRotation) { 205 return; 206 } 207 mDisplayLayout.rotateTo(context.getResources(), toRotation); 208 updateDisplayBounds(); 209 finishOffset(0, TRANSITION_DIRECTION_EXIT); 210 } 211 212 /** 213 * Offset the windows by a given offset on Y-axis, triggered also from screen rotation. 214 * Directly perform manipulation/offset on the leash. 215 */ scheduleOffset(int xOffset, int yOffset)216 public void scheduleOffset(int xOffset, int yOffset) { 217 final float fromPos = mLastVisualOffset; 218 final int direction = yOffset > 0 219 ? TRANSITION_DIRECTION_TRIGGER 220 : TRANSITION_DIRECTION_EXIT; 221 if (direction == TRANSITION_DIRECTION_TRIGGER) { 222 beginCUJTracing(CUJ_ONE_HANDED_ENTER_TRANSITION, "enterOneHanded"); 223 } else { 224 beginCUJTracing(CUJ_ONE_HANDED_EXIT_TRANSITION, "stopOneHanded"); 225 } 226 mDisplayAreaTokenMap.forEach( 227 (token, leash) -> { 228 animateWindows(token, leash, fromPos, yOffset, direction, 229 mEnterExitAnimationDurationMs); 230 }); 231 mLastVisualOffset = yOffset; 232 } 233 234 @VisibleForTesting resetWindowsOffset()235 void resetWindowsOffset() { 236 final SurfaceControl.Transaction tx = 237 mSurfaceControlTransactionFactory.getTransaction(); 238 mDisplayAreaTokenMap.forEach( 239 (token, leash) -> { 240 final OneHandedAnimationController.OneHandedTransitionAnimator animator = 241 mAnimationController.getAnimatorMap().remove(token); 242 if (animator != null && animator.isRunning()) { 243 animator.cancel(); 244 } 245 tx.setPosition(leash, 0, 0) 246 .setWindowCrop(leash, -1, -1) 247 .setCornerRadius(leash, -1); 248 }); 249 tx.apply(); 250 mLastVisualOffset = 0; 251 mLastVisualDisplayBounds.offsetTo(0, 0); 252 } 253 animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, float toPos, @OneHandedAnimationController.TransitionDirection int direction, int durationMs)254 private void animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, 255 float toPos, @OneHandedAnimationController.TransitionDirection int direction, 256 int durationMs) { 257 final OneHandedAnimationController.OneHandedTransitionAnimator animator = 258 mAnimationController.getAnimator(token, leash, fromPos, toPos, 259 mLastVisualDisplayBounds); 260 if (animator != null) { 261 animator.setTransitionDirection(direction) 262 .addOneHandedAnimationCallback(mOneHandedAnimationCallback) 263 .addOneHandedAnimationCallback(mTutorialHandler) 264 .setDuration(durationMs) 265 .start(); 266 } 267 } 268 269 @VisibleForTesting finishOffset(int offset, @OneHandedAnimationController.TransitionDirection int direction)270 void finishOffset(int offset, @OneHandedAnimationController.TransitionDirection int direction) { 271 if (direction == TRANSITION_DIRECTION_EXIT) { 272 // We must do this to ensure reset property for leash when exit one handed mode 273 resetWindowsOffset(); 274 } 275 mLastVisualOffset = direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0; 276 mLastVisualDisplayBounds.offsetTo(0, Math.round(mLastVisualOffset)); 277 for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { 278 final OneHandedTransitionCallback cb = mTransitionCallbacks.get(i); 279 if (direction == TRANSITION_DIRECTION_TRIGGER) { 280 cb.onStartFinished(getLastVisualDisplayBounds()); 281 } else { 282 cb.onStopFinished(getLastVisualDisplayBounds()); 283 } 284 } 285 } 286 287 /** 288 * The latest visual bounds of displayArea translated 289 * 290 * @return Rect latest finish_offset 291 */ getLastVisualDisplayBounds()292 private Rect getLastVisualDisplayBounds() { 293 return mLastVisualDisplayBounds; 294 } 295 296 @VisibleForTesting 297 @Nullable getLastDisplayBounds()298 Rect getLastDisplayBounds() { 299 return mLastVisualDisplayBounds; 300 } 301 getDisplayLayout()302 public DisplayLayout getDisplayLayout() { 303 return mDisplayLayout; 304 } 305 306 @VisibleForTesting setDisplayLayout(@onNull DisplayLayout displayLayout)307 void setDisplayLayout(@NonNull DisplayLayout displayLayout) { 308 mDisplayLayout.set(displayLayout); 309 updateDisplayBounds(); 310 } 311 312 @VisibleForTesting getDisplayAreaTokenMap()313 ArrayMap<WindowContainerToken, SurfaceControl> getDisplayAreaTokenMap() { 314 return mDisplayAreaTokenMap; 315 } 316 317 @VisibleForTesting updateDisplayBounds()318 void updateDisplayBounds() { 319 mDefaultDisplayBounds.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 320 mLastVisualDisplayBounds.set(mDefaultDisplayBounds); 321 } 322 323 /** 324 * Register transition callback 325 */ registerTransitionCallback(OneHandedTransitionCallback callback)326 public void registerTransitionCallback(OneHandedTransitionCallback callback) { 327 mTransitionCallbacks.add(callback); 328 } 329 beginCUJTracing(@nteractionJankMonitor.CujType int cujType, @Nullable String tag)330 void beginCUJTracing(@InteractionJankMonitor.CujType int cujType, @Nullable String tag) { 331 final Map.Entry<WindowContainerToken, SurfaceControl> firstEntry = 332 getDisplayAreaTokenMap().entrySet().iterator().next(); 333 final InteractionJankMonitor.Configuration.Builder builder = 334 InteractionJankMonitor.Configuration.Builder.withSurface( 335 cujType, mContext, firstEntry.getValue()); 336 if (!TextUtils.isEmpty(tag)) { 337 builder.setTag(tag); 338 } 339 mJankMonitor.begin(builder); 340 } 341 endCUJTracing(@nteractionJankMonitor.CujType int cujType)342 void endCUJTracing(@InteractionJankMonitor.CujType int cujType) { 343 mJankMonitor.end(cujType); 344 } 345 cancelCUJTracing(@nteractionJankMonitor.CujType int cujType)346 void cancelCUJTracing(@InteractionJankMonitor.CujType int cujType) { 347 mJankMonitor.cancel(cujType); 348 } 349 dump(@onNull PrintWriter pw)350 void dump(@NonNull PrintWriter pw) { 351 final String innerPrefix = " "; 352 pw.println(TAG); 353 pw.print(innerPrefix + "mDisplayLayout.rotation()="); 354 pw.println(mDisplayLayout.rotation()); 355 pw.print(innerPrefix + "mDisplayAreaTokenMap="); 356 pw.println(mDisplayAreaTokenMap); 357 pw.print(innerPrefix + "mDefaultDisplayBounds="); 358 pw.println(mDefaultDisplayBounds); 359 pw.print(innerPrefix + "mIsReady="); 360 pw.println(mIsReady); 361 pw.print(innerPrefix + "mLastVisualDisplayBounds="); 362 pw.println(mLastVisualDisplayBounds); 363 pw.print(innerPrefix + "mLastVisualOffset="); 364 pw.println(mLastVisualOffset); 365 366 if (mAnimationController != null) { 367 mAnimationController.dump(pw); 368 } 369 } 370 } 371