diff --git a/res/values/strings.xml b/res/values/strings.xml index 09e687e826b..4c0de24eb3f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11330,9 +11330,15 @@ Boot device using 16K page size supported kernel - Reboot with 16K page compatible kernel? + Reboot with 16KB pages compatible kernel? - WARNING: Some applications may not be compatible with this mode + WARNING: Some applications may not be compatible with this mode. Device will reboot after confirmation. + + Reboot with 4KB pages compatible kernel? + + Device will reboot after confirmation. + + Failed to update kernel to 16KB pages compatible kernel. DSU Loader diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java index 9aadd73c211..9752ecf0b02 100644 --- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java +++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java @@ -17,29 +17,74 @@ package com.android.settings.development; import android.content.Context; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; import android.os.SystemProperties; +import android.os.UpdateEngine; +import android.os.UpdateEngineStable; +import android.os.UpdateEngineStableCallback; import android.provider.Settings; +import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.content.ContextCompat; import androidx.preference.Preference; import androidx.preference.SwitchPreference; +import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + /** Controller for 16K pages developer option */ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin, Enable16kbPagesDialogHost { + private static final String TAG = "Enable16kPages"; + private static final String REBOOT_REASON = "toggle16k"; private static final String ENABLE_16K_PAGES = "enable_16k_pages"; - private static final String DEV_OPTION_PROPERTY = "ro.product.build.16k_page.enabled"; + + @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; + private static final String OTA_16K_PATH = "/system/boot_otas/boot_ota_16k.zip"; + private static final String OTA_4K_PATH = "/system/boot_otas/boot_ota_4k.zip"; + private static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin"; + private static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt"; + private static final int OFFSET_TO_FILE_NAME = 30; + private @Nullable DevelopmentSettingsDashboardFragment mFragment = null; + private boolean mEnable16k; + + private final ListeningExecutorService mExecutorService = + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); public Enable16kPagesPreferenceController( @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) { @@ -59,12 +104,8 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean optionEnabled = (Boolean) newValue; - if (optionEnabled) { - Enable16kPagesWarningDialog.show(mFragment, this); - } else { - // TODO(b/295573133):Directly reboot into 4k - } + mEnable16k = (Boolean) newValue; + Enable16kPagesWarningDialog.show(mFragment, this, mEnable16k); return true; } @@ -93,10 +134,157 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen /** Called when user confirms reboot dialog */ @Override public void on16kPagesDialogConfirmed() { - // TODO(b/295573133) : integrate update engine + // Apply update in background + ListenableFuture future = mExecutorService.submit(() -> installUpdate()); + Futures.addCallback( + future, + new FutureCallback<>() { + + @Override + public void onSuccess(@NonNull Object result) { + // This means UpdateEngineStable is working on applying update in + // background. + // Result of that operation will be provided by separate callback. + Log.i(TAG, "applyPayload call to UpdateEngineStable succeeded."); + } + + @Override + public void onFailure(Throwable t) { + Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!"); + displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); + } + }, + ContextCompat.getMainExecutor(mContext)); } /** Called when user dismisses to reboot dialog */ @Override public void on16kPagesDialogDismissed() {} + + private void installUpdate() { + String updateFilePath = mEnable16k ? OTA_16K_PATH : OTA_4K_PATH; + try { + File updateFile = new File(updateFilePath); + applyUpdateFile(updateFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @VisibleForTesting + void applyUpdateFile(@NonNull File updateFile) throws IOException, FileNotFoundException { + boolean payloadFound = false; + boolean propertiesFound = false; + long payloadOffset = 0; + long payloadSize = 0; + + List properties = new ArrayList<>(); + try (ZipFile zip = new ZipFile(updateFile)) { + Enumeration entries = zip.entries(); + long offset = 0; + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + String fileName = zipEntry.getName(); + long extraSize = zipEntry.getExtra() == null ? 0 : zipEntry.getExtra().length; + offset += OFFSET_TO_FILE_NAME + fileName.length() + extraSize; + + if (zipEntry.isDirectory()) { + continue; + } + + long length = zipEntry.getCompressedSize(); + if (PAYLOAD_BINARY_FILE_NAME.equals(fileName)) { + if (zipEntry.getMethod() != ZipEntry.STORED) { + throw new IOException("Unknown compression method."); + } + payloadFound = true; + payloadOffset = offset; + payloadSize = length; + } else if (PAYLOAD_PROPERTIES_FILE_NAME.equals(fileName)) { + propertiesFound = true; + InputStream inputStream = zip.getInputStream(zipEntry); + if (inputStream != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = br.readLine()) != null) { + properties.add(line); + } + } + } + offset += length; + } + } + + if (!payloadFound) { + throw new FileNotFoundException( + "Failed to find payload in zip: " + updateFile.getAbsolutePath()); + } + + if (!propertiesFound) { + throw new FileNotFoundException( + "Failed to find payload properties in zip: " + updateFile.getAbsolutePath()); + } + + if (payloadSize == 0) { + throw new IOException("Found empty payload in zip: " + updateFile.getAbsolutePath()); + } + + applyPayload(updateFile, payloadOffset, payloadSize, properties); + } + + @VisibleForTesting + void applyPayload( + @NonNull File updateFile, + long payloadOffset, + long payloadSize, + @NonNull List properties) + throws FileNotFoundException { + String[] header = properties.stream().toArray(String[]::new); + UpdateEngineStable updateEngineStable = new UpdateEngineStable(); + try { + ParcelFileDescriptor pfd = + ParcelFileDescriptor.open(updateFile, ParcelFileDescriptor.MODE_READ_ONLY); + updateEngineStable.bind( + new OtaUpdateCallback(updateEngineStable), + new Handler(mContext.getMainLooper())); + updateEngineStable.applyPayloadFd(pfd, payloadOffset, payloadSize, header); + } finally { + Log.e(TAG, "Failure while applying an update using update engine"); + } + } + + private void displayToast(String message) { + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + + private class OtaUpdateCallback extends UpdateEngineStableCallback { + UpdateEngineStable mUpdateEngineStable; + + OtaUpdateCallback(@NonNull UpdateEngineStable engine) { + mUpdateEngineStable = engine; + } + + @Override + public void onStatusUpdate(int status, float percent) {} + + @Override + public void onPayloadApplicationComplete(int errorCode) { + // unbind the callback from update engine + mUpdateEngineStable.unbind(); + + if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS) { + Log.i(TAG, "applyPayload successful"); + Settings.Global.putInt( + mContext.getContentResolver(), + Settings.Global.ENABLE_16K_PAGES, + mEnable16k ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE); + + PowerManager pm = mContext.getSystemService(PowerManager.class); + pm.reboot(REBOOT_REASON); + } else { + Log.e(TAG, "applyPayload failed, error code: " + errorCode); + displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); + } + } + } } diff --git a/src/com/android/settings/development/Enable16kPagesWarningDialog.java b/src/com/android/settings/development/Enable16kPagesWarningDialog.java index f141c93d143..5c27b3f53c0 100644 --- a/src/com/android/settings/development/Enable16kPagesWarningDialog.java +++ b/src/com/android/settings/development/Enable16kPagesWarningDialog.java @@ -35,6 +35,7 @@ public class Enable16kPagesWarningDialog extends InstrumentedDialogFragment implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { public static final String TAG = "Enable16KDialog"; + private static final String DIALOG_BUNDLE_KEY = "SHOW_16K_DIALOG"; private Enable16kbPagesDialogHost mHost; @@ -42,17 +43,22 @@ public class Enable16kPagesWarningDialog extends InstrumentedDialogFragment mHost = host; } - /** Used to display warning dialog */ + /** This method is used to show warning dialog to apply 16K update and reboot */ public static void show( - @NonNull Fragment hostFragment, @NonNull Enable16kbPagesDialogHost dialogHost) { + @NonNull Fragment hostFragment, + @NonNull Enable16kbPagesDialogHost dialogHost, + boolean enable16k) { final FragmentManager manager = hostFragment.getActivity().getSupportFragmentManager(); Fragment existingFragment = manager.findFragmentByTag(TAG); if (existingFragment == null) { existingFragment = new Enable16kPagesWarningDialog(); - existingFragment.setTargetFragment(hostFragment, 0 /* requestCode */); } if (existingFragment instanceof Enable16kPagesWarningDialog) { + Bundle bundle = new Bundle(); + bundle.putBoolean(DIALOG_BUNDLE_KEY, enable16k); + existingFragment.setArguments(bundle); + existingFragment.setTargetFragment(hostFragment, 0 /* requestCode */); ((Enable16kPagesWarningDialog) existingFragment).setHost(dialogHost); ((Enable16kPagesWarningDialog) existingFragment).show(manager, TAG); } @@ -66,9 +72,17 @@ public class Enable16kPagesWarningDialog extends InstrumentedDialogFragment @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final Bundle bundle = getArguments(); + boolean is16kDialog = bundle.getBoolean(DIALOG_BUNDLE_KEY); return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.confirm_enable_16k_pages_title) - .setMessage(R.string.confirm_enable_16k_pages_text) + .setTitle( + is16kDialog + ? R.string.confirm_enable_16k_pages_title + : R.string.confirm_enable_4k_pages_title) + .setMessage( + is16kDialog + ? R.string.confirm_enable_16k_pages_text + : R.string.confirm_enable_4k_pages_text) .setPositiveButton(android.R.string.ok, this /* onClickListener */) .setNegativeButton(android.R.string.cancel, this /* onClickListener */) .create(); diff --git a/tests/robotests/assets/empty_payload_ota.zip b/tests/robotests/assets/empty_payload_ota.zip new file mode 100644 index 00000000000..c28b5dfd543 Binary files /dev/null and b/tests/robotests/assets/empty_payload_ota.zip differ diff --git a/tests/robotests/assets/no_payload_ota.zip b/tests/robotests/assets/no_payload_ota.zip new file mode 100644 index 00000000000..00add65c77e Binary files /dev/null and b/tests/robotests/assets/no_payload_ota.zip differ diff --git a/tests/robotests/assets/no_properties_ota.zip b/tests/robotests/assets/no_properties_ota.zip new file mode 100644 index 00000000000..4ca60e02e3b Binary files /dev/null and b/tests/robotests/assets/no_properties_ota.zip differ diff --git a/tests/robotests/assets/valid_ota.zip b/tests/robotests/assets/valid_ota.zip new file mode 100644 index 00000000000..20fa2dadf5e Binary files /dev/null and b/tests/robotests/assets/valid_ota.zip differ diff --git a/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java new file mode 100644 index 00000000000..0c9906cdf72 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java @@ -0,0 +1,124 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.os.FileUtils; +import android.os.SystemProperties; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +@RunWith(RobolectricTestRunner.class) +public class Enable16kPagesPreferenceControllerTest { + + @Mock private DevelopmentSettingsDashboardFragment mFragment; + private Context mContext; + private Enable16kPagesPreferenceController mController; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mController = spy(new Enable16kPagesPreferenceController(mContext, mFragment)); + doNothing().when(mController).applyPayload(any(), anyLong(), anyLong(), anyList()); + } + + @Test + public void onSystemPropertyDisabled_shouldDisablePreference() { + SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "false"); + assertThat(mController.isAvailable()).isEqualTo(false); + } + + @Test + public void onSystemPropertyEnabled_shouldEnablePreference() { + SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "true"); + assertThat(mController.isAvailable()).isEqualTo(true); + } + + // TODO(b/303280163) : add tests to based on page size and whether preference is turned on/off + + @Test + public void validateUpdateParsing_validFile() throws IOException { + // TODO(b/295573133) : Add tests to verify applyPayload arguments + String filename = "valid_ota.zip"; + File updateFile = copyFromAssetToDataDir(filename); + mController.applyUpdateFile(updateFile); + } + + @Test + public void validateUpdateParsing_emptyPayloadFile() { + String filename = "empty_payload_ota.zip"; + File updateFile = copyFromAssetToDataDir(filename); + assertThrows(IOException.class, () -> mController.applyUpdateFile(updateFile)); + } + + @Test + public void validateUpdateParsing_noPayloadFile() { + String filename = "no_payload_ota.zip"; + File updateFile = copyFromAssetToDataDir(filename); + assertThrows(FileNotFoundException.class, () -> mController.applyUpdateFile(updateFile)); + } + + @Test + public void validateUpdateParsing_noPropertiesFile() { + String filename = "no_properties_ota.zip"; + File updateFile = copyFromAssetToDataDir(filename); + assertThrows(FileNotFoundException.class, () -> mController.applyUpdateFile(updateFile)); + } + + private File copyFromAssetToDataDir(String filename) { + try { + InputStream in = mContext.getAssets().open(filename); + File destination = + File.createTempFile( + "test_update", ".zip", new File(mContext.getDataDir().getPath())); + FileUtils.setPermissions( + /* path= */ destination, + /* mode= */ FileUtils.S_IRWXU | FileUtils.S_IRGRP | FileUtils.S_IROTH, + /* uid= */ -1, + /* gid= */ -1); + OutputStream out = new FileOutputStream(destination); + FileUtils.copy(in, out); + return destination; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}