1 /*
2  * Copyright (C) 2021 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.car.carlauncher.displayarea;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import static com.android.car.carlauncher.CarLauncher.TAG;
22 import static com.android.car.carlauncher.displayarea.CarDisplayAreaController.BACKGROUND_LAYER_INDEX;
23 import static com.android.car.carlauncher.displayarea.CarDisplayAreaController.FOREGROUND_LAYER_INDEX;
24 
25 import android.app.ActivityOptions;
26 import android.car.Car;
27 import android.car.app.CarActivityManager;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.res.Resources;
32 import android.graphics.Rect;
33 import android.hardware.display.DisplayManager;
34 import android.os.Handler;
35 import android.util.ArrayMap;
36 import android.util.DisplayMetrics;
37 import android.util.Log;
38 import android.view.Display;
39 import android.view.SurfaceControl;
40 import android.window.DisplayAreaAppearedInfo;
41 import android.window.DisplayAreaInfo;
42 import android.window.DisplayAreaOrganizer;
43 import android.window.WindowContainerToken;
44 import android.window.WindowContainerTransaction;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.VisibleForTesting;
48 
49 import com.android.car.carlauncher.AppGridActivity;
50 import com.android.car.carlauncher.R;
51 import com.android.wm.shell.common.SyncTransactionQueue;
52 
53 import java.util.List;
54 import java.util.concurrent.Executor;
55 
56 import javax.annotation.Nullable;
57 
58 /**
59  * Organizer for controlling the policies defined in
60  * {@link com.android.server.wm.CarDisplayAreaPolicyProvider}
61  */
62 public class CarDisplayAreaOrganizer extends DisplayAreaOrganizer {
63 
64     /**
65      * The display partition to launch applications by default.
66      */
67     public static final int FOREGROUND_DISPLAY_AREA_ROOT = FEATURE_VENDOR_FIRST + 1;
68 
69     /**
70      * Background applications task container.
71      */
72     public static final int BACKGROUND_TASK_CONTAINER = FEATURE_VENDOR_FIRST + 2;
73 
74     private static final int FEATURE_TASKDISPLAYAREA_PARENT = FEATURE_VENDOR_FIRST + 3;
75 
76     /**
77      * Control bar task container.
78      */
79     public static final int CONTROL_BAR_DISPLAY_AREA = FEATURE_VENDOR_FIRST + 4;
80 
81     public static final int FEATURE_TITLE_BAR = FEATURE_VENDOR_FIRST + 5;
82 
83     static final int FEATURE_VOICE_PLATE = FEATURE_VENDOR_FIRST + 6;
84 
85     @Nullable
86     private final ComponentName mAssistantVoicePlateActivityName;
87 
88     private static CarDisplayAreaOrganizer sCarDisplayAreaOrganizer;
89 
90     private final Context mContext;
91     private final Intent mMapsIntent;
92     private final SyncTransactionQueue mTransactionQueue;
93     private final Rect mBackgroundApplicationDisplayBounds = new Rect();
94     private final Intent mAudioControlIntent;
95     private boolean mIsShowingBackgroundDisplay;
96     private boolean mIsShowingControlBarDisplay;
97     private final CarLauncherDisplayAreaAnimationController mAnimationController;
98     private final Handler mHandlerForAnimation;
99     private final Rect mLastVisualDisplayBounds = new Rect();
100     private final ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
101             new ArrayMap();
102 
103     private WindowContainerToken mBackgroundDisplayToken;
104     private WindowContainerToken mForegroundDisplayToken;
105     private WindowContainerToken mControbarDisplayToken;
106     private WindowContainerToken mTitleBarDisplayToken;
107     private WindowContainerToken mVoicePlateDisplayToken;
108     private int mDpiDensity = -1;
109     private DisplayAreaAppearedInfo mBackgroundApplicationDisplay;
110     private DisplayAreaAppearedInfo mForegroundApplicationDisplay;
111     private DisplayAreaAppearedInfo mTitleBarDisplay;
112     private DisplayAreaAppearedInfo mControlBarDisplay;
113     private boolean mIsRegistered = false;
114     private boolean mIsDisplayAreaAnimating = false;
115 
116     private AppGridActivity.CAR_LAUNCHER_STATE mToState;
117     DisplayAreaAnimationRunnable mDisplayAreaAnimationRunnable = null;
118 
119     @VisibleForTesting
120     CarLauncherDisplayAreaAnimationCallback mDisplayAreaAnimationCallback =
121             new CarLauncherDisplayAreaAnimationCallback() {
122                 @Override
123                 public void onAnimationStart(
124                         CarLauncherDisplayAreaAnimationController
125                                 .CarLauncherDisplayAreaTransitionAnimator animator) {
126 
127                     mIsDisplayAreaAnimating = true;
128                     SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
129                     // Update the foreground panel layer index to animate on top of the
130                     // background DA.
131                     tx.setLayer(mForegroundApplicationDisplay.getLeash(),
132                             BACKGROUND_LAYER_INDEX + 1);
133                     tx.apply(true);
134                 }
135 
136                 @Override
137                 public void onAnimationEnd(SurfaceControl.Transaction tx,
138                         CarLauncherDisplayAreaAnimationController
139                                 .CarLauncherDisplayAreaTransitionAnimator animator) {
140                     mIsDisplayAreaAnimating = false;
141                     mAnimationController.removeAnimator(animator.getToken());
142                     if (mAnimationController.isAnimatorsConsumed()) {
143                         WindowContainerTransaction wct = new WindowContainerTransaction();
144 
145                         if (mToState == AppGridActivity.CAR_LAUNCHER_STATE.DEFAULT) {
146                             // Foreground DA opens to default height.
147                             updateBackgroundDisplayBounds(wct);
148                         }
149                     }
150                 }
151 
152                 @Override
153                 public void onAnimationCancel(
154                         CarLauncherDisplayAreaAnimationController
155                                 .CarLauncherDisplayAreaTransitionAnimator animator) {
156                     mIsDisplayAreaAnimating = false;
157                     mAnimationController.removeAnimator(animator.getToken());
158                 }
159             };
160 
161     /**
162      * Gets the instance of {@link CarDisplayAreaOrganizer}.
163      */
getInstance(Executor executor, Context context, Intent mapsIntent, Intent audioControlIntent, SyncTransactionQueue tx)164     public static CarDisplayAreaOrganizer getInstance(Executor executor,
165             Context context, Intent mapsIntent, Intent audioControlIntent,
166             SyncTransactionQueue tx) {
167         if (sCarDisplayAreaOrganizer == null) {
168             sCarDisplayAreaOrganizer = new CarDisplayAreaOrganizer(executor,
169                     context, mapsIntent, audioControlIntent, tx);
170         }
171         return sCarDisplayAreaOrganizer;
172     }
173 
CarDisplayAreaOrganizer(Executor executor, Context context, Intent mapsIntent, Intent audioControlIntent, SyncTransactionQueue tx)174     private CarDisplayAreaOrganizer(Executor executor, Context context, Intent mapsIntent,
175             Intent audioControlIntent, SyncTransactionQueue tx) {
176         super(executor);
177         mContext = context;
178         mMapsIntent = mapsIntent;
179         mAudioControlIntent = audioControlIntent;
180         mTransactionQueue = tx;
181         // TODO(b/201712747): Gets the Assistant Activity by resolving the indirect Intent.
182         mAssistantVoicePlateActivityName = ComponentName.unflattenFromString(
183                 context.getResources().getString(R.string.config_assistantVoicePlateActivity));
184         mAnimationController = new CarLauncherDisplayAreaAnimationController(mContext);
185         mHandlerForAnimation = mContext.getMainThreadHandler();
186 
187         Car.createCar(context, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
188                 mCarServiceLifecycleListener);
189     }
190 
191     private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener =
192             new Car.CarServiceLifecycleListener() {
193                 @Override
194                 public void onLifecycleChanged(@NonNull Car car, boolean ready) {
195                     if (ready) {
196                         CarActivityManager carAm = (CarActivityManager) car.getCarManager(
197                                 Car.CAR_ACTIVITY_SERVICE);
198                         setPersistentActivity(carAm, mMapsIntent.getComponent(),
199                                 BACKGROUND_TASK_CONTAINER, "Background");
200                         setPersistentActivity(carAm, mAssistantVoicePlateActivityName,
201                                 FEATURE_VOICE_PLATE, "VoicePlate");
202                     }
203                 }
204             };
205 
setPersistentActivity(CarActivityManager am, @Nullable ComponentName activity, int featureId, String featureName)206     private static void setPersistentActivity(CarActivityManager am,
207             @Nullable ComponentName activity, int featureId, String featureName) {
208         if (activity == null) {
209             Log.e(TAG, "Empty activity for " + featureName + " (" + featureId + ")");
210             return;
211         }
212         int ret = am.setPersistentActivity(activity, DEFAULT_DISPLAY, featureId);
213         if (ret != CarActivityManager.RESULT_SUCCESS) {
214             Log.e(TAG, "Failed to set PersistentActivity: activity=" + activity
215                     + ", ret=" + ret);
216             return;
217         }
218     }
219 
getDpiDensity()220     int getDpiDensity() {
221         if (mDpiDensity != -1) {
222             return mDpiDensity;
223         }
224 
225         DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
226         Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
227         Resources displayResources = mContext.createDisplayContext(display).getResources();
228         mDpiDensity = displayResources.getConfiguration().densityDpi;
229 
230         return mDpiDensity;
231     }
232 
isDisplayAreaAnimating()233     boolean isDisplayAreaAnimating() {
234         return mIsDisplayAreaAnimating;
235     }
236 
updateBackgroundDisplayBounds(WindowContainerTransaction wct)237     private void updateBackgroundDisplayBounds(WindowContainerTransaction wct) {
238         Rect backgroundApplicationDisplayBound = mBackgroundApplicationDisplayBounds;
239         WindowContainerToken backgroundDisplayToken =
240                 mBackgroundApplicationDisplay.getDisplayAreaInfo().token;
241 
242         int backgroundDisplayWidthDp =
243                 backgroundApplicationDisplayBound.width() * DisplayMetrics.DENSITY_DEFAULT
244                         / getDpiDensity();
245         int backgroundDisplayHeightDp =
246                 backgroundApplicationDisplayBound.height() * DisplayMetrics.DENSITY_DEFAULT
247                         / getDpiDensity();
248         wct.setBounds(backgroundDisplayToken, backgroundApplicationDisplayBound);
249         wct.setScreenSizeDp(backgroundDisplayToken, backgroundDisplayWidthDp,
250                 backgroundDisplayHeightDp);
251         wct.setSmallestScreenWidthDp(backgroundDisplayToken,
252                 Math.min(backgroundDisplayWidthDp, backgroundDisplayHeightDp));
253 
254         mTransactionQueue.runInSync(t -> {
255             t.setPosition(mBackgroundApplicationDisplay.getLeash(),
256                     backgroundApplicationDisplayBound.left,
257                     backgroundApplicationDisplayBound.top);
258         });
259 
260         applyTransaction(wct);
261     }
262 
resetWindowsOffset()263     void resetWindowsOffset() {
264         SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
265         mDisplayAreaTokenMap.forEach(
266                 (token, leash) -> {
267                     CarLauncherDisplayAreaAnimationController
268                             .CarLauncherDisplayAreaTransitionAnimator animator =
269                             mAnimationController.getAnimatorMap().remove(token);
270                     if (animator != null && animator.isRunning()) {
271                         animator.cancel();
272                     }
273                     tx.setPosition(leash, /* x= */ 0, /* y= */ 0)
274                             .setWindowCrop(leash, /* width= */ -1, /* height= */ -1)
275                             .setCornerRadius(leash, /* cornerRadius= */ -1);
276                 });
277         tx.apply();
278     }
279 
280     /**
281      * Offsets the windows by a given offset on Y-axis, triggered also from screen rotation.
282      * Directly perform manipulation/offset on the leash.
283      */
scheduleOffset(int fromPos, int toPos, Rect finalBackgroundBounds, DisplayAreaAppearedInfo backgroundApplicationDisplay, DisplayAreaAppearedInfo foregroundDisplay, DisplayAreaAppearedInfo titleBarDisplay, DisplayAreaAppearedInfo controlBarDisplay, AppGridActivity.CAR_LAUNCHER_STATE toState, int durationMs)284     void scheduleOffset(int fromPos, int toPos, Rect finalBackgroundBounds,
285             DisplayAreaAppearedInfo backgroundApplicationDisplay,
286             DisplayAreaAppearedInfo foregroundDisplay,
287             DisplayAreaAppearedInfo titleBarDisplay,
288             DisplayAreaAppearedInfo controlBarDisplay,
289             AppGridActivity.CAR_LAUNCHER_STATE toState,
290             int durationMs) {
291         mToState = toState;
292         mBackgroundApplicationDisplay = backgroundApplicationDisplay;
293         mForegroundApplicationDisplay = foregroundDisplay;
294         mControlBarDisplay = controlBarDisplay;
295         mTitleBarDisplay = titleBarDisplay;
296         mDisplayAreaTokenMap.forEach(
297                 (token, leash) -> {
298                     if (token == mBackgroundDisplayToken) {
299                         mBackgroundApplicationDisplayBounds.set(finalBackgroundBounds);
300                     } else if (token == mForegroundDisplayToken) {
301                         animateWindows(token, leash, fromPos, toPos, durationMs);
302                     }
303                 });
304 
305         if (mToState == AppGridActivity.CAR_LAUNCHER_STATE.CONTROL_BAR) {
306             WindowContainerTransaction wct = new WindowContainerTransaction();
307             updateBackgroundDisplayBounds(wct);
308         }
309     }
310 
animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos, float toPos, int durationMs)311     void animateWindows(WindowContainerToken token, SurfaceControl leash, float fromPos,
312             float toPos, int durationMs) {
313         CarLauncherDisplayAreaAnimationController.CarLauncherDisplayAreaTransitionAnimator
314                 animator =
315                 mAnimationController.getAnimator(token, leash, fromPos, toPos,
316                         mLastVisualDisplayBounds);
317 
318 
319         if (animator != null) {
320             if (mDisplayAreaAnimationRunnable != null) {
321                 mDisplayAreaAnimationRunnable.stopAnimation();
322                 mHandlerForAnimation.removeCallbacks(mDisplayAreaAnimationRunnable);
323             }
324             mDisplayAreaAnimationRunnable = new DisplayAreaAnimationRunnable(animator, durationMs);
325             mHandlerForAnimation.post(mDisplayAreaAnimationRunnable);
326         }
327     }
328 
329     /**
330      * A custom runnable with a flag to stop running the code within the {@link #run()} method when
331      * the runnable is in the message queue. In such cases calling
332      * {@link #removeCallbacksAndMessages(null)} won't work it only stops pending messages
333      * (Runnables) not currently running runnable.
334      */
335     private class DisplayAreaAnimationRunnable implements Runnable {
336         private boolean mStopAnimation = false;
337         private final CarLauncherDisplayAreaAnimationController
338                 .CarLauncherDisplayAreaTransitionAnimator mAnimator;
339         private final int mDurationMs;
340 
DisplayAreaAnimationRunnable( CarLauncherDisplayAreaAnimationController .CarLauncherDisplayAreaTransitionAnimator animator, int durationMs)341         DisplayAreaAnimationRunnable(
342                 CarLauncherDisplayAreaAnimationController
343                         .CarLauncherDisplayAreaTransitionAnimator animator,
344                 int durationMs) {
345             mAnimator = animator;
346             mDurationMs = durationMs;
347         }
348 
349         @Override
run()350         public void run() {
351             if (mStopAnimation) {
352                 return;
353             }
354 
355             mAnimator.addDisplayAreaAnimationCallback(mDisplayAreaAnimationCallback)
356                     .setDuration(mDurationMs)
357                     .start();
358         }
359 
stopAnimation()360         public void stopAnimation() {
361             // we don't call animator.cancel() here because if there is only one animation call
362             // such as just to open the DA then it will get canceled here.
363             mStopAnimation = true;
364         }
365     }
366 
367     @Override
onDisplayAreaAppeared(@onNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash)368     public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
369             @NonNull SurfaceControl leash) {
370         if (displayAreaInfo.featureId == BACKGROUND_TASK_CONTAINER) {
371             mBackgroundDisplayToken = displayAreaInfo.token;
372             mIsShowingBackgroundDisplay = true;
373         } else if (displayAreaInfo.featureId == CONTROL_BAR_DISPLAY_AREA) {
374             mControbarDisplayToken = displayAreaInfo.token;
375             mIsShowingControlBarDisplay = true;
376         } else if (displayAreaInfo.featureId == FOREGROUND_DISPLAY_AREA_ROOT) {
377             mForegroundDisplayToken = displayAreaInfo.token;
378         } else if (displayAreaInfo.featureId == FEATURE_TITLE_BAR) {
379             mTitleBarDisplayToken = displayAreaInfo.token;
380         } else if (displayAreaInfo.featureId == FEATURE_VOICE_PLATE) {
381             mVoicePlateDisplayToken = displayAreaInfo.token;
382         }
383         mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
384     }
385 
386     /**
387      * Launches the map in the background DA.
388      */
startMapsInBackGroundDisplayArea()389     public void startMapsInBackGroundDisplayArea() {
390         ActivityOptions options = ActivityOptions
391                 .makeCustomAnimation(mContext,
392                         /* enterResId= */ 0, /* exitResId= */ 0);
393         mContext.startActivity(mMapsIntent, options.toBundle());
394     }
395 
396     /**
397      * Launches the control bar in the control bar DA.
398      */
startControlBarInDisplayArea()399     public void startControlBarInDisplayArea() {
400         ActivityOptions options = ActivityOptions
401                 .makeCustomAnimation(mContext,
402                         /* enterResId= */ 0, /* exitResId= */ 0);
403         options.setLaunchTaskDisplayArea(mControbarDisplayToken);
404         mContext.startActivity(mAudioControlIntent, options.toBundle());
405     }
406 
407     @Override
onDisplayAreaVanished(@onNull DisplayAreaInfo displayAreaInfo)408     public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
409         if (displayAreaInfo.featureId == BACKGROUND_TASK_CONTAINER) {
410             mIsShowingBackgroundDisplay = false;
411         } else if (displayAreaInfo.featureId == CONTROL_BAR_DISPLAY_AREA) {
412             mIsShowingControlBarDisplay = false;
413         }
414         if (!mIsRegistered) {
415             mDisplayAreaTokenMap.remove(displayAreaInfo.token);
416         }
417     }
418 
419     @Override
onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo)420     public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
421         super.onDisplayAreaInfoChanged(displayAreaInfo);
422         if (mForegroundApplicationDisplay == null) return;
423         SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
424         tx.setLayer(mForegroundApplicationDisplay.getLeash(), FOREGROUND_LAYER_INDEX);
425         tx.apply(true);
426     }
427 
428     @Override
registerOrganizer(int displayAreaFeature)429     public List<DisplayAreaAppearedInfo> registerOrganizer(int displayAreaFeature) {
430         List<DisplayAreaAppearedInfo> displayAreaInfos =
431                 super.registerOrganizer(displayAreaFeature);
432         for (DisplayAreaAppearedInfo info : displayAreaInfos) {
433             onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
434         }
435         mIsRegistered = true;
436         return displayAreaInfos;
437     }
438 
439     @Override
unregisterOrganizer()440     public void unregisterOrganizer() {
441         super.unregisterOrganizer();
442         mIsRegistered = false;
443     }
444 }
445