Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a697fba5 authored by Pawan Wagh's avatar Pawan Wagh
Browse files

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
parent 14d4df60
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -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"
+44 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+6 −65
Original line number Diff line number Diff line
@@ -23,17 +23,11 @@ import android.os.ParcelFileDescriptor;
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 com.google.common.util.concurrent.MoreExecutors;
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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
    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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
    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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
            @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 class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
    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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
    @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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
    @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 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
        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;
+119 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+119 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading