Adds RearDisplay education dialog to SysUI

To accompany the rear display API's to allow
apps to move their content to the cover screen
on a foldable device, to use the powerful rear
cameras for selfies, there needs to be a consisten
educational dialog to let users know what is going
to happen. This will be shown for all apps that use
the rear display jetpack API's, to keep consistency
and remove any knowledge an app would have to know
about the device they are on.

Bug: 207686851
Test: Manual RearDisplay flow testing
Change-Id: Ib9e2b286c0d84e2411f19f7cf0b0397486e4d4b1
This commit is contained in:
Kenneth Ford
2022-11-15 18:10:10 +00:00
parent a6abfc6e1a
commit 6e1f4770aa
11 changed files with 479 additions and 0 deletions

View File

@@ -126,6 +126,9 @@
<uses-permission android:name="android.permission.ALLOW_SLIPPERY_TOUCHES" />
<uses-permission android:name="android.permission.INPUT_CONSUMER" />
<!-- DeviceStateManager -->
<uses-permission android:name="android.permission.CONTROL_DEVICE_STATE" />
<!-- DreamManager -->
<uses-permission android:name="android.permission.READ_DREAM_STATE" />
<uses-permission android:name="android.permission.WRITE_DREAM_STATE" />

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center" >
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardElevation="0dp"
app:cardCornerRadius="28dp"
app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/rear_display_folded_animation"
android:layout_width="@dimen/rear_display_animation_width"
android:layout_height="@dimen/rear_display_animation_height"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="fitXY"
app:lottie_rawRes="@raw/rear_display_folded"
app:lottie_autoPlay="true"
app:lottie_repeatMode="reverse"/>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_fold_bottom_sheet_title"
android:textAppearance="@style/TextAppearance.Dialog.Title"
android:lineSpacingExtra="2sp"
android:paddingTop="@dimen/rear_display_title_top_padding"
android:paddingBottom="@dimen/rear_display_title_bottom_padding"
android:gravity="center_horizontal|center_vertical"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_bottom_sheet_description"
android:textAppearance="@style/TextAppearance.Dialog.Body"
android:lineSpacingExtra="2sp"
android:translationY="-1.24sp"
android:gravity="center_horizontal|top"
/>
</LinearLayout>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center" >
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardElevation="0dp"
app:cardCornerRadius="28dp"
app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/rear_display_folded_animation"
android:layout_width="@dimen/rear_display_animation_width"
android:layout_height="@dimen/rear_display_animation_height"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="fitXY"
app:lottie_rawRes="@raw/rear_display_turnaround"
app:lottie_autoPlay="true"
app:lottie_repeatMode="reverse"/>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_unfold_bottom_sheet_title"
android:textAppearance="@style/TextAppearance.Dialog.Title"
android:lineSpacingExtra="2sp"
android:paddingTop="@dimen/rear_display_title_top_padding"
android:paddingBottom="@dimen/rear_display_title_bottom_padding"
android:gravity="center_horizontal|center_vertical"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_bottom_sheet_description"
android:textAppearance="@style/TextAppearance.Dialog.Body"
android:lineSpacingExtra="2sp"
android:translationY="-1.24sp"
android:gravity="center_horizontal|top"
/>
<TextView
android:id="@+id/rear_display_warning_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rear_display_bottom_sheet_warning"
android:textAppearance="@style/TextAppearance.Dialog.Body"
android:lineSpacingExtra="2sp"
android:gravity="center_horizontal|top"
/>
</LinearLayout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -247,4 +247,8 @@
<color name="dream_overlay_clock_ambient_text_shadow_color">#4D000000</color>
<color name="dream_overlay_status_bar_key_text_shadow_color">#66000000</color>
<color name="dream_overlay_status_bar_ambient_text_shadow_color">#59000000</color>
<!-- Rear Display Education -->
<color name="rear_display_overlay_animation_background_color">#1E1B17</color>
<color name="rear_display_overlay_dialog_background_color">#1E1B17</color>
</resources>

View File

@@ -1592,4 +1592,10 @@
<dimen name="config_rounded_mask_size">0px</dimen>
<dimen name="config_rounded_mask_size_top">0px</dimen>
<dimen name="config_rounded_mask_size_bottom">0px</dimen>
<!-- Rear Display Education dimens -->
<dimen name="rear_display_animation_width">273dp</dimen>
<dimen name="rear_display_animation_height">200dp</dimen>
<dimen name="rear_display_title_top_padding">24dp</dimen>
<dimen name="rear_display_title_bottom_padding">16dp</dimen>
</resources>

View File

@@ -2728,4 +2728,17 @@
shortcut button on the lock screen. [CHAR LIMIT=NONE].
-->
<string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; At least one device is available</string>
<!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_cancel">Cancel</string>
<!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_confirm">Flip now</string>
<!-- Text for education page title to guide user to unfold phone. [CHAR_LIMIT=50] -->
<string name="rear_display_fold_bottom_sheet_title">Unfold phone for a better selfie</string>
<!-- Text for education page title to guide user to flip to the front display. [CHAR_LIMIT=50] -->
<string name="rear_display_unfold_bottom_sheet_title">Flip to front display for a better selfie?</string>
<!-- Text for education page description to suggest user to use rear selfie capture. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_description">Use the rear-facing camera for a wider photo with higher resolution.</string>
<!-- Text for education page description to warn user that the display will turn off if the button is clicked. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_warning"><b>&#x2731; This screen will turn off</b></string>
</resources>

View File

@@ -36,6 +36,7 @@ import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator
import com.android.systemui.power.PowerUI
import com.android.systemui.reardisplay.RearDisplayDialogController
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
@@ -243,4 +244,11 @@ abstract class SystemUICoreStartableModule {
@IntoMap
@ClassKey(ChipbarCoordinator::class)
abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable
/** Inject into RearDisplayDialogController) */
@Binds
@IntoMap
@ClassKey(RearDisplayDialogController::class)
abstract fun bindRearDisplayDialogController(sysui: RearDisplayDialogController): CoreStartable
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.reardisplay;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerGlobal;
import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.CoreStartable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieDrawable;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* Provides an educational dialog to the user alerting them to what
* they may need to do to enter rear display mode. This may be to open the
* device if it is currently folded, or to confirm that they would like
* the content to move to the screen on their device that is aligned with
* the rear camera. This includes a device animation to provide more context
* to the user.
*
* We are suppressing lint for the VisibleForTests check because the use of
* DeviceStateManagerGlobal as in this file should not be encouraged for other use-cases.
* The lint check will notify any other use-cases that they are possibly doing something
* incorrectly.
*/
@SuppressLint("VisibleForTests") // TODO(b/260264542) Migrate away from DeviceStateManagerGlobal
@SysUISingleton
public class RearDisplayDialogController implements CoreStartable, CommandQueue.Callbacks {
private int[] mFoldedStates;
private boolean mStartedFolded;
private boolean mServiceNotified = false;
private int mAnimationRepeatCount = LottieDrawable.INFINITE;
private DeviceStateManagerGlobal mDeviceStateManagerGlobal;
private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback =
new DeviceStateManagerCallback();
private final Context mContext;
private final CommandQueue mCommandQueue;
private final Executor mExecutor;
@VisibleForTesting
SystemUIDialog mRearDisplayEducationDialog;
@Inject
public RearDisplayDialogController(Context context, CommandQueue commandQueue,
@Main Executor executor) {
mContext = context;
mCommandQueue = commandQueue;
mExecutor = executor;
}
@Override
public void start() {
mCommandQueue.addCallback(this);
}
@Override
public void showRearDisplayDialog(int currentBaseState) {
initializeValues(currentBaseState);
createAndShowDialog();
}
private void createAndShowDialog() {
mServiceNotified = false;
Context dialogContext = mRearDisplayEducationDialog.getContext();
View dialogView;
if (mStartedFolded) {
dialogView = View.inflate(dialogContext,
R.layout.activity_rear_display_education, null);
} else {
dialogView = View.inflate(dialogContext,
R.layout.activity_rear_display_education_opened, null);
}
LottieAnimationView animationView = dialogView.findViewById(
R.id.rear_display_folded_animation);
animationView.setRepeatCount(mAnimationRepeatCount);
mRearDisplayEducationDialog.setView(dialogView);
configureDialogButtons();
mRearDisplayEducationDialog.show();
}
/**
* Configures the buttons on the dialog depending on the starting device posture
*/
private void configureDialogButtons() {
// If we are open, we need to provide a confirm option
if (!mStartedFolded) {
mRearDisplayEducationDialog.setPositiveButton(
R.string.rear_display_bottom_sheet_confirm,
(dialog, which) -> closeOverlayAndNotifyService(false), true);
}
mRearDisplayEducationDialog.setNegativeButton(R.string.rear_display_bottom_sheet_cancel,
(dialog, which) -> closeOverlayAndNotifyService(true), true);
mRearDisplayEducationDialog.setOnDismissListener(dialog -> {
// Dialog is being dismissed before we've notified the system server
if (!mServiceNotified) {
closeOverlayAndNotifyService(true);
}
});
}
/**
* Initializes properties and values we need when getting ready to show the dialog.
*
* Ensures we're not using old values from when the dialog may have been shown previously.
*/
private void initializeValues(int startingBaseState) {
mRearDisplayEducationDialog = new SystemUIDialog(mContext);
if (mFoldedStates == null) {
mFoldedStates = mContext.getResources().getIntArray(
com.android.internal.R.array.config_foldedDeviceStates);
}
mStartedFolded = isFoldedState(startingBaseState);
mDeviceStateManagerGlobal = DeviceStateManagerGlobal.getInstance();
mDeviceStateManagerGlobal.registerDeviceStateCallback(mDeviceStateManagerCallback,
mExecutor);
}
private boolean isFoldedState(int state) {
for (int i = 0; i < mFoldedStates.length; i++) {
if (mFoldedStates[i] == state) return true;
}
return false;
}
/**
* Closes the educational overlay, and notifies the system service if rear display mode
* should be cancelled or enabled.
*/
private void closeOverlayAndNotifyService(boolean shouldCancelRequest) {
mServiceNotified = true;
mDeviceStateManagerGlobal.unregisterDeviceStateCallback(mDeviceStateManagerCallback);
mDeviceStateManagerGlobal.onStateRequestOverlayDismissed(shouldCancelRequest);
}
/**
* TestAPI to allow us to set the folded states array, instead of reading from resources.
*/
@TestApi
void setFoldedStates(int[] foldedStates) {
mFoldedStates = foldedStates;
}
@TestApi
void setDeviceStateManagerCallback(
DeviceStateManager.DeviceStateCallback deviceStateManagerCallback) {
mDeviceStateManagerCallback = deviceStateManagerCallback;
}
@TestApi
void setAnimationRepeatCount(int repeatCount) {
mAnimationRepeatCount = repeatCount;
}
private class DeviceStateManagerCallback implements DeviceStateManager.DeviceStateCallback {
@Override
public void onBaseStateChanged(int state) {
if (mStartedFolded && !isFoldedState(state)) {
// We've opened the device, we can close the overlay
mRearDisplayEducationDialog.dismiss();
closeOverlayAndNotifyService(false);
} else if (!mStartedFolded && isFoldedState(state)) {
// We've closed the device, finish activity
mRearDisplayEducationDialog.dismiss();
closeOverlayAndNotifyService(true);
}
}
// We only care about physical device changes in this scenario
@Override
public void onStateChanged(int state) {}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.reardisplay;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import android.hardware.devicestate.DeviceStateManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class RearDisplayDialogControllerTest extends SysuiTestCase {
@Mock
private CommandQueue mCommandQueue;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private static final int CLOSED_BASE_STATE = 0;
private static final int OPEN_BASE_STATE = 1;
@Test
public void testClosedDialogIsShown() {
RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
mCommandQueue, mFakeExecutor);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(CLOSED_BASE_STATE);
assertTrue(controller.mRearDisplayEducationDialog.isShowing());
View deviceOpenedWarningTextView = controller.mRearDisplayEducationDialog.findViewById(
R.id.rear_display_warning_text_view);
assertNull(deviceOpenedWarningTextView);
}
@Test
public void testOpenDialogIsShown() {
RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
mCommandQueue, mFakeExecutor);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(OPEN_BASE_STATE);
assertTrue(controller.mRearDisplayEducationDialog.isShowing());
View deviceOpenedWarningTextView = controller.mRearDisplayEducationDialog.findViewById(
R.id.rear_display_warning_text_view);
assertNotNull(deviceOpenedWarningTextView);
}
/**
* Empty device state manager callbacks, so we can verify that the correct
* dialogs are being created regardless of device state of the test device.
*/
private static class TestDeviceStateManagerCallback implements
DeviceStateManager.DeviceStateCallback {
@Override
public void onStateChanged(int state) { }
}
}