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