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