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