Merge "feat(HCT): Perform custom migration logic for existing HCT users" into main

This commit is contained in:
Treehugger Robot
2024-12-13 09:22:08 -08:00
committed by Android (Google) Code Review
5 changed files with 428 additions and 0 deletions

View File

@@ -5439,6 +5439,16 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".accessibility.HighContrastTextMigrationReceiver"
android:permission="android.permission.MANAGE_ACCESSIBILITY"
android:exported="true"> <!-- Exported for SettingsProvider restore from backup. -->
<intent-filter>
<action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
<action android:name="com.android.settings.accessibility.ACTION_HIGH_CONTRAST_TEXT_RESTORED"/>
</intent-filter>
</receiver>
<activity <activity
android:name="Settings$ChangeNfcTagAppsActivity" android:name="Settings$ChangeNfcTagAppsActivity"
android:exported="true" android:exported="true"

View File

@@ -5397,6 +5397,10 @@
<string name="accessibility_toggle_high_text_contrast_preference_title">High contrast text</string> <string name="accessibility_toggle_high_text_contrast_preference_title">High contrast text</string>
<!-- Summary for the accessibility preference to high contrast text. [CHAR LIMIT=NONE] --> <!-- Summary for the accessibility preference to high contrast text. [CHAR LIMIT=NONE] -->
<string name="accessibility_toggle_high_text_contrast_preference_summary">Change text color to black or white. Maximizes contrast with the background.</string> <string name="accessibility_toggle_high_text_contrast_preference_summary">Change text color to black or white. Maximizes contrast with the background.</string>
<!-- Content for the notification to high contrast text. [CHAR LIMIT=NONE] -->
<string name="accessibility_notification_high_contrast_text_content">High contrast text has a new look and feel.</string>
<!-- Action for the notification to high contrast text. [CHAR LIMIT=35] -->
<string name="accessibility_notification_high_contrast_text_action">Open Settings</string>
<!-- Title for the accessibility preference to high contrast text. [CHAR LIMIT=35] --> <!-- Title for the accessibility preference to high contrast text. [CHAR LIMIT=35] -->
<string name="accessibility_toggle_maximize_text_contrast_preference_title">Maximize text contrast</string> <string name="accessibility_toggle_maximize_text_contrast_preference_title">Maximize text contrast</string>
<!-- Summary for the accessibility preference to high contrast text. [CHAR LIMIT=NONE] --> <!-- Summary for the accessibility preference to high contrast text. [CHAR LIMIT=NONE] -->

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2024 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.settings.accessibility;
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
import android.annotation.IntDef;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.graphics.hwui.flags.Flags;
import com.android.settings.R;
import com.google.common.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Handling smooth migration to the new high contrast text appearance
*/
public class HighContrastTextMigrationReceiver extends BroadcastReceiver {
private static final String TAG = HighContrastTextMigrationReceiver.class.getSimpleName();
@VisibleForTesting
static final String NOTIFICATION_CHANNEL = "high_contrast_text_notification_channel";
@VisibleForTesting
static final String ACTION_RESTORED =
"com.android.settings.accessibility.ACTION_HIGH_CONTRAST_TEXT_RESTORED";
@VisibleForTesting
static final int NOTIFICATION_ID = 1;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
PromptState.UNKNOWN,
PromptState.PROMPT_SHOWN,
PromptState.PROMPT_UNNECESSARY,
})
public @interface PromptState {
int UNKNOWN = 0;
int PROMPT_SHOWN = 1;
int PROMPT_UNNECESSARY = 2;
}
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
if (!Flags.highContrastTextSmallTextRect()) {
return;
}
if (ACTION_RESTORED.equals(intent.getAction())) {
Log.i(TAG, "HCT attempted to be restored from backup; showing notification for userId: "
+ context.getUserId());
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
PromptState.PROMPT_SHOWN);
showNotification(context);
} else if (Intent.ACTION_PRE_BOOT_COMPLETED.equals(intent.getAction())) {
final boolean hasSeenPromptIfNecessary = Settings.Secure.getInt(
context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, PromptState.UNKNOWN)
!= PromptState.UNKNOWN;
if (hasSeenPromptIfNecessary) {
Log.i(TAG, "Has seen HCT prompt if necessary; skip HCT migration for userId: "
+ context.getUserId());
return;
}
final boolean isHctEnabled = Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, OFF) == ON;
if (isHctEnabled) {
Log.i(TAG, "HCT enabled before OTA update; performing migration for userId: "
+ context.getUserId());
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
OFF);
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
PromptState.PROMPT_SHOWN);
showNotification(context);
} else {
Log.i(TAG,
"HCT was not enabled before OTA update; not performing migration for "
+ "userId: " + context.getUserId());
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
PromptState.PROMPT_UNNECESSARY);
}
}
}
private void showNotification(Context context) {
Notification.Builder notificationBuilder = new Notification.Builder(context,
NOTIFICATION_CHANNEL)
.setSmallIcon(R.drawable.ic_settings_24dp)
.setContentTitle(context.getString(
R.string.accessibility_toggle_high_text_contrast_preference_title))
.setContentText(context.getString(
R.string.accessibility_notification_high_contrast_text_content))
.setAutoCancel(true);
Intent settingsIntent = new Intent(Settings.ACTION_TEXT_READING_SETTINGS);
settingsIntent.setPackage(context.getPackageName());
if (settingsIntent.resolveActivity(context.getPackageManager()) != null) {
Bundle fragmentArgs = new Bundle();
fragmentArgs.putString(EXTRA_FRAGMENT_ARG_KEY,
TextReadingPreferenceFragment.HIGH_TEXT_CONTRAST_KEY);
settingsIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs);
PendingIntent settingsPendingIntent = PendingIntent.getActivity(context,
/* requestCode = */ 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE);
Notification.Action settingsAction = new Notification.Action.Builder(
/* icon= */ null,
context.getString(
R.string.accessibility_notification_high_contrast_text_action),
settingsPendingIntent
).build();
notificationBuilder.addAction(settingsAction);
}
NotificationManager notificationManager =
context.getSystemService(NotificationManager.class);
NotificationChannel notificationChannel = new NotificationChannel(
NOTIFICATION_CHANNEL,
context.getString(
R.string.accessibility_toggle_high_text_contrast_preference_title),
NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(notificationChannel);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}

