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