Show persistent notification for page-agnostic mode
When device enters page-agnostic mode using 16KB developer
options, show notification to user using boot receiver and service.
On clicked on notification, show detailed instructions on how to
get back to production mode. Removing OEM carrier unlock allowed
condition.
Bug: 295035851
Bug: 338139755
Bug: 302600682
Test: m Settings && adb install -r $ANDROID_PRODUCT_OUT/system_ext/priv-app/Settings/Settings.apk
Change-Id: Ib7a57af4c6151d2a8da1ec94130532d10b1679aa
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2325408..c3c5012 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -231,6 +231,28 @@
</intent-filter>
</receiver>
+ <receiver
+ android:name=".development.Enable16KBootReceiver"
+ android:enabled="true"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name=".development.PageAgnosticNotificationService"
+ android:enabled="true"
+ android:exported="false"
+ android:permission="android.permission.POST_NOTIFICATIONS"/>
+
+ <activity android:name=".development.PageAgnosticWarningActivity"
+ android:enabled="true"
+ android:launchMode="singleTask"
+ android:taskAffinity=""
+ android:excludeFromRecents="true"
+ android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight"/>
+
<activity android:name=".SubSettings"
android:exported="false"
android:theme="@style/Theme.SubSettings"
diff --git a/src/com/android/settings/development/Enable16KBootReceiver.java b/src/com/android/settings/development/Enable16KBootReceiver.java
new file mode 100644
index 0000000..007a67b
--- /dev/null
+++ b/src/com/android/settings/development/Enable16KBootReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * 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.development;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+public class Enable16KBootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ String action = intent.getAction();
+ if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ return;
+ }
+
+ // Do nothing if device is not in page-agnostic mode
+ if (!Enable16kUtils.isPageAgnosticModeOn(context)) {
+ return;
+ }
+
+ // start a service to post persistent notification
+ Intent startNotificationIntent = new Intent(context, PageAgnosticNotificationService.class);
+ context.startServiceAsUser(startNotificationIntent, UserHandle.SYSTEM);
+ }
+}
diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
index 7156f0b..23a6a22 100644
--- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java
+++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
@@ -23,17 +23,11 @@
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.RecoverySystem;
-import android.os.SystemProperties;
import android.os.SystemUpdateManager;
import android.os.UpdateEngine;
import android.os.UpdateEngineStable;
import android.os.UpdateEngineStableCallback;
-import android.os.UserHandle;
-import android.os.UserManager;
import android.provider.Settings;
-import android.service.oemlock.OemLockManager;
-import android.system.Os;
-import android.system.OsConstants;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
@@ -59,7 +53,6 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
-import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -80,10 +73,6 @@
private static final String TAG = "Enable16kPages";
private static final String REBOOT_REASON = "toggle16k";
private static final String ENABLE_16K_PAGES = "enable_16k_pages";
-
- @VisibleForTesting
- static final String DEV_OPTION_PROPERTY = "ro.product.build.16k_page.enabled";
-
private static final int ENABLE_4K_PAGE_SIZE = 0;
private static final int ENABLE_16K_PAGE_SIZE = 1;
@@ -97,9 +86,6 @@
private static final int OFFSET_TO_FILE_NAME = 30;
public static final String EXPERIMENTAL_UPDATE_TITLE = "Android 16K Kernel Experimental Update";
- private static final long PAGE_SIZE = Os.sysconf(OsConstants._SC_PAGESIZE);
- private static final int PAGE_SIZE_16KB = 16 * 1024;
-
private @NonNull DevelopmentSettingsDashboardFragment mFragment;
private boolean mEnable16k;
@@ -112,12 +98,12 @@
@NonNull Context context, @NonNull DevelopmentSettingsDashboardFragment fragment) {
super(context);
this.mFragment = fragment;
- mEnable16k = (PAGE_SIZE == PAGE_SIZE_16KB);
+ mEnable16k = Enable16kUtils.isUsing16kbPages();
}
@Override
public boolean isAvailable() {
- return SystemProperties.getBoolean(DEV_OPTION_PROPERTY, false);
+ return Enable16kUtils.is16KbToggleAvailable();
}
@Override
@@ -129,12 +115,12 @@
public boolean onPreferenceChange(Preference preference, Object newValue) {
mEnable16k = (Boolean) newValue;
// Prompt user to do oem unlock first
- if (!isDeviceOEMUnlocked()) {
+ if (!Enable16kUtils.isDeviceOEMUnlocked(mContext)) {
Enable16KOemUnlockDialog.show(mFragment);
return false;
}
- if (isDataf2fs()) {
+ if (!Enable16kUtils.isDataExt4()) {
EnableExt4WarningDialog.show(mFragment, this);
return false;
}
@@ -145,7 +131,7 @@
@Override
public void updateState(Preference preference) {
int defaultOptionValue =
- PAGE_SIZE == PAGE_SIZE_16KB ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
+ Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
final int optionValue =
Settings.Global.getInt(
mContext.getContentResolver(),
@@ -169,7 +155,7 @@
@Override
protected void onDeveloperOptionsSwitchEnabled() {
int currentStatus =
- PAGE_SIZE == PAGE_SIZE_16KB ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
+ Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
Settings.Global.putInt(
mContext.getContentResolver(), Settings.Global.ENABLE_16K_PAGES, currentStatus);
}
@@ -432,51 +418,6 @@
return infoBundle;
}
- private boolean isDataf2fs() {
- try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
- String line;
- while ((line = br.readLine()) != null) {
- final String[] fields = line.split(" ");
- final String partition = fields[1];
- final String fsType = fields[2];
- if (partition.equals("/data") && fsType.equals("f2fs")) {
- return true;
- }
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to read /proc/mounts");
- displayToast(mContext.getString(R.string.format_ext4_failure_toast));
- }
-
- return false;
- }
-
- private boolean isDeviceOEMUnlocked() {
- // OEM unlock is checked for bootloader, carrier and user. Check all three to ensure
- // that device is unlocked and it is also allowed by user as well as carrier
- final OemLockManager oemLockManager = mContext.getSystemService(OemLockManager.class);
- final UserManager userManager = mContext.getSystemService(UserManager.class);
- if (oemLockManager == null || userManager == null) {
- Log.e(TAG, "Required services not found on device to check for OEM unlock state.");
- return false;
- }
-
- // If either of device or carrier is not allowed to unlock, return false
- if (!oemLockManager.isDeviceOemUnlocked()
- || !oemLockManager.isOemUnlockAllowedByCarrier()) {
- Log.e(TAG, "Device is not OEM unlocked or it is not allowed by carrier");
- return false;
- }
-
- final UserHandle userHandle = UserHandle.of(UserHandle.myUserId());
- if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_FACTORY_RESET, userHandle)) {
- Log.e(TAG, "Factory reset is not allowed for user.");
- return false;
- }
-
- return true;
- }
-
// if BOARD_16K_OTA_MOVE_VENDOR, OTAs will be present on the /vendor partition
private File getOtaFile() throws FileNotFoundException {
String otaPath = mEnable16k ? OTA_16K_PATH : OTA_4K_PATH;
diff --git a/src/com/android/settings/development/Enable16kUtils.java b/src/com/android/settings/development/Enable16kUtils.java
new file mode 100644
index 0000000..7b6ab68
--- /dev/null
+++ b/src/com/android/settings/development/Enable16kUtils.java
@@ -0,0 +1,119 @@
+/*
+ * 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.development;
+
+import android.content.Context;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.oemlock.OemLockManager;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class Enable16kUtils {
+ private static final long PAGE_SIZE = Os.sysconf(OsConstants._SC_PAGESIZE);
+ private static final int PAGE_SIZE_16KB = 16 * 1024;
+
+ @VisibleForTesting
+ static final String DEV_OPTION_PROPERTY = "ro.product.build.16k_page.enabled";
+
+ private static final String TAG = "Enable16kUtils";
+
+ /**
+ * @param context uses context to retrieve OEM unlock info
+ * @return true if device is OEM unlocked and factory reset is allowed for user.
+ */
+ public static boolean isDeviceOEMUnlocked(@NonNull Context context) {
+ // OEM unlock is checked for bootloader, carrier and user. Check all three to ensure
+ // that device is unlocked and it is also allowed by user as well as carrier
+ final OemLockManager oemLockManager = context.getSystemService(OemLockManager.class);
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ if (oemLockManager == null || userManager == null) {
+ Log.e(TAG, "Required services not found on device to check for OEM unlock state.");
+ return false;
+ }
+
+ // If either of device or carrier is not allowed to unlock, return false
+ if (!oemLockManager.isDeviceOemUnlocked()) {
+ Log.e(TAG, "Device is not OEM unlocked");
+ return false;
+ }
+
+ final UserHandle userHandle = UserHandle.of(UserHandle.myUserId());
+ if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_FACTORY_RESET, userHandle)) {
+ Log.e(TAG, "Factory reset is not allowed for user.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @return true if /data partition is ext4
+ */
+ public static boolean isDataExt4() {
+ try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ Log.i(TAG, line);
+ final String[] fields = line.split(" ");
+ final String partition = fields[1];
+ final String fsType = fields[2];
+ if (partition.equals("/data") && fsType.equals("ext4")) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read /proc/mounts");
+ }
+
+ return false;
+ }
+
+ /**
+ * @return returns true if 16KB developer option is available for the device.
+ */
+ public static boolean is16KbToggleAvailable() {
+ return SystemProperties.getBoolean(DEV_OPTION_PROPERTY, false);
+ }
+
+ /**
+ * 16kB page-agnostic mode requires /data to be ext4, ro.product.build.16k_page.enabled for
+ * device and Device OEM unlocked.
+ *
+ * @param context is needed to query OEM unlock state
+ * @return true if device is in page-agnostic mode.
+ */
+ public static boolean isPageAgnosticModeOn(@NonNull Context context) {
+ return is16KbToggleAvailable() && isDeviceOEMUnlocked(context) && isDataExt4();
+ }
+
+ /**
+ * @return returns true if current page size is 16KB
+ */
+ public static boolean isUsing16kbPages() {
+ return PAGE_SIZE == PAGE_SIZE_16KB;
+ }
+}
diff --git a/src/com/android/settings/development/PageAgnosticNotificationService.java b/src/com/android/settings/development/PageAgnosticNotificationService.java
new file mode 100644
index 0000000..29bc776
--- /dev/null
+++ b/src/com/android/settings/development/PageAgnosticNotificationService.java
@@ -0,0 +1,119 @@
+/*
+ * 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.development;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+public class PageAgnosticNotificationService extends Service {
+
+ private static final String NOTIFICATION_CHANNEL_ID =
+ "com.android.settings.development.PageAgnosticNotificationService";
+ private static final int NOTIFICATION_ID = 1;
+
+ private NotificationManager mNotificationManager;
+
+ @Nullable
+ @Override
+ public IBinder onBind(@NonNull Intent intent) {
+ return null;
+ }
+
+ // create a notification channel to post persistent notification
+ private void createNotificationChannel() {
+ NotificationChannel channel =
+ new NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ getString(R.string.page_agnostic_notification_channel_name),
+ NotificationManager.IMPORTANCE_HIGH);
+ mNotificationManager = getSystemService(NotificationManager.class);
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ createNotificationChannel();
+ }
+
+ private Notification buildNotification() {
+ // Get the title and text according to page size
+ boolean isIn16kbMode = Enable16kUtils.isUsing16kbPages();
+ String title =
+ isIn16kbMode
+ ? getString(R.string.page_agnostic_16k_pages_title)
+ : getString(R.string.page_agnostic_4k_pages_title);
+ String text =
+ isIn16kbMode
+ ? getString(R.string.page_agnostic_16k_pages_text_short)
+ : getString(R.string.page_agnostic_4k_pages_text_short);
+
+ Intent notifyIntent = new Intent(this, PageAgnosticWarningActivity.class);
+ // Set the Activity to start in a new, empty task.
+ notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ // Create the PendingIntent.
+ PendingIntent notifyPendingIntent =
+ PendingIntent.getActivity(
+ this,
+ 0,
+ notifyIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ Notification.Action action =
+ new Notification.Action.Builder(
+ R.drawable.empty_icon,
+ getString(R.string.page_agnostic_notification_action),
+ notifyPendingIntent)
+ .build();
+
+ Notification.Builder builder =
+ new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.ic_settings_24dp)
+ .setStyle(new Notification.BigTextStyle().bigText(text))
+ .setContentIntent(notifyPendingIntent)
+ .addAction(action);
+
+ return builder.build();
+ }
+
+ @Override
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ Notification notification = buildNotification();
+ if (mNotificationManager != null) {
+ mNotificationManager.notify(NOTIFICATION_ID, notification);
+ }
+
+ // When clicked on notification, show dialog with full text
+ return Service.START_NOT_STICKY;
+ }
+}
diff --git a/src/com/android/settings/development/PageAgnosticWarningActivity.java b/src/com/android/settings/development/PageAgnosticWarningActivity.java
new file mode 100644
index 0000000..8fd6074
--- /dev/null
+++ b/src/com/android/settings/development/PageAgnosticWarningActivity.java
@@ -0,0 +1,72 @@
+/*
+ * 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.development;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+
+public class PageAgnosticWarningActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ String title =
+ Enable16kUtils.isUsing16kbPages()
+ ? getString(R.string.page_agnostic_16k_pages_title)
+ : getString(R.string.page_agnostic_4k_pages_title);
+
+ String warningText =
+ Enable16kUtils.isUsing16kbPages()
+ ? getString(R.string.page_agnostic_16k_pages_text)
+ : getString(R.string.page_agnostic_4k_pages_text);
+ showWarningDialog(title, warningText);
+ }
+
+ // Create warning dialog and make links clickable
+ private void showWarningDialog(String title, String warningText) {
+
+ AlertDialog dialog =
+ new AlertDialog.Builder(this)
+ .setTitle(title)
+ .setMessage(Html.fromHtml(warningText, Html.FROM_HTML_MODE_COMPACT))
+ .setCancelable(false)
+ .setPositiveButton(
+ android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(
+ @NonNull DialogInterface dialog, int which) {
+ dialog.cancel();
+ finish();
+ }
+ })
+ .create();
+ dialog.show();
+
+ ((TextView) dialog.findViewById(android.R.id.message))
+ .setMovementMethod(LinkMovementMethod.getInstance());
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
index 0c9906c..e27e3a2 100644
--- a/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
@@ -62,13 +62,13 @@
@Test
public void onSystemPropertyDisabled_shouldDisablePreference() {
- SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "false");
+ SystemProperties.set(Enable16kUtils.DEV_OPTION_PROPERTY, "false");
assertThat(mController.isAvailable()).isEqualTo(false);
}
@Test
public void onSystemPropertyEnabled_shouldEnablePreference() {
- SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "true");
+ SystemProperties.set(Enable16kUtils.DEV_OPTION_PROPERTY, "true");
assertThat(mController.isAvailable()).isEqualTo(true);
}