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