View File

@@ -22,6 +22,7 @@ import android.provider.Settings;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference; import androidx.preference.TwoStatePreference;
import com.android.graphics.hwui.flags.Flags;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoint; import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoint;
import com.android.settings.core.TogglePreferenceController; import com.android.settings.core.TogglePreferenceController;
@@ -60,6 +61,20 @@ public class HighTextContrastPreferenceController extends TogglePreferenceContro
isChecked ? 1 : 0, isChecked ? 1 : 0,
AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint)); AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint));
if (Flags.highContrastTextSmallTextRect()) {
// Set PROMPT_UNNECESSARY when the user modifies the HighContrastText setting
// This is needed for the following scenario:
// On Android 16, create secondary user, ACTION_PRE_BOOT_COMPLETED won't be sent to
// the secondary user. The user enables HCT.
// When updating OS to Android 17, ACTION_PRE_BOOT_COMPLETED will be sent to the
// secondary user when switch to the secondary user.
// If the prompt status is not updated in Android 16, we would automatically disable
// HCT and show the HCT prompt, which is an undesired behavior.
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
HighContrastTextMigrationReceiver.PromptState.PROMPT_UNNECESSARY);
}
return Settings.Secure.putInt(mContext.getContentResolver(), return Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, (isChecked ? 1 : 0)); Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, (isChecked ? 1 : 0));
} }

View File

@@ -0,0 +1,241 @@
/*
* Copyright (C) 2024 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.settings.accessibility;
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.ACTION_RESTORED;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.NOTIFICATION_CHANNEL;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.NOTIFICATION_ID;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.PROMPT_SHOWN;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.PROMPT_UNNECESSARY;
import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.UNKNOWN;
import static com.google.common.truth.Truth.assertThat;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import androidx.test.core.app.ApplicationProvider;
import com.android.graphics.hwui.flags.Flags;
import com.android.settings.R;
import com.android.settings.Utils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowNotification;
import org.robolectric.shadows.ShadowNotificationManager;
import org.robolectric.shadows.ShadowPackageManager;
/** Tests for {@link HighContrastTextMigrationReceiver}. */
@RunWith(RobolectricTestRunner.class)
public class HighContrastTextMigrationReceiverTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private HighContrastTextMigrationReceiver mReceiver;
private ShadowNotificationManager mShadowNotificationManager;
@Before
public void setUp() {
NotificationManager notificationManager =
mContext.getSystemService(NotificationManager.class);
mShadowNotificationManager = Shadows.shadowOf(notificationManager);
// Setup Settings app as a system app
ShadowPackageManager shadowPm = Shadows.shadowOf(mContext.getPackageManager());
ComponentName textReadingComponent = new ComponentName(Utils.SETTINGS_PACKAGE_NAME,
com.android.settings.Settings.TextReadingSettingsActivity.class.getName());
ActivityInfo activityInfo = shadowPm.addActivityIfNotPresent(textReadingComponent);
activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
shadowPm.addOrUpdateActivity(activityInfo);
mReceiver = new HighContrastTextMigrationReceiver();
}
@Test
@DisableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onReceive_flagOff_settingsNotSet() {
mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED));
assertPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF);
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onRestored_hctStateOn_showPromptHctKeepsOn() {
setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ ON);
mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED));
assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, ON);
verifyNotificationSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onRestored_hctStateOff_showPromptHctKeepsOff() {
setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF);
mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED));
assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, OFF);
verifyNotificationSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateUnknownHctOn_showPromptAndAutoDisableHct() {
setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ ON);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF);
verifyNotificationSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateUnknownAndHctOff_promptIsUnnecessaryHctKeepsOff() {
setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF);
verifyNotificationNotSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateShownAndHctOn_promptStateUnchangedHctKeepsOn() {
setPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ ON);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ ON);
verifyNotificationNotSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateShownAndHctOff_promptStateUnchangedHctKeepsOff() {
setPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF);
verifyNotificationNotSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateUnnecessaryAndHctOn_promptStateUnchangedHctKeepsOn() {
setPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ ON);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ ON);
verifyNotificationNotSent();
}
@Test
@EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
public void onPreBootCompleted_promptStateUnnecessaryHctOff_promptStateUnchangedHctKeepsOff() {
setPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF);
Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
mReceiver.onReceive(mContext, intent);
assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF);
verifyNotificationNotSent();
}
private void verifyNotificationNotSent() {
Notification notification = mShadowNotificationManager.getNotification(NOTIFICATION_ID);
assertThat(notification).isNull();
}
private void verifyNotificationSent() {
// Verify hct channel created
assertThat(mShadowNotificationManager.getNotificationChannels().stream().anyMatch(
channel -> channel.getId().equals(NOTIFICATION_CHANNEL))).isTrue();
// Verify hct notification is sent with correct content
Notification notification = mShadowNotificationManager.getNotification(NOTIFICATION_ID);
assertThat(notification).isNotNull();
ShadowNotification shadowNotification = Shadows.shadowOf(notification);
assertThat(shadowNotification.getContentTitle()).isEqualTo(mContext.getString(
R.string.accessibility_toggle_high_text_contrast_preference_title));
assertThat(shadowNotification.getContentText()).isEqualTo(
mContext.getString(R.string.accessibility_notification_high_contrast_text_content));
assertThat(notification.actions.length).isEqualTo(1);
assertThat(notification.actions[0].title.toString()).isEqualTo(
mContext.getString(R.string.accessibility_notification_high_contrast_text_action));
PendingIntent pendingIntent = notification.actions[0].actionIntent;
Intent settingsIntent = Shadows.shadowOf(pendingIntent).getSavedIntent();
Bundle fragmentArgs = settingsIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
assertThat(fragmentArgs).isNotNull();
assertThat(fragmentArgs.getString(EXTRA_FRAGMENT_ARG_KEY))
.isEqualTo(TextReadingPreferenceFragment.HIGH_TEXT_CONTRAST_KEY);
}
private void assertPromptStateAndHctState(
@HighContrastTextMigrationReceiver.PromptState int promptState,
@AccessibilityUtil.State int hctState) {
assertThat(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, UNKNOWN))
.isEqualTo(promptState);
assertThat(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, OFF))
.isEqualTo(hctState);
}
private void setPromptStateAndHctState(
@HighContrastTextMigrationReceiver.PromptState int promptState,
@AccessibilityUtil.State int hctState) {
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, promptState);
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, hctState);
}
}