Add support for A/B (Seamless) System Updates

Loosely based on:
0465cb691d
This commit is contained in:
Gabriele M
2017-07-04 19:05:04 +02:00
parent 311b2f7cfd
commit e245631b46
12 changed files with 354 additions and 7 deletions

View File

@@ -30,6 +30,8 @@
<service android:name=".controller.UpdaterService" /> <service android:name=".controller.UpdaterService" />
<receiver android:name=".UpdaterReceiver" exported="false" />
<receiver android:name=".UpdatesCheckReceiver"> <receiver android:name=".UpdatesCheckReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>

View File

@@ -40,4 +40,12 @@
<string name="resume_button">Resume</string> <string name="resume_button">Resume</string>
<string name="cancel_button">Delete</string> <string name="cancel_button">Delete</string>
<string name="install_button">Install</string> <string name="install_button">Install</string>
<string name="installing_update">Installing update package</string>
<string name="installing_update_error">Install error</string>
<string name="installing_update_finished">Update installed</string>
<string name="finalizing_package">Finalizing package installation</string>
<string name="preparing_ota_first_boot">Preparing for first boot</string>
<string name="reboot">Reboot</string>
</resources> </resources>

View File

@@ -26,6 +26,7 @@ public class UpdateDownload extends Update {
private int mProgress; private int mProgress;
private long mEta; private long mEta;
private long mSpeed; private long mSpeed;
private int mInstallProgress;
public UpdateStatus getStatus() { public UpdateStatus getStatus() {
return mStatus; return mStatus;
@@ -82,4 +83,12 @@ public class UpdateDownload extends Update {
public void setSpeed(long speed) { public void setSpeed(long speed) {
mSpeed = speed; mSpeed = speed;
} }
public int getInstallProgress() {
return mInstallProgress;
}
public void setInstallProgress(int progress) {
mInstallProgress = progress;
}
} }

View File

@@ -27,7 +27,8 @@ public enum UpdateStatus {
VERIFIED, VERIFIED,
VERIFICATION_FAILED, VERIFICATION_FAILED,
INSTALLING, INSTALLING,
INSTALLED; INSTALLED,
INSTALLATION_FAILED;
public static final class Persistent { public static final class Persistent {
public static final int UNKNOWN = 0; public static final int UNKNOWN = 0;

View File

@@ -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);
}
}
}

View File

@@ -70,7 +70,8 @@ public class UpdatesActivity extends AppCompatActivity {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (UpdaterController.ACTION_UPDATE_STATUS.equals(intent.getAction())) { if (UpdaterController.ACTION_UPDATE_STATUS.equals(intent.getAction())) {
mAdapter.notifyDataSetChanged(); 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); String downloadId = intent.getStringExtra(UpdaterController.EXTRA_DOWNLOAD_ID);
mAdapter.notifyItemChanged(downloadId); mAdapter.notifyItemChanged(downloadId);
} }
@@ -88,6 +89,7 @@ public class UpdatesActivity extends AppCompatActivity {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS); intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS);
intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS); intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS);
intentFilter.addAction(UpdaterController.ACTION_INSTALL_PROGRESS);
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
} }

View File

