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 mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); 158 } 159 160 @Override onDisplayAreaVanished(@onNull DisplayAreaInfo displayAreaInfo)161 public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { 162 final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); 163 if (leash != null) { 164 leash.release(); 165 } 166 mDisplayAreaTokenMap.remove(displayAreaInfo.token); 167 } 168 169 @Override registerOrganizer(int displayAreaFeature)170 public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) { 171 final List<DisplayAreaAppearedInfo> displayAreaInfos = 172 super.registerOrganizer(displayAreaFeature); 173 for (int i = 0; i < displayAreaInfos.size(); i++) { 174 final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); 175 onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); 176 } 177 mIsReady = true; 178 updateDisplayBounds(); 179 return displayAreaInfos; 180 } 181 182 @Override unregisterOrganizer()183 public void unregisterOrganizer() { 184 super.unregisterOrganizer(); 185 mIsReady = false; 186 resetWindowsOffset(); 187 } 188 isReady()189 boolean isReady() { 190 return mIsReady; 191 } 192 193 /** 194 * Handler for display rotation changes by {@link DisplayLayout} 195 * 196 * @param context Any context 197 * @param toRotation target rotation of the display (after rotating). 198 * @param wct A task transaction {@link WindowContainerTransaction} from 199 * {@link DisplayChangeController} to populate. 200 */ onRotateDisplay(Context context, int toRotation, WindowContainerTransaction wct)201 public void onRotateDisplay(Context context, int toRotation, WindowContainerTransaction wct) { 202 if (mDisplayLayout.rotation() == toRotation) { 203 return; 204 } 205 mDisplayLayout.rotateTo(context.getResources(), toRotation); 206 updateDisplayBounds(); 207 finishOffset(0, TRANSITION_DIRECTION_EXIT); 208 } 209 210 /** 211 * Offset the windows by a given offset on Y-axis, triggered also from screen rotation. 212 * Directly perform manipulation/offset on the leash. 213 */ scheduleOffset(int xOffset, int yOffset)214 public void scheduleOffset(int xOffset, int yOffset) { 215 final float fromPos = mLastVisualOffset; 216 final int direction = yOffset > 0 217 ? TRANSITION_DIRECTION_TRIGGER 218 : TRANSITION_DIRECTION_EXIT; 219 if (direction == TRANSITION_DIRECTION_TRIGGER) { 220 beginCUJTracing(CUJ_ONE_HANDED_ENTER_TRANSITION, "enterOneHanded"); 221 } else { 222 beginCUJTracing(CUJ_ONE_HANDED_EXIT_TRANSITION, "stopOneHanded"); 223 } 224 mDisplayAreaTokenMap.forEach( 225 (token, leash) -> { 226 animateWindows(token, leash, fromPos, yOffset, direction, 227 mEnterExitAnimationDurationMs); 228 }); 229 mLastVisualOffset = yOffset; 230 } 231 232 @VisibleForTesting resetWindowsOffset()233 void resetWindowsOffset() { 234 final SurfaceControl.Transaction tx = 235 mSurfaceControlTransactionFactory.getTransaction(); 236 mDisplayAreaTokenMap.forEach( 237 (token, leash) -> { 238 final OneHandedAnimationController.OneHandedTransitionAnimator animator = 239 mAnimationController.getAnimatorMap().remove(token); 240 if (animator != null && animator.isRunning()) { 241 animator.cancel(); 242 } 243 tx.setPosition(leash, 0, 0) 244 .setWindowCrop(leash, -1, -1) 245 .setCornerRadius(leash, -1); 246 }); 247 tx.apply(); 248 mLastVisualOffset = 0; 249 mLastVisualDisplayBounds.offsetTo(0, 0); 250 } 251 animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, float toPos, @OneHandedAnimationController.TransitionDirection int direction, int durationMs)252 private void animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, 253 float toPos, @OneHandedAnimationController.TransitionDirection int direction, 254 int durationMs) { 255 final OneHandedAnimationController.OneHandedTransitionAnimator animator = 256 mAnimationController.getAnimator(token, leash, fromPos, toPos, 257 mLastVisualDisplayBounds); 258 if (animator != null) { 259 animator.setTransitionDirection(direction) 260 .addOneHandedAnimationCallback(mOneHandedAnimationCallback) 261 .addOneHandedAnimationCallback(mTutorialHandler) 262 .setDuration(durationMs) 263 .start(); 264 } 265 } 266 267 @VisibleForTesting finishOffset(int offset, @OneHandedAnimationController.TransitionDirection int direction)268 void finishOffset(int offset, @OneHandedAnimationController.TransitionDirection int direction) { 269 if (direction == TRANSITION_DIRECTION_EXIT) { 270 // We must do this to ensure reset property for leash when exit one handed mode 271 resetWindowsOffset(); 272 } 273 mLastVisualOffset = direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0; 274 mLastVisualDisplayBounds.offsetTo(0, Math.round(mLastVisualOffset)); 275 for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) { 276 final OneHandedTransitionCallback cb = mTransitionCallbacks.get(i); 277 if (direction == TRANSITION_DIRECTION_TRIGGER) { 278 cb.onStartFinished(getLastVisualDisplayBounds()); 279 } else { 280 cb.onStopFinished(getLastVisualDisplayBounds()); 281 } 282 } 283 } 284 285 /** 286 * The latest visual bounds of displayArea translated 287 * 288 * @return Rect latest finish_offset 289 */ getLastVisualDisplayBounds()290 private Rect getLastVisualDisplayBounds() { 291 return mLastVisualDisplayBounds; 292 } 293 294 @VisibleForTesting 295 @Nullable getLastDisplayBounds()296 Rect getLastDisplayBounds() { 297 return mLastVisualDisplayBounds; 298 } 299 getDisplayLayout()300 public DisplayLayout getDisplayLayout() { 301 return mDisplayLayout; 302 } 303 304 @VisibleForTesting setDisplayLayout(@onNull DisplayLayout displayLayout)305 void setDisplayLayout(@NonNull DisplayLayout displayLayout) { 306 mDisplayLayout.set(displayLayout); 307 updateDisplayBounds(); 308 } 309 310 @VisibleForTesting getDisplayAreaTokenMap()311 ArrayMap<WindowContainerToken, SurfaceControl> getDisplayAreaTokenMap() { 312 return mDisplayAreaTokenMap; 313 } 314 315 @VisibleForTesting updateDisplayBounds()316 void updateDisplayBounds() { 317 mDefaultDisplayBounds.set(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 318 mLastVisualDisplayBounds.set(mDefaultDisplayBounds); 319 } 320 321 /** 322 * Register transition callback 323 */ registerTransitionCallback(OneHandedTransitionCallback callback)324 public void registerTransitionCallback(OneHandedTransitionCallback callback) { 325 mTransitionCallbacks.add(callback); 326 } 327 beginCUJTracing(@nteractionJankMonitor.CujType int cujType, @Nullable String tag)328 void beginCUJTracing(@InteractionJankMonitor.CujType int cujType, @Nullable String tag) { 329 final Map.Entry<WindowContainerToken, SurfaceControl> firstEntry = 330 getDisplayAreaTokenMap().entrySet().iterator().next(); 331 final InteractionJankMonitor.Configuration.Builder builder = 332 InteractionJankMonitor.Configuration.Builder.withSurface( 333 cujType, mContext, firstEntry.getValue()); 334 if (!TextUtils.isEmpty(tag)) { 335 builder.setTag(tag); 336 } 337 mJankMonitor.begin(builder); 338 } 339 endCUJTracing(@nteractionJankMonitor.CujType int cujType)340 void endCUJTracing(@InteractionJankMonitor.CujType int cujType) { 341 mJankMonitor.end(cujType); 342 } 343 cancelCUJTracing(@nteractionJankMonitor.CujType int cujType)344 void cancelCUJTracing(@InteractionJankMonitor.CujType int cujType) { 345 mJankMonitor.cancel(cujType); 346 } 347 dump(@onNull PrintWriter pw)348 void dump(@NonNull PrintWriter pw) { 349 final String innerPrefix = " "; 350 pw.println(TAG); 351 pw.print(innerPrefix + "mDisplayLayout.rotation()="); 352 pw.println(mDisplayLayout.rotation()); 353 pw.print(innerPrefix + "mDisplayAreaTokenMap="); 354 pw.println(mDisplayAreaTokenMap); 355 pw.print(innerPrefix + "mDefaultDisplayBounds="); 356 pw.println(mDefaultDisplayBounds); 357 pw.print(innerPrefix + "mIsReady="); 358 pw.println(mIsReady); 359 pw.print(innerPrefix + "mLastVisualDisplayBounds="); 360 pw.println(mLastVisualDisplayBounds); 361 pw.print(innerPrefix + "mLastVisualOffset="); 362 pw.println(mLastVisualOffset); 363 364 if (mAnimationController != null) { 365 mAnimationController.dump(pw); 366 } 367 } 368 } 369