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