@@ -99,6 +99,9 @@ public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.
if (mUpdaterController.isDownloading(downloadId)) { if (mUpdaterController.isDownloading(downloadId)) {
setButtonAction(viewHolder.mButton1, Action.PAUSE, downloadId, true); setButtonAction(viewHolder.mButton1, Action.PAUSE, downloadId, true);
viewHolder.mButton2.setEnabled(false); viewHolder.mButton2.setEnabled(false);
} else if (mUpdaterController.isInstallingUpdate()) {
viewHolder.mButton1.setEnabled(false);
viewHolder.mButton2.setEnabled(false);
} else { } else {
// Allow one active download // Allow one active download
boolean enabled = !mUpdaterController.hasActiveDownloads() && boolean enabled = !mUpdaterController.hasActiveDownloads() &&

View File

@@ -0,0 +1,159 @@
/*
* 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.controller;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
import android.util.Log;
import org.lineageos.updater.UpdateDownload;
import org.lineageos.updater.UpdateStatus;
import org.lineageos.updater.misc.Utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
class ABUpdateInstaller {
private static final String TAG = "ABUpdateInstaller";
private static boolean sIsInstallingUpdate;
private static final String PAYLOAD_BIN_PATH = "payload.bin";
private static final String PAYLOAD_PROPERTIES_PATH = "payload_properties.txt";
private final UpdaterController mUpdaterController;
private final String mDownloadId;
private final UpdateEngineCallback mUpdateEngineCallback = new UpdateEngineCallback() {
@Override
public void onStatusUpdate(int status, float percent) {
switch (status) {
case UpdateEngine.UpdateStatusConstants.DOWNLOADING:
case UpdateEngine.UpdateStatusConstants.FINALIZING: {
int progress = Math.round(percent * 100);
mUpdaterController.getUpdate(mDownloadId).setInstallProgress(progress);
mUpdaterController.notifyInstallProgress(mDownloadId);
}
break;
case UpdateEngine.UpdateStatusConstants.REPORTING_ERROR_EVENT: {
UpdateDownload update = mUpdaterController.getUpdate(mDownloadId);
update.setInstallProgress(0);
update.setStatus(UpdateStatus.INSTALLATION_FAILED);
mUpdaterController.notifyUpdateChange(mDownloadId);;
}
break;
}
}
@Override
public void onPayloadApplicationComplete(int errorCode) {
sIsInstallingUpdate = false;
switch (errorCode) {
case UpdateEngine.ErrorCodeConstants.SUCCESS: {
UpdateDownload update = mUpdaterController.getUpdate(mDownloadId);
update.setInstallProgress(0);
update.setStatus(UpdateStatus.INSTALLED);
mUpdaterController.notifyUpdateChange(mDownloadId);
}
break;
default: {
UpdateDownload update = mUpdaterController.getUpdate(mDownloadId);
update.setInstallProgress(0);
update.setStatus(UpdateStatus.INSTALLATION_FAILED);
mUpdaterController.notifyUpdateChange(mDownloadId);
}
break;
}
}
};
static synchronized boolean start(UpdaterController updaterController,
String downloadId) {
if (sIsInstallingUpdate) {
return false;
}
ABUpdateInstaller installer = new ABUpdateInstaller(updaterController, downloadId);
sIsInstallingUpdate = installer.startUpdate();
return sIsInstallingUpdate;
}
static synchronized boolean isInstallingUpdate() {
return sIsInstallingUpdate;
}
private ABUpdateInstaller(UpdaterController updaterController, String downloadId) {
mUpdaterController = updaterController;
mDownloadId = downloadId;
}
private boolean startUpdate() {
File file = mUpdaterController.getUpdate(mDownloadId).getFile();
if (!file.exists()) {
Log.e(TAG, "The given update doesn't exist");
return false;
}
mUpdaterController.getUpdate(mDownloadId).setStatus(UpdateStatus.INSTALLING);
mUpdaterController.notifyUpdateChange(mDownloadId);
long offset;
String[] headerKeyValuePairs;
try {
ZipFile zipFile = new ZipFile(file);
offset = Utils.getZipEntryOffset(zipFile, PAYLOAD_BIN_PATH);
ZipEntry payloadPropEntry = zipFile.getEntry(PAYLOAD_PROPERTIES_PATH);
try (InputStream is = zipFile.getInputStream(payloadPropEntry);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr)) {
List<String> 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;
}
}

View File

@@ -39,6 +39,7 @@ import java.util.Set;
public class UpdaterController implements UpdaterControllerInt { public class UpdaterController implements UpdaterControllerInt {
public static final String ACTION_DOWNLOAD_PROGRESS = "action_download_progress"; 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 ACTION_UPDATE_STATUS = "action_update_status_change";
public static final String EXTRA_DOWNLOAD_ID = "extra_download_id"; public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
@@ -92,20 +93,27 @@ public class UpdaterController implements UpdaterControllerInt {
private Map<String, DownloadEntry> mDownloads = new HashMap<>(); private Map<String, DownloadEntry> mDownloads = new HashMap<>();
private void notifyUpdateChange(String downloadId) { void notifyUpdateChange(String downloadId) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(ACTION_UPDATE_STATUS); intent.setAction(ACTION_UPDATE_STATUS);
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
mBroadcastManager.sendBroadcast(intent); mBroadcastManager.sendBroadcast(intent);
} }
private void notifyDownloadProgress(String downloadId) { void notifyDownloadProgress(String downloadId) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(ACTION_DOWNLOAD_PROGRESS); intent.setAction(ACTION_DOWNLOAD_PROGRESS);
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId); intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
mBroadcastManager.sendBroadcast(intent); 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() { private void tryReleaseWakelock() {
if (!hasActiveDownloads()) { if (!hasActiveDownloads()) {
mWakeLock.release(); mWakeLock.release();
@@ -433,4 +441,9 @@ public class UpdaterController implements UpdaterControllerInt {
public boolean isVerifyingUpdate() { public boolean isVerifyingUpdate() {
return mVerifyingUpdates > 0; return mVerifyingUpdates > 0;
} }
@Override
public boolean isInstallingUpdate() {
return ABUpdateInstaller.isInstallingUpdate();
}
} }

View File

@@ -43,4 +43,6 @@ public interface UpdaterControllerInt {
boolean hasActiveDownloads(); boolean hasActiveDownloads();
boolean isVerifyingUpdate(); boolean isVerifyingUpdate();
boolean isInstallingUpdate();
} }

View File

@@ -32,11 +32,13 @@ import android.util.Log;
import org.lineageos.updater.R; import org.lineageos.updater.R;
import org.lineageos.updater.UpdateDownload; import org.lineageos.updater.UpdateDownload;
import org.lineageos.updater.UpdaterReceiver;
import org.lineageos.updater.UpdatesActivity; import org.lineageos.updater.UpdatesActivity;
import org.lineageos.updater.misc.Utils; import org.lineageos.updater.misc.Utils;
import java.io.IOException; import java.io.IOException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.zip.ZipFile;
public class UpdaterService extends Service { public class UpdaterService extends Service {
@@ -60,7 +62,7 @@ public class UpdaterService extends Service {
private NotificationManager mNotificationManager; private NotificationManager mNotificationManager;
private NotificationCompat.BigTextStyle mNotificationStyle;; private NotificationCompat.BigTextStyle mNotificationStyle;;
private UpdaterControllerInt mUpdaterController; private UpdaterController mUpdaterController;
@Override @Override
public void onCreate() { public void onCreate() {
@@ -91,11 +93,16 @@ public class UpdaterService extends Service {
} else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction())) { } else if (UpdaterController.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction())) {
UpdateDownload update = mUpdaterController.getUpdate(downloadId); UpdateDownload update = mUpdaterController.getUpdate(downloadId);
handleDownloadProgressChange(update); 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 intentFilter = new IntentFilter();
intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS); intentFilter.addAction(UpdaterController.ACTION_DOWNLOAD_PROGRESS);
intentFilter.addAction(UpdaterController.ACTION_INSTALL_PROGRESS);
intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS); intentFilter.addAction(UpdaterController.ACTION_UPDATE_STATUS);
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter); LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
@@ -141,9 +148,16 @@ public class UpdaterService extends Service {
} else if (ACTION_INSTALL_UPDATE.equals(intent.getAction())) { } else if (ACTION_INSTALL_UPDATE.equals(intent.getAction())) {
String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID); String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID);
try { 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) { } catch (IOException e) {
Log.e(TAG, "Could not install update"); Log.e(TAG, "Could not install update", e);
// TODO: user facing message // TODO: user facing message
} }
} }
@@ -260,6 +274,43 @@ public class UpdaterService extends Service {
tryStopSelf(); tryStopSelf();
break; 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()); 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) { private PendingIntent getResumePendingIntent(String downloadId) {
final Intent intent = new Intent(this, UpdaterService.class); final Intent intent = new Intent(this, UpdaterService.class);
intent.setAction(ACTION_DOWNLOAD_CONTROL); intent.setAction(ACTION_DOWNLOAD_CONTROL);
@@ -306,4 +376,12 @@ public class UpdaterService extends Service {
return PendingIntent.getService(this, 0, intent, return PendingIntent.getService(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 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);
}
} }

View File

@@ -37,9 +37,12 @@ import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Utils { public class Utils {
@@ -168,4 +171,36 @@ public class Utils {
} }
return false; 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<? extends ZipEntry> 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");
}
} }