1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.development; 18 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.os.Handler; 22 import android.os.ParcelFileDescriptor; 23 import android.os.PersistableBundle; 24 import android.os.PowerManager; 25 import android.os.RecoverySystem; 26 import android.os.SystemUpdateManager; 27 import android.os.UpdateEngine; 28 import android.os.UpdateEngineStable; 29 import android.os.UpdateEngineStableCallback; 30 import android.provider.Settings; 31 import android.util.Log; 32 import android.widget.LinearLayout; 33 import android.widget.ProgressBar; 34 import android.widget.Toast; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.annotation.VisibleForTesting; 39 import androidx.appcompat.app.AlertDialog; 40 import androidx.core.content.ContextCompat; 41 import androidx.preference.Preference; 42 import androidx.preference.SwitchPreference; 43 44 import com.android.settings.R; 45 import com.android.settings.core.PreferenceControllerMixin; 46 import com.android.settingslib.development.DeveloperOptionsPreferenceController; 47 48 import com.google.common.util.concurrent.FutureCallback; 49 import com.google.common.util.concurrent.Futures; 50 import com.google.common.util.concurrent.ListenableFuture; 51 import com.google.common.util.concurrent.ListeningExecutorService; 52 import com.google.common.util.concurrent.MoreExecutors; 53 54 import java.io.BufferedReader; 55 import java.io.File; 56 import java.io.FileNotFoundException; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.InputStreamReader; 60 import java.util.ArrayList; 61 import java.util.Enumeration; 62 import java.util.List; 63 import java.util.concurrent.Executors; 64 import java.util.zip.ZipEntry; 65 import java.util.zip.ZipFile; 66 67 /** Controller for 16K pages developer option */ 68 public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferenceController 69 implements Preference.OnPreferenceChangeListener, 70 PreferenceControllerMixin, 71 Enable16kbPagesDialogHost, 72 EnableExt4DialogHost { 73 74 private static final String TAG = "Enable16kPages"; 75 private static final String REBOOT_REASON = "toggle16k"; 76 private static final String ENABLE_16K_PAGES = "enable_16k_pages"; 77 private static final int ENABLE_4K_PAGE_SIZE = 0; 78 private static final int ENABLE_16K_PAGE_SIZE = 1; 79 80 private static final String SYSTEM_PATH = "/system"; 81 private static final String VENDOR_PATH = "/vendor"; 82 private static final String OTA_16K_PATH = "/boot_otas/boot_ota_16k.zip"; 83 private static final String OTA_4K_PATH = "/boot_otas/boot_ota_4k.zip"; 84 85 private static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin"; 86 private static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt"; 87 private static final int OFFSET_TO_FILE_NAME = 30; 88 public static final String EXPERIMENTAL_UPDATE_TITLE = "Android 16K Kernel Experimental Update"; 89 90 private @Nullable DevelopmentSettingsDashboardFragment mFragment; 91 private boolean mEnable16k; 92 93 private final ListeningExecutorService mExecutorService = 94 MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); 95 96 private AlertDialog mProgressDialog; 97 Enable16kPagesPreferenceController( @onNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment)98 public Enable16kPagesPreferenceController( 99 @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) { 100 super(context); 101 this.mFragment = fragment; 102 mEnable16k = Enable16kUtils.isUsing16kbPages(); 103 } 104 105 @Override isAvailable()106 public boolean isAvailable() { 107 return Enable16kUtils.is16KbToggleAvailable(); 108 } 109 110 @Override getPreferenceKey()111 public String getPreferenceKey() { 112 return ENABLE_16K_PAGES; 113 } 114 115 @Override onPreferenceChange(Preference preference, Object newValue)116 public boolean onPreferenceChange(Preference preference, Object newValue) { 117 mEnable16k = (Boolean) newValue; 118 // Prompt user to do oem unlock first 119 if (!Enable16kUtils.isDeviceOEMUnlocked(mContext)) { 120 Enable16KOemUnlockDialog.show(mFragment); 121 return false; 122 } 123 124 if (!Enable16kUtils.isDataExt4()) { 125 EnableExt4WarningDialog.show(mFragment, this); 126 return false; 127 } 128 Enable16kPagesWarningDialog.show(mFragment, this, mEnable16k); 129 return true; 130 } 131 132 @Override updateState(Preference preference)133 public void updateState(Preference preference) { 134 int defaultOptionValue = 135 Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE; 136 final int optionValue = 137 Settings.Global.getInt( 138 mContext.getContentResolver(), 139 Settings.Global.ENABLE_16K_PAGES, 140 defaultOptionValue /* default */); 141 142 ((SwitchPreference) mPreference).setChecked(optionValue == ENABLE_16K_PAGE_SIZE); 143 } 144 145 @Override onDeveloperOptionsSwitchDisabled()146 protected void onDeveloperOptionsSwitchDisabled() { 147 // TODO(295035851) : Revert kernel when dev option turned off 148 super.onDeveloperOptionsSwitchDisabled(); 149 Settings.Global.putInt( 150 mContext.getContentResolver(), 151 Settings.Global.ENABLE_16K_PAGES, 152 ENABLE_4K_PAGE_SIZE); 153 ((SwitchPreference) mPreference).setChecked(false); 154 } 155 156 @Override onDeveloperOptionsSwitchEnabled()157 protected void onDeveloperOptionsSwitchEnabled() { 158 int currentStatus = 159 Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE; 160 Settings.Global.putInt( 161 mContext.getContentResolver(), Settings.Global.ENABLE_16K_PAGES, currentStatus); 162 } 163 164 /** Called when user confirms reboot dialog */ 165 @Override on16kPagesDialogConfirmed()166 public void on16kPagesDialogConfirmed() { 167 // Show progress bar 168 mProgressDialog = makeProgressDialog(); 169 mProgressDialog.show(); 170 171 // Apply update in background 172 ListenableFuture future = mExecutorService.submit(() -> installUpdate()); 173 Futures.addCallback( 174 future, 175 new FutureCallback<>() { 176 177 @Override 178 public void onSuccess(@NonNull Object result) { 179 // This means UpdateEngineStable is working on applying update in 180 // background. 181 // Result of that operation will be provided by separate callback. 182 Log.i(TAG, "applyPayload call to UpdateEngineStable succeeded."); 183 } 184 185 @Override 186 public void onFailure(@NonNull Throwable t) { 187 hideProgressDialog(); 188 Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!", t); 189 displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); 190 } 191 }, 192 ContextCompat.getMainExecutor(mContext)); 193 } 194 195 /** Called when user dismisses to reboot dialog */ 196 @Override on16kPagesDialogDismissed()197 public void on16kPagesDialogDismissed() { 198 if (mPreference == null) { 199 return; 200 } 201 updateState(mPreference); 202 } 203 installUpdate()204 private void installUpdate() { 205 // Check if there is any pending system update 206 SystemUpdateManager manager = mContext.getSystemService(SystemUpdateManager.class); 207 Bundle data = manager.retrieveSystemUpdateInfo(); 208 int status = data.getInt(SystemUpdateManager.KEY_STATUS); 209 if (status != SystemUpdateManager.STATUS_UNKNOWN 210 && status != SystemUpdateManager.STATUS_IDLE) { 211 throw new RuntimeException( 212 "System has pending update! Please restart the device to complete applying" 213 + " pending update. If you are seeing this after using 16KB developer" 214 + " options, please check configuration and OTA packages!"); 215 } 216 217 // Publish system update info 218 PersistableBundle info = createUpdateInfo(SystemUpdateManager.STATUS_IN_PROGRESS); 219 manager.updateSystemUpdateInfo(info); 220 221 try { 222 File updateFile = getOtaFile(); 223 Log.i(TAG, "Update file path is " + updateFile.getAbsolutePath()); 224 applyUpdateFile(updateFile); 225 } catch (IOException e) { 226 throw new RuntimeException(e); 227 } 228 } 229 230 @VisibleForTesting applyUpdateFile(@onNull File updateFile)231 void applyUpdateFile(@NonNull File updateFile) throws IOException, FileNotFoundException { 232 boolean payloadFound = false; 233 boolean propertiesFound = false; 234 long payloadOffset = 0; 235 long payloadSize = 0; 236 237 List<String> properties = new ArrayList<>(); 238 try (ZipFile zip = new ZipFile(updateFile)) { 239 Enumeration<? extends ZipEntry> entries = zip.entries(); 240 long offset = 0; 241 while (entries.hasMoreElements()) { 242 ZipEntry zipEntry = entries.nextElement(); 243 String fileName = zipEntry.getName(); 244 long extraSize = zipEntry.getExtra() == null ? 0 : zipEntry.getExtra().length; 245 offset += OFFSET_TO_FILE_NAME + fileName.length() + extraSize; 246 247 if (zipEntry.isDirectory()) { 248 continue; 249 } 250 251 long length = zipEntry.getCompressedSize(); 252 if (PAYLOAD_BINARY_FILE_NAME.equals(fileName)) { 253 if (zipEntry.getMethod() != ZipEntry.STORED) { 254 throw new IOException("Unknown compression method."); 255 } 256 payloadFound = true; 257 payloadOffset = offset; 258 payloadSize = length; 259 } else if (PAYLOAD_PROPERTIES_FILE_NAME.equals(fileName)) { 260 propertiesFound = true; 261 InputStream inputStream = zip.getInputStream(zipEntry); 262 if (inputStream != null) { 263 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); 264 String line; 265 while ((line = br.readLine()) != null) { 266 properties.add(line); 267 } 268 } 269 } 270 offset += length; 271 } 272 } 273 274 if (!payloadFound) { 275 throw new FileNotFoundException( 276 "Failed to find payload in zip: " + updateFile.getAbsolutePath()); 277 } 278 279 if (!propertiesFound) { 280 throw new FileNotFoundException( 281 "Failed to find payload properties in zip: " + updateFile.getAbsolutePath()); 282 } 283 284 if (payloadSize == 0) { 285 throw new IOException("Found empty payload in zip: " + updateFile.getAbsolutePath()); 286 } 287 288 applyPayload(updateFile, payloadOffset, payloadSize, properties); 289 } 290 hideProgressDialog()291 private void hideProgressDialog() { 292 // Hide progress bar 293 if (mProgressDialog != null && mProgressDialog.isShowing()) { 294 mProgressDialog.hide(); 295 } 296 } 297 298 @VisibleForTesting applyPayload( @onNull File updateFile, long payloadOffset, long payloadSize, @NonNull List<String> properties)299 void applyPayload( 300 @NonNull File updateFile, 301 long payloadOffset, 302 long payloadSize, 303 @NonNull List<String> properties) 304 throws FileNotFoundException { 305 String[] header = properties.stream().toArray(String[]::new); 306 UpdateEngineStable updateEngineStable = new UpdateEngineStable(); 307 try { 308 ParcelFileDescriptor pfd = 309 ParcelFileDescriptor.open(updateFile, ParcelFileDescriptor.MODE_READ_ONLY); 310 updateEngineStable.bind( 311 new OtaUpdateCallback(updateEngineStable), 312 new Handler(mContext.getMainLooper())); 313 updateEngineStable.applyPayloadFd(pfd, payloadOffset, payloadSize, header); 314 } finally { 315 Log.e(TAG, "Failure while applying an update using update engine"); 316 } 317 } 318 displayToast(String message)319 private void displayToast(String message) { 320 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 321 } 322 323 @Override onExt4DialogConfirmed()324 public void onExt4DialogConfirmed() { 325 // user has confirmed to wipe the device 326 ListenableFuture future = mExecutorService.submit(() -> wipeData()); 327 Futures.addCallback( 328 future, 329 new FutureCallback<>() { 330 @Override 331 public void onSuccess(@NonNull Object result) { 332 Log.i(TAG, "Wiping /data with recovery system."); 333 } 334 335 @Override 336 public void onFailure(@NonNull Throwable t) { 337 Log.e(TAG, "Failed to change the /data partition to ext4"); 338 displayToast(mContext.getString(R.string.format_ext4_failure_toast)); 339 } 340 }, 341 ContextCompat.getMainExecutor(mContext)); 342 } 343 wipeData()344 private void wipeData() { 345 RecoverySystem recoveryService = mContext.getSystemService(RecoverySystem.class); 346 try { 347 recoveryService.wipePartitionToExt4(); 348 } catch (IOException e) { 349 throw new RuntimeException(e); 350 } 351 } 352 353 @Override onExt4DialogDismissed()354 public void onExt4DialogDismissed() { 355 // Do nothing 356 } 357 358 private class OtaUpdateCallback extends UpdateEngineStableCallback { 359 UpdateEngineStable mUpdateEngineStable; 360 OtaUpdateCallback(@onNull UpdateEngineStable engine)361 OtaUpdateCallback(@NonNull UpdateEngineStable engine) { 362 mUpdateEngineStable = engine; 363 } 364 365 @Override onStatusUpdate(int status, float percent)366 public void onStatusUpdate(int status, float percent) {} 367 368 @Override onPayloadApplicationComplete(int errorCode)369 public void onPayloadApplicationComplete(int errorCode) { 370 Log.i(TAG, "Callback from update engine stable received. unbinding.."); 371 // unbind the callback from update engine 372 mUpdateEngineStable.unbind(); 373 374 // Hide progress bar 375 hideProgressDialog(); 376 377 if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS) { 378 Log.i(TAG, "applyPayload successful"); 379 380 // Save changed preference 381 Settings.Global.putInt( 382 mContext.getContentResolver(), 383 Settings.Global.ENABLE_16K_PAGES, 384 mEnable16k ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE); 385 386 // Publish system update info 387 SystemUpdateManager manager = mContext.getSystemService(SystemUpdateManager.class); 388 PersistableBundle info = 389 createUpdateInfo(SystemUpdateManager.STATUS_WAITING_REBOOT); 390 manager.updateSystemUpdateInfo(info); 391 392 // Restart device to complete update 393 PowerManager pm = mContext.getSystemService(PowerManager.class); 394 pm.reboot(REBOOT_REASON); 395 } else { 396 Log.e(TAG, "applyPayload failed, error code: " + errorCode); 397 displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); 398 } 399 } 400 } 401 makeProgressDialog()402 private AlertDialog makeProgressDialog() { 403 AlertDialog.Builder builder = new AlertDialog.Builder(mFragment.getActivity()); 404 builder.setTitle(R.string.progress_16k_ota_title); 405 406 final ProgressBar progressBar = new ProgressBar(mFragment.getActivity()); 407 LinearLayout.LayoutParams params = 408 new LinearLayout.LayoutParams( 409 LinearLayout.LayoutParams.WRAP_CONTENT, 410 LinearLayout.LayoutParams.WRAP_CONTENT); 411 progressBar.setLayoutParams(params); 412 progressBar.setPadding(0, 24, 0, 24); 413 builder.setView(progressBar); 414 builder.setCancelable(false); 415 return builder.create(); 416 } 417 createUpdateInfo(int status)418 private PersistableBundle createUpdateInfo(int status) { 419 PersistableBundle infoBundle = new PersistableBundle(); 420 infoBundle.putInt(SystemUpdateManager.KEY_STATUS, status); 421 infoBundle.putBoolean(SystemUpdateManager.KEY_IS_SECURITY_UPDATE, false); 422 infoBundle.putString(SystemUpdateManager.KEY_TITLE, EXPERIMENTAL_UPDATE_TITLE); 423 return infoBundle; 424 } 425 426 // if BOARD_16K_OTA_MOVE_VENDOR, OTAs will be present on the /vendor partition getOtaFile()427 private File getOtaFile() throws FileNotFoundException { 428 String otaPath = mEnable16k ? OTA_16K_PATH : OTA_4K_PATH; 429 // Check if boot ota exists on vendor path and prefer vendor ota if present 430 String vendorOta = VENDOR_PATH + otaPath; 431 File vendorOtaFile = new File(vendorOta); 432 if (vendorOtaFile != null && vendorOtaFile.exists()) { 433 return vendorOtaFile; 434 } 435 436 // otherwise, fallback to boot ota from system partition 437 String systemOta = SYSTEM_PATH + otaPath; 438 File systemOtaFile = new File(systemOta); 439 if (systemOtaFile == null || !systemOtaFile.exists()) { 440 throw new FileNotFoundException("File not found at path " + systemOta); 441 } 442 return systemOtaFile; 443 } 444 } 445