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