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