From e245631b46e229dcb5bde0cf2a9639cc095c1168 Mon Sep 17 00:00:00 2001 From: Gabriele M Date: Tue, 4 Jul 2017 19:05:04 +0200 Subject: [PATCH] Add support for A/B (Seamless) System Updates Loosely based on: https://github.com/LineageOS/android_packages_apps_CMUpdater/commit/0465cb691de5acd2f459ce0687b988ddf050b354 --- AndroidManifest.xml | 2 + res/values/strings.xml | 8 + src/org/lineageos/updater/UpdateDownload.java | 9 + src/org/lineageos/updater/UpdateStatus.java | 3 +- .../lineageos/updater/UpdaterReceiver.java | 35 ++++ .../lineageos/updater/UpdatesActivity.java | 4 +- .../lineageos/updater/UpdatesListAdapter.java | 3 + .../updater/controller/ABUpdateInstaller.java | 159 ++++++++++++++++++ .../updater/controller/UpdaterController.java | 17 +- .../controller/UpdaterControllerInt.java | 2 + .../updater/controller/UpdaterService.java | 84 ++++++++- src/org/lineageos/updater/misc/Utils.java | 35 ++++ 12 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 src/org/lineageos/updater/UpdaterReceiver.java create mode 100644 src/org/lineageos/updater/controller/ABUpdateInstaller.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 46daa57e..9b47ac95 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -30,6 +30,8 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index dd5ad34d..aa9155d7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -40,4 +40,12 @@ Resume Delete Install + + Installing update package + Install error + Update installed + Finalizing package installation + Preparing for first boot + + Reboot diff --git a/src/org/lineageos/updater/UpdateDownload.java b/src/org/lineageos/updater/UpdateDownload.java index 447f0168..989ad8d9 100644 --- a/src/org/lineageos/updater/UpdateDownload.java +++ b/src/org/lineageos/updater/UpdateDownload.java @@ -26,6 +26,7 @@ public class UpdateDownload extends Update { private int mProgress; private long mEta; private long mSpeed; + private int mInstallProgress; public UpdateStatus getStatus() { return mStatus; @@ -82,4 +83,12 @@ public class UpdateDownload extends Update { public void setSpeed(long speed) { mSpeed = speed; } + + public int getInstallProgress() { + return mInstallProgress; + } + + public void setInstallProgress(int progress) { + mInstallProgress = progress; + } } diff --git a/src/org/lineageos/updater/UpdateStatus.java b/src/org/lineageos/updater/UpdateStatus.java index 6ec426f2..5cd1db74 100644 --- a/src/org/lineageos/updater/UpdateStatus.java +++ b/src/org/lineageos/updater/UpdateStatus.java @@ -27,7 +27,8 @@ public enum UpdateStatus { VERIFIED, VERIFICATION_FAILED, INSTALLING, - INSTALLED; + INSTALLED, + INSTALLATION_FAILED; public static final class Persistent { public static final int UNKNOWN = 0; diff --git a/src/org/lineageos/updater/UpdaterReceiver.java b/src/org/lineageos/updater/UpdaterReceiver.java new file mode 100644 index 00000000..7bdaa4ca --- /dev/null +++ b/src/org/lineageos/updater/UpdaterReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 The LineageOS 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 org.lineageos.updater; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; + +public class UpdaterReceiver extends BroadcastReceiver { + + public static final String ACTION_INSTALL_REBOOT = + "org.lineageos.updater.action.INSTALL_REBOOT"; + + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_INSTALL_REBOOT.equals(intent.getAction())) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(null); + } + } +} diff --git a/src/org/lineageos/updater/UpdatesActivity.java b/src/org/lineageos/updater/UpdatesActivity.java index ef20b5b9..55739efb 100644 --- a/src/org/lineageos/updater/UpdatesActivity.java +++ b/src/org/lineageos/updater/UpdatesActivity.java @@ -70,7 +70,8 @@ public class UpdatesActivity extends AppCompatActivity { public void onReceive(Context context, Intent intent) { if (UpdaterController.ACTION_UPDATE_STATUS.equals(intent.getAction())) { mAdapter.notifyDataSetChanged(); - } else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction())) { + } else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction()) || + UpdaterController.ACTION_INSTALL_PROGRESS.equals(intent.getAction())) { String downloadId = intent.getStringExtra(UpdaterController.EXTRA_DOWNLOAD_ID); mAdapter.notifyItemChanged(downloadId); } @@ -88,6 +89,7 @@ public class UpdatesActivity extends AppCompatActivity { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS); intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS); + intentFilter.addAction(UpdaterController.ACTION_INSTALL_PROGRESS); LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); } diff --git a/src/org/lineageos/updater/UpdatesListAdapter.java b/src/org/lineageos/updater/UpdatesListAdapter.java index 98d472d4..8552fa13 100644 --- a/src/org/lineageos/updater/UpdatesListAdapter.java +++ b/src/org/lineageos/updater/UpdatesListAdapter.java @@ -99,6 +99,9 @@ public class UpdatesListAdapter extends RecyclerView.Adapter lines = new ArrayList<>(); + for (String line; (line = br.readLine()) != null;) { + lines.add(line); + } + headerKeyValuePairs = new String[lines.size()]; + headerKeyValuePairs = lines.toArray(headerKeyValuePairs); + } + zipFile.close(); + } catch (IOException | IllegalArgumentException e) { + Log.e(TAG, "Could not prepare " + file, e); + mUpdaterController.getUpdate(mDownloadId).setStatus(UpdateStatus.INSTALLATION_FAILED); + mUpdaterController.notifyUpdateChange(mDownloadId); + return false; + } + + UpdateEngine updateEngine = new UpdateEngine(); + updateEngine.bind(mUpdateEngineCallback); + String zipFileUri = "file://" + file.getAbsolutePath(); + updateEngine.applyPayload(zipFileUri, offset, 0, headerKeyValuePairs); + + return true; + } + + static boolean isABUpdate(ZipFile zipFile) { + return zipFile.getEntry(PAYLOAD_BIN_PATH) != null && + zipFile.getEntry(PAYLOAD_PROPERTIES_PATH) != null; + } +} diff --git a/src/org/lineageos/updater/controller/UpdaterController.java b/src/org/lineageos/updater/controller/UpdaterController.java index 8b3ec0fd..66e59902 100644 --- a/src/org/lineageos/updater/controller/UpdaterController.java +++ b/src/org/lineageos/updater/controller/UpdaterController.java @@ -39,6 +39,7 @@ import java.util.Set; public class UpdaterController implements UpdaterControllerInt { public static final String ACTION_DOWNLOAD_PROGRESS = "action_download_progress"; + public static final String ACTION_INSTALL_PROGRESS = "action_install_progress"; public static final String ACTION_UPDATE_STATUS = "action_update_status_change"; public static final String EXTRA_DOWNLOAD_ID = "extra_download_id"; @@ -92,20 +93,27 @@ public class UpdaterController implements UpdaterControllerInt { private Map mDownloads = new HashMap<>(); - private void notifyUpdateChange(String downloadId) { + void notifyUpdateChange(String downloadId) { Intent intent = new Intent(); intent.setAction(ACTION_UPDATE_STATUS); intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); mBroadcastManager.sendBroadcast(intent); } - private void notifyDownloadProgress(String downloadId) { + void notifyDownloadProgress(String downloadId) { Intent intent = new Intent(); intent.setAction(ACTION_DOWNLOAD_PROGRESS); intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); mBroadcastManager.sendBroadcast(intent); } + void notifyInstallProgress(String downloadId) { + Intent intent = new Intent(); + intent.setAction(ACTION_INSTALL_PROGRESS); + intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); + mBroadcastManager.sendBroadcast(intent); + } + private void tryReleaseWakelock() { if (!hasActiveDownloads()) { mWakeLock.release(); @@ -433,4 +441,9 @@ public class UpdaterController implements UpdaterControllerInt { public boolean isVerifyingUpdate() { return mVerifyingUpdates > 0; } + + @Override + public boolean isInstallingUpdate() { + return ABUpdateInstaller.isInstallingUpdate(); + } } diff --git a/src/org/lineageos/updater/controller/UpdaterControllerInt.java b/src/org/lineageos/updater/controller/UpdaterControllerInt.java index 734e798a..014d8af7 100644 --- a/src/org/lineageos/updater/controller/UpdaterControllerInt.java +++ b/src/org/lineageos/updater/controller/UpdaterControllerInt.java @@ -43,4 +43,6 @@ public interface UpdaterControllerInt { boolean hasActiveDownloads(); boolean isVerifyingUpdate(); + + boolean isInstallingUpdate(); } diff --git a/src/org/lineageos/updater/controller/UpdaterService.java b/src/org/lineageos/updater/controller/UpdaterService.java index a0444b72..eed5769c 100644 --- a/src/org/lineageos/updater/controller/UpdaterService.java +++ b/src/org/lineageos/updater/controller/UpdaterService.java @@ -32,11 +32,13 @@ import android.util.Log; import org.lineageos.updater.R; import org.lineageos.updater.UpdateDownload; +import org.lineageos.updater.UpdaterReceiver; import org.lineageos.updater.UpdatesActivity; import org.lineageos.updater.misc.Utils; import java.io.IOException; import java.text.NumberFormat; +import java.util.zip.ZipFile; public class UpdaterService extends Service { @@ -60,7 +62,7 @@ public class UpdaterService extends Service { private NotificationManager mNotificationManager; private NotificationCompat.BigTextStyle mNotificationStyle;; - private UpdaterControllerInt mUpdaterController; + private UpdaterController mUpdaterController; @Override public void onCreate() { @@ -91,11 +93,16 @@ public class UpdaterService extends Service { } else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction())) { UpdateDownload update = mUpdaterController.getUpdate(downloadId); handleDownloadProgressChange(update); + } else if (UpdaterController.ACTION_INSTALL_PROGRESS.equals(intent.getAction())) { + UpdateDownload update = mUpdaterController.getUpdate(downloadId); + mNotificationBuilder.setContentTitle(update.getName()); + handleInstallProgress(update); } } }; IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS); + intentFilter.addAction(UpdaterController.ACTION_INSTALL_PROGRESS); intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS); LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); @@ -141,9 +148,16 @@ public class UpdaterService extends Service { } else if (ACTION_INSTALL_UPDATE.equals(intent.getAction())) { String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID); try { - Utils.triggerUpdate(this, mUpdaterController.getUpdate(downloadId)); + ZipFile zipFile = new ZipFile(mUpdaterController.getUpdate(downloadId).getFile()); + boolean isABUpdate = ABUpdateInstaller.isABUpdate(zipFile); + zipFile.close(); + if (isABUpdate) { + ABUpdateInstaller.start(mUpdaterController, downloadId); + } else { + Utils.triggerUpdate(this, mUpdaterController.getUpdate(downloadId)); + } } catch (IOException e) { - Log.e(TAG, "Could not install update"); + Log.e(TAG, "Could not install update", e); // TODO: user facing message } } @@ -260,6 +274,43 @@ public class UpdaterService extends Service { tryStopSelf(); break; } + case INSTALLING: { + mNotificationBuilder.mActions.clear(); + mNotificationBuilder.setProgress(0, 0, true); + mNotificationStyle.setSummaryText(null); + String text = getString(R.string.installing_update); + mNotificationStyle.bigText(text); + mNotificationBuilder.setTicker(text); + mNotificationBuilder.setOngoing(true); + startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + break; + } + case INSTALLED: { + stopForeground(STOP_FOREGROUND_DETACH); + mNotificationBuilder.setProgress(100, 100, false); + String text = getString(R.string.installing_update_finished); + mNotificationStyle.bigText(text); + mNotificationBuilder.addAction(R.drawable.ic_tab_install, + getString(R.string.reboot), + getRebootPendingIntent()); + mNotificationBuilder.setTicker(text); + mNotificationBuilder.setOngoing(false); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + tryStopSelf(); + break; + } + case INSTALLATION_FAILED: { + stopForeground(STOP_FOREGROUND_DETACH); + mNotificationBuilder.setProgress(0, 0, false); + String text = getString(R.string.installing_update_error); + mNotificationStyle.bigText(text); + mNotificationBuilder.setTicker(text); + mNotificationBuilder.setOngoing(false); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + tryStopSelf(); + break; + } } } @@ -281,6 +332,25 @@ public class UpdaterService extends Service { mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); } + private void handleInstallProgress(UpdateDownload update) { + int progress = update.getInstallProgress(); + mNotificationBuilder.setProgress(100, progress, false); + + mNotificationStyle.setBigContentTitle(update.getName()); + mNotificationBuilder.setContentTitle(update.getName()); + + if (progress == 0) { + mNotificationStyle.bigText(getString(R.string.finalizing_package)); + mNotificationBuilder.setProgress(0, 0, true); + } else { + String percent = NumberFormat.getPercentInstance().format(progress / 100.f); + mNotificationStyle.setSummaryText(percent); + mNotificationStyle.bigText(getString(R.string.preparing_ota_first_boot)); + } + + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + } + private PendingIntent getResumePendingIntent(String downloadId) { final Intent intent = new Intent(this, UpdaterService.class); intent.setAction(ACTION_DOWNLOAD_CONTROL); @@ -306,4 +376,12 @@ public class UpdaterService extends Service { return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); } + + private PendingIntent getRebootPendingIntent() { + final Intent intent = new Intent(this, UpdaterReceiver.class); + intent.setAction(UpdaterReceiver.ACTION_INSTALL_REBOOT); + return PendingIntent.getBroadcast(this, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); + } + } diff --git a/src/org/lineageos/updater/misc/Utils.java b/src/org/lineageos/updater/misc/Utils.java index 5ccb303e..79707023 100644 --- a/src/org/lineageos/updater/misc/Utils.java +++ b/src/org/lineageos/updater/misc/Utils.java @@ -37,9 +37,12 @@ import java.io.FileReader; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public class Utils { @@ -168,4 +171,36 @@ public class Utils { } return false; } + + /** + * Get the offset to the compressed data of a file inside the given zip + * + * @param zipFile input zip file + * @param entryPath full path of the entry + * @return the offset of the compressed, or -1 if not found + * @throws IOException + * @throws IllegalArgumentException if the given entry is not found + */ + public static long getZipEntryOffset(ZipFile zipFile, String entryPath) + throws IOException { + // Each entry has an header of (30 + n + m) bytes + // 'n' is the length of the file name + // 'm' is the length of the extra field + final int FIXED_HEADER_SIZE = 30; + Enumeration zipEntries = zipFile.entries(); + long offset = 0; + while (zipEntries.hasMoreElements()) { + ZipEntry entry = zipEntries.nextElement(); + int n = entry.getName().length(); + int m = entry.getExtra() == null ? 0 : entry.getExtra().length; + int headerSize = FIXED_HEADER_SIZE + n + m; + offset += headerSize; + if (entry.getName().equals(entryPath)) { + return offset; + } + offset += entry.getCompressedSize(); + } + Log.e(TAG, "Entry " + entryPath + " not found"); + throw new IllegalArgumentException("The given entry was not found"); + } }