• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.systemui.reardisplay;
18 
19 import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.SuppressLint;
24 import android.annotation.TestApi;
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.hardware.devicestate.DeviceState;
29 import android.hardware.devicestate.DeviceStateManager;
30 import android.hardware.devicestate.DeviceStateManagerGlobal;
31 import android.hardware.devicestate.feature.flags.Flags;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup.LayoutParams;
35 import android.widget.LinearLayout;
36 
37 import com.android.systemui.CoreStartable;
38 import com.android.systemui.dagger.SysUISingleton;
39 import com.android.systemui.dagger.qualifiers.Main;
40 import com.android.systemui.res.R;
41 import com.android.systemui.statusbar.CommandQueue;
42 import com.android.systemui.statusbar.phone.SystemUIDialog;
43 import com.android.systemui.statusbar.policy.ConfigurationController;
44 
45 import com.airbnb.lottie.LottieAnimationView;
46 import com.airbnb.lottie.LottieDrawable;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.concurrent.Executor;
51 
52 import javax.inject.Inject;
53 
54 /**
55  * Provides an educational dialog to the user alerting them to what
56  * they may need to do to enter rear display mode. This may be to open the
57  * device if it is currently folded, or to confirm that they would like
58  * the content to move to the screen on their device that is aligned with
59  * the rear camera. This includes a device animation to provide more context
60  * to the user.
61  *
62  * We are suppressing lint for the VisibleForTests check because the use of
63  * DeviceStateManagerGlobal as in this file should not be encouraged for other use-cases.
64  * The lint check will notify any other use-cases that they are possibly doing something
65  * incorrectly.
66  */
67 @SuppressLint("VisibleForTests") // TODO(b/260264542) Migrate away from DeviceStateManagerGlobal
68 @SysUISingleton
69 public class RearDisplayDialogController implements
70         CoreStartable,
71         ConfigurationController.ConfigurationListener,
72         CommandQueue.Callbacks {
73 
74     private List<Integer> mFoldedStates;
75     private boolean mStartedFolded;
76     private boolean mServiceNotified = false;
77     private int mAnimationRepeatCount = LottieDrawable.INFINITE;
78 
79     private final DeviceStateManager mDeviceStateManager;
80     private DeviceStateManagerGlobal mDeviceStateManagerGlobal;
81     private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback =
82             new DeviceStateManagerCallback();
83 
84     private final CommandQueue mCommandQueue;
85     private final Executor mExecutor;
86     private final Resources mResources;
87     private final LayoutInflater mLayoutInflater;
88     private final SystemUIDialog.Factory mSystemUIDialogFactory;
89 
90     private SystemUIDialog mRearDisplayEducationDialog;
91     @Nullable LinearLayout mDialogViewContainer;
92 
93     @Inject
RearDisplayDialogController( CommandQueue commandQueue, @Main Executor executor, @Main Resources resources, LayoutInflater layoutInflater, SystemUIDialog.Factory systemUIDialogFactory, DeviceStateManager deviceStateManager)94     public RearDisplayDialogController(
95             CommandQueue commandQueue,
96             @Main Executor executor,
97             @Main Resources resources,
98             LayoutInflater layoutInflater,
99             SystemUIDialog.Factory systemUIDialogFactory,
100             DeviceStateManager deviceStateManager) {
101         mCommandQueue = commandQueue;
102         mExecutor = executor;
103         mResources = resources;
104         mLayoutInflater = layoutInflater;
105         mSystemUIDialogFactory = systemUIDialogFactory;
106         mDeviceStateManager = deviceStateManager;
107     }
108 
109     @Override
start()110     public void start() {
111         mCommandQueue.addCallback(this);
112     }
113 
114     @Override
showRearDisplayDialog(int currentBaseState)115     public void showRearDisplayDialog(int currentBaseState) {
116         initializeValues(currentBaseState);
117         createAndShowDialog();
118     }
119 
120     @Override
onConfigChanged(Configuration newConfig)121     public void onConfigChanged(Configuration newConfig) {
122         if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
123                 && mDialogViewContainer != null) {
124             // Refresh the dialog view when configuration is changed.
125             View dialogView = createDialogView(mRearDisplayEducationDialog.getContext());
126             mDialogViewContainer.removeAllViews();
127             mDialogViewContainer.addView(dialogView);
128         }
129     }
130 
createAndShowDialog()131     private void createAndShowDialog() {
132         mServiceNotified = false;
133         Context dialogContext = mRearDisplayEducationDialog.getContext();
134         View dialogView = createDialogView(dialogContext);
135         mDialogViewContainer = new LinearLayout(dialogContext);
136         mDialogViewContainer.setLayoutParams(
137                 new LinearLayout.LayoutParams(
138                         LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
139         mDialogViewContainer.setOrientation(LinearLayout.VERTICAL);
140         mDialogViewContainer.addView(dialogView);
141 
142         mRearDisplayEducationDialog.setView(mDialogViewContainer);
143 
144         configureDialogButtons();
145 
146         mRearDisplayEducationDialog.show();
147     }
148 
createDialogView(Context context)149     private View createDialogView(Context context) {
150         View dialogView;
151         LayoutInflater inflater = mLayoutInflater.cloneInContext(context);
152         if (mStartedFolded) {
153             dialogView = inflater.inflate(R.layout.activity_rear_display_education, null);
154         } else {
155             dialogView = inflater.inflate(
156                     R.layout.activity_rear_display_education_opened, null);
157         }
158         LottieAnimationView animationView = dialogView.findViewById(
159                 R.id.rear_display_folded_animation);
160         animationView.setRepeatCount(mAnimationRepeatCount);
161         return dialogView;
162     }
163 
164     /**
165      * Configures the buttons on the dialog depending on the starting device posture
166      */
configureDialogButtons()167     private void configureDialogButtons() {
168         // If we are open, we need to provide a confirm option
169         if (!mStartedFolded) {
170             mRearDisplayEducationDialog.setPositiveButton(
171                     R.string.rear_display_bottom_sheet_confirm,
172                     (dialog, which) -> closeOverlayAndNotifyService(false), true);
173         }
174         mRearDisplayEducationDialog.setNegativeButton(R.string.rear_display_bottom_sheet_cancel,
175                 (dialog, which) -> closeOverlayAndNotifyService(true), true);
176         mRearDisplayEducationDialog.setOnDismissListener(dialog -> {
177             // Dialog is being dismissed before we've notified the system server
178             if (!mServiceNotified) {
179                 closeOverlayAndNotifyService(true);
180             }
181         });
182     }
183 
184     /**
185      * Initializes properties and values we need when getting ready to show the dialog.
186      *
187      * Ensures we're not using old values from when the dialog may have been shown previously.
188      */
initializeValues(int startingBaseState)189     private void initializeValues(int startingBaseState) {
190         mRearDisplayEducationDialog = mSystemUIDialogFactory.create();
191         if (Flags.deviceStatePropertyMigration()) {
192             if (mFoldedStates == null) {
193                 mFoldedStates = new ArrayList<>();
194                 List<DeviceState> deviceStates = mDeviceStateManager.getSupportedDeviceStates();
195                 for (int i = 0; i < deviceStates.size(); i++) {
196                     DeviceState state = deviceStates.get(i);
197                     if (state.hasProperty(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY)) {
198                         mFoldedStates.add(state.getIdentifier());
199                     }
200                 }
201             }
202         } else {
203             // TODO(b/329170810): Refactor and remove with updated DeviceStateManager values.
204             if (mFoldedStates == null) {
205                 mFoldedStates = copyIntArrayToList(mResources.getIntArray(
206                         com.android.internal.R.array.config_foldedDeviceStates));
207             }
208         }
209         mStartedFolded = isFoldedState(startingBaseState);
210         mDeviceStateManagerGlobal = DeviceStateManagerGlobal.getInstance();
211         mDeviceStateManagerGlobal.registerDeviceStateCallback(mDeviceStateManagerCallback,
212                 mExecutor);
213     }
214 
copyIntArrayToList(int[] intArray)215     private List<Integer> copyIntArrayToList(int[] intArray) {
216         List<Integer> integerList = new ArrayList<>(intArray.length);
217         for (int i = 0; i < intArray.length; i++) {
218             integerList.add(intArray[i]);
219         }
220         return integerList;
221     }
222 
isFoldedState(int state)223     private boolean isFoldedState(int state) {
224         for (int i = 0; i < mFoldedStates.size(); i++) {
225             if (mFoldedStates.get(i) == state) return true;
226         }
227         return false;
228     }
229 
230     /**
231      * Closes the educational overlay, and notifies the system service if rear display mode
232      * should be cancelled or enabled.
233      */
closeOverlayAndNotifyService(boolean shouldCancelRequest)234     private void closeOverlayAndNotifyService(boolean shouldCancelRequest) {
235         mServiceNotified = true;
236         mDeviceStateManagerGlobal.unregisterDeviceStateCallback(mDeviceStateManagerCallback);
237         mDeviceStateManagerGlobal.onStateRequestOverlayDismissed(shouldCancelRequest);
238         mDialogViewContainer = null;
239     }
240 
241     /**
242      * TestAPI to allow us to set the folded states array, instead of reading from resources.
243      */
244     @TestApi
setFoldedStates(List<Integer> foldedStates)245     void setFoldedStates(List<Integer> foldedStates) {
246         mFoldedStates = foldedStates;
247     }
248 
249     @TestApi
setDeviceStateManagerCallback( DeviceStateManager.DeviceStateCallback deviceStateManagerCallback)250     void setDeviceStateManagerCallback(
251             DeviceStateManager.DeviceStateCallback deviceStateManagerCallback) {
252         mDeviceStateManagerCallback = deviceStateManagerCallback;
253     }
254 
255     @TestApi
setAnimationRepeatCount(int repeatCount)256     void setAnimationRepeatCount(int repeatCount) {
257         mAnimationRepeatCount = repeatCount;
258     }
259 
260     private class DeviceStateManagerCallback implements DeviceStateManager.DeviceStateCallback {
261         @Override
onDeviceStateChanged(@onNull DeviceState state)262         public void onDeviceStateChanged(@NonNull DeviceState state) {
263             if (mStartedFolded && !state.hasProperty(
264                     DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED)) {
265                 // We've opened the device, we can close the overlay
266                 mRearDisplayEducationDialog.dismiss();
267                 closeOverlayAndNotifyService(false);
268             } else if (!mStartedFolded && state.hasProperty(
269                     DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED)) {
270                 // We've closed the device, finish activity
271                 mRearDisplayEducationDialog.dismiss();
272                 closeOverlayAndNotifyService(true);
273             }
274         }
275     }
276 }
277 
278