Same as change I488018d01c0baf74660362a384e53bfe5d85de2b ("Use a service to export the updates"). While at it, move the code out of UpdaterService to better separate the code that deal with AB updates and regular updates. Change-Id: I2bc3e78e80f5e63c57303cbbcdc0353dbab0f67f
552 lines
20 KiB
Java
552 lines
20 KiB
Java
/*
|
|
* 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.content.Context;
|
|
import android.content.Intent;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.os.PowerManager;
|
|
import android.os.SystemClock;
|
|
import android.support.v4.content.LocalBroadcastManager;
|
|
import android.util.Log;
|
|
|
|
import org.lineageos.updater.UpdatesDbHelper;
|
|
import org.lineageos.updater.download.DownloadClient;
|
|
import org.lineageos.updater.misc.Utils;
|
|
import org.lineageos.updater.model.Update;
|
|
import org.lineageos.updater.model.UpdateInfo;
|
|
import org.lineageos.updater.model.UpdateStatus;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
public class UpdaterController implements Controller {
|
|
|
|
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_REMOVED = "action_update_removed";
|
|
public static final String ACTION_UPDATE_STATUS = "action_update_status_change";
|
|
public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";
|
|
|
|
private final String TAG = "UpdaterController";
|
|
|
|
private static UpdaterController sUpdaterController;
|
|
|
|
private static final int MAX_REPORT_INTERVAL_MS = 1000;
|
|
|
|
private final Context mContext;
|
|
private final LocalBroadcastManager mBroadcastManager;
|
|
private final UpdatesDbHelper mUpdatesDbHelper;
|
|
|
|
private final PowerManager.WakeLock mWakeLock;
|
|
|
|
private final File mDownloadRoot;
|
|
|
|
private int mActiveDownloads = 0;
|
|
private Set<String> mVerifyingUpdates = new HashSet<>();
|
|
|
|
public static synchronized UpdaterController getInstance() {
|
|
return sUpdaterController;
|
|
}
|
|
|
|
protected static synchronized UpdaterController getInstance(Context context) {
|
|
if (sUpdaterController == null) {
|
|
sUpdaterController = new UpdaterController(context);
|
|
}
|
|
return sUpdaterController;
|
|
}
|
|
|
|
private UpdaterController(Context context) {
|
|
mBroadcastManager = LocalBroadcastManager.getInstance(context);
|
|
mUpdatesDbHelper = new UpdatesDbHelper(context);
|
|
mDownloadRoot = Utils.getDownloadPath(context);
|
|
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
|
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Updater");
|
|
mWakeLock.setReferenceCounted(false);
|
|
mContext = context.getApplicationContext();
|
|
|
|
Utils.cleanupDownloadsDir(context);
|
|
|
|
for (Update update : mUpdatesDbHelper.getUpdates()) {
|
|
addUpdate(update, false);
|
|
}
|
|
}
|
|
|
|
private class DownloadEntry {
|
|
final Update mUpdate;
|
|
DownloadClient mDownloadClient;
|
|
private DownloadEntry(Update update) {
|
|
mUpdate = update;
|
|
}
|
|
}
|
|
|
|
private Map<String, DownloadEntry> mDownloads = new HashMap<>();
|
|
|
|
void notifyUpdateChange(String downloadId) {
|
|
Intent intent = new Intent();
|
|
intent.setAction(ACTION_UPDATE_STATUS);
|
|
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
|
|
mBroadcastManager.sendBroadcast(intent);
|
|
}
|
|
|
|
void notifyUpdateDelete(String downloadId) {
|
|
Intent intent = new Intent();
|
|
intent.setAction(ACTION_UPDATE_REMOVED);
|
|
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
|
|
mBroadcastManager.sendBroadcast(intent);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
private void addDownloadClient(DownloadEntry entry, DownloadClient downloadClient) {
|
|
if (entry.mDownloadClient != null) {
|
|
return;
|
|
}
|
|
entry.mDownloadClient = downloadClient;
|
|
mActiveDownloads++;
|
|
}
|
|
|
|
private void removeDownloadClient(DownloadEntry entry) {
|
|
if (entry.mDownloadClient == null) {
|
|
return;
|
|
}
|
|
entry.mDownloadClient = null;
|
|
mActiveDownloads--;
|
|
}
|
|
|
|
private DownloadClient.DownloadCallback getDownloadCallback(final String downloadId) {
|
|
return new DownloadClient.DownloadCallback() {
|
|
|
|
@Override
|
|
public void onResponse(int statusCode, String url, DownloadClient.Headers headers) {
|
|
final Update update = mDownloads.get(downloadId).mUpdate;
|
|
if (update.getFileSize() <= 0) {
|
|
String contentLength = headers.get("Content-Length");
|
|
if (contentLength != null) {
|
|
try {
|
|
long size = Long.parseLong(contentLength);
|
|
if (size > 0) {
|
|
update.setFileSize(size);
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
Log.e(TAG, "Could not get content-length");
|
|
}
|
|
}
|
|
}
|
|
update.setStatus(UpdateStatus.DOWNLOADING);
|
|
update.setPersistentStatus(UpdateStatus.Persistent.INCOMPLETE);
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mUpdatesDbHelper.addUpdateWithOnConflict(update,
|
|
SQLiteDatabase.CONFLICT_REPLACE);
|
|
}
|
|
}).start();
|
|
notifyUpdateChange(downloadId);
|
|
}
|
|
|
|
@Override
|
|
public void onSuccess(File destination) {
|
|
Log.d(TAG, "Download complete");
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
update.setStatus(UpdateStatus.VERIFYING);
|
|
removeDownloadClient(mDownloads.get(downloadId));
|
|
verifyUpdateAsync(downloadId);
|
|
notifyUpdateChange(downloadId);
|
|
tryReleaseWakelock();
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(boolean cancelled) {
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
if (cancelled) {
|
|
Log.d(TAG, "Download cancelled");
|
|
// Already notified
|
|
} else {
|
|
Log.e(TAG, "Download failed");
|
|
removeDownloadClient(mDownloads.get(downloadId));
|
|
update.setStatus(UpdateStatus.PAUSED_ERROR);
|
|
notifyUpdateChange(downloadId);
|
|
}
|
|
tryReleaseWakelock();
|
|
}
|
|
};
|
|
}
|
|
|
|
private DownloadClient.ProgressListener getProgressListener(final String downloadId) {
|
|
return new DownloadClient.ProgressListener() {
|
|
private long mLastUpdate = 0;
|
|
private int mProgress = 0;
|
|
|
|
@Override
|
|
public void update(long bytesRead, long contentLength, long speed, long eta,
|
|
boolean done) {
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
if (contentLength <= 0) {
|
|
if (update.getFileSize() <= 0) {
|
|
return;
|
|
} else {
|
|
contentLength = update.getFileSize();
|
|
}
|
|
}
|
|
if (contentLength <= 0) {
|
|
return;
|
|
}
|
|
final long now = SystemClock.elapsedRealtime();
|
|
int progress = Math.round(bytesRead * 100 / contentLength);
|
|
if (progress != mProgress || mLastUpdate - now > MAX_REPORT_INTERVAL_MS) {
|
|
mProgress = progress;
|
|
mLastUpdate = now;
|
|
update.setProgress(progress);
|
|
update.setEta(eta);
|
|
update.setSpeed(speed);
|
|
notifyDownloadProgress(downloadId);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private void verifyUpdateAsync(final String downloadId) {
|
|
mVerifyingUpdates.add(downloadId);
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
File file = update.getFile();
|
|
if (file.exists() && verifyPackage(file)) {
|
|
update.setPersistentStatus(UpdateStatus.Persistent.VERIFIED);
|
|
mUpdatesDbHelper.changeUpdateStatus(update);
|
|
update.setStatus(UpdateStatus.VERIFIED);
|
|
} else {
|
|
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
|
|
mUpdatesDbHelper.removeUpdate(downloadId);
|
|
update.setProgress(0);
|
|
update.setStatus(UpdateStatus.VERIFICATION_FAILED);
|
|
}
|
|
mVerifyingUpdates.remove(downloadId);
|
|
notifyUpdateChange(downloadId);
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
private boolean verifyPackage(File file) {
|
|
try {
|
|
android.os.RecoverySystem.verifyPackage(file, null, null);
|
|
Log.e(TAG, "Verification successful");
|
|
return true;
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Verification failed", e);
|
|
if (file.exists()) {
|
|
file.delete();
|
|
} else {
|
|
// The download was probably stopped. Exit silently
|
|
Log.e(TAG, "Error while verifying the file", e);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean fixUpdateStatus(Update update) {
|
|
switch (update.getPersistentStatus()) {
|
|
case UpdateStatus.Persistent.VERIFIED:
|
|
case UpdateStatus.Persistent.INCOMPLETE:
|
|
if (update.getFile() == null || !update.getFile().exists()) {
|
|
update.setStatus(UpdateStatus.UNKNOWN);
|
|
return false;
|
|
} else if (update.getFileSize() > 0) {
|
|
update.setStatus(UpdateStatus.PAUSED);
|
|
int progress = Math.round(
|
|
update.getFile().length() * 100 / update.getFileSize());
|
|
update.setProgress(progress);
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void setUpdatesNotAvailableOnline(List<String> downloadIds) {
|
|
for (String downloadId : downloadIds) {
|
|
DownloadEntry update = mDownloads.get(downloadId);
|
|
if (update != null) {
|
|
update.mUpdate.setAvailableOnline(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setUpdatesAvailableOnline(List<String> downloadIds, boolean purgeList) {
|
|
List<String> toRemove = new ArrayList<>();
|
|
for (DownloadEntry entry : mDownloads.values()) {
|
|
boolean online = downloadIds.contains(entry.mUpdate.getDownloadId());
|
|
entry.mUpdate.setAvailableOnline(online);
|
|
if (!online && purgeList &&
|
|
entry.mUpdate.getPersistentStatus() == UpdateStatus.Persistent.UNKNOWN) {
|
|
toRemove.add(entry.mUpdate.getDownloadId());
|
|
}
|
|
}
|
|
for (String downloadId : toRemove) {
|
|
Log.d(TAG, downloadId + " no longer available online, removing");
|
|
mDownloads.remove(downloadId);
|
|
notifyUpdateDelete(downloadId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean addUpdate(UpdateInfo update) {
|
|
return addUpdate(update, true);
|
|
}
|
|
|
|
private boolean addUpdate(final UpdateInfo updateInfo, boolean availableOnline) {
|
|
Log.d(TAG, "Adding download: " + updateInfo.getDownloadId());
|
|
if (mDownloads.containsKey(updateInfo.getDownloadId())) {
|
|
Log.d(TAG, "Download (" + updateInfo.getDownloadId() + ") already added");
|
|
Update updateAdded = mDownloads.get(updateInfo.getDownloadId()).mUpdate;
|
|
updateAdded.setAvailableOnline(availableOnline && updateAdded.getAvailableOnline());
|
|
updateAdded.setDownloadUrl(updateInfo.getDownloadUrl());
|
|
return false;
|
|
}
|
|
Update update = new Update(updateInfo);
|
|
if (!fixUpdateStatus(update) && !availableOnline) {
|
|
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
|
|
deleteUpdateAsync(update);
|
|
Log.d(TAG, update.getDownloadId() + " had an invalid status and is not online");
|
|
return false;
|
|
}
|
|
update.setAvailableOnline(availableOnline);
|
|
mDownloads.put(update.getDownloadId(), new DownloadEntry(update));
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean startDownload(String downloadId) {
|
|
Log.d(TAG, "Starting " + downloadId);
|
|
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
|
|
return false;
|
|
}
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
File destination = new File(mDownloadRoot, update.getName());
|
|
if (destination.exists()) {
|
|
destination = Utils.appendSequentialNumber(destination);
|
|
Log.d(TAG, "Changing name with " + destination.getName());
|
|
}
|
|
update.setFile(destination);
|
|
DownloadClient downloadClient;
|
|
try {
|
|
downloadClient = new DownloadClient.Builder()
|
|
.setUrl(update.getDownloadUrl())
|
|
.setDestination(update.getFile())
|
|
.setDownloadCallback(getDownloadCallback(downloadId))
|
|
.setProgressListener(getProgressListener(downloadId))
|
|
.build();
|
|
} catch (IOException exception) {
|
|
Log.e(TAG, "Could not build download client");
|
|
update.setStatus(UpdateStatus.PAUSED_ERROR);
|
|
notifyUpdateChange(downloadId);
|
|
return false;
|
|
}
|
|
addDownloadClient(mDownloads.get(downloadId), downloadClient);
|
|
update.setStatus(UpdateStatus.STARTING);
|
|
notifyUpdateChange(downloadId);
|
|
downloadClient.start();
|
|
mWakeLock.acquire();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean resumeDownload(String downloadId) {
|
|
Log.d(TAG, "Resuming " + downloadId);
|
|
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
|
|
return false;
|
|
}
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
File file = update.getFile();
|
|
if (file == null || !file.exists()) {
|
|
Log.e(TAG, "The destination file of " + downloadId + " doesn't exist, can't resume");
|
|
update.setStatus(UpdateStatus.PAUSED_ERROR);
|
|
notifyUpdateChange(downloadId);
|
|
return false;
|
|
}
|
|
if (file.exists() && update.getFileSize() > 0 && file.length() >= update.getFileSize()) {
|
|
Log.d(TAG, "File already downloaded, starting verification");
|
|
update.setStatus(UpdateStatus.VERIFYING);
|
|
verifyUpdateAsync(downloadId);
|
|
notifyUpdateChange(downloadId);
|
|
} else {
|
|
DownloadClient downloadClient;
|
|
try {
|
|
downloadClient = new DownloadClient.Builder()
|
|
.setUrl(update.getDownloadUrl())
|
|
.setDestination(update.getFile())
|
|
.setDownloadCallback(getDownloadCallback(downloadId))
|
|
.setProgressListener(getProgressListener(downloadId))
|
|
.build();
|
|
} catch (IOException exception) {
|
|
Log.e(TAG, "Could not build download client");
|
|
update.setStatus(UpdateStatus.PAUSED_ERROR);
|
|
notifyUpdateChange(downloadId);
|
|
return false;
|
|
}
|
|
addDownloadClient(mDownloads.get(downloadId), downloadClient);
|
|
update.setStatus(UpdateStatus.STARTING);
|
|
notifyUpdateChange(downloadId);
|
|
downloadClient.resume();
|
|
mWakeLock.acquire();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean pauseDownload(String downloadId) {
|
|
Log.d(TAG, "Pausing " + downloadId);
|
|
if (!isDownloading(downloadId)) {
|
|
return false;
|
|
}
|
|
|
|
DownloadEntry entry = mDownloads.get(downloadId);
|
|
entry.mDownloadClient.cancel();
|
|
removeDownloadClient(entry);
|
|
entry.mUpdate.setStatus(UpdateStatus.PAUSED);
|
|
entry.mUpdate.setEta(0);
|
|
entry.mUpdate.setSpeed(0);
|
|
notifyUpdateChange(downloadId);
|
|
return true;
|
|
}
|
|
|
|
private void deleteUpdateAsync(final Update update) {
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
File file = update.getFile();
|
|
if (file.exists() && !file.delete()) {
|
|
Log.e(TAG, "Could not delete " + file.getAbsolutePath());
|
|
}
|
|
mUpdatesDbHelper.removeUpdate(update.getDownloadId());
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteUpdate(String downloadId) {
|
|
Log.d(TAG, "Cancelling " + downloadId);
|
|
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
|
|
return false;
|
|
}
|
|
Update update = mDownloads.get(downloadId).mUpdate;
|
|
update.setStatus(UpdateStatus.DELETED);
|
|
update.setProgress(0);
|
|
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
|
|
deleteUpdateAsync(update);
|
|
|
|
if (!update.getAvailableOnline()) {
|
|
Log.d(TAG, "Download no longer available online, removing");
|
|
mDownloads.remove(downloadId);
|
|
notifyUpdateDelete(downloadId);
|
|
} else {
|
|
notifyUpdateChange(downloadId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public Set<String> getIds() {
|
|
return mDownloads.keySet();
|
|
}
|
|
|
|
@Override
|
|
public List<UpdateInfo> getUpdates() {
|
|
List<UpdateInfo> updates = new ArrayList<>();
|
|
for (DownloadEntry entry : mDownloads.values()) {
|
|
updates.add(entry.mUpdate);
|
|
}
|
|
return updates;
|
|
}
|
|
|
|
@Override
|
|
public UpdateInfo getUpdate(String downloadId) {
|
|
DownloadEntry entry = mDownloads.get(downloadId);
|
|
return entry != null ? entry.mUpdate : null;
|
|
}
|
|
|
|
Update getActualUpdate(String downloadId) {
|
|
DownloadEntry entry = mDownloads.get(downloadId);
|
|
return entry != null ? entry.mUpdate : null;
|
|
}
|
|
|
|
@Override
|
|
public boolean isDownloading(String downloadId) {
|
|
return mDownloads.containsKey(downloadId) &&
|
|
mDownloads.get(downloadId).mDownloadClient != null;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasActiveDownloads() {
|
|
return mActiveDownloads > 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean isVerifyingUpdate() {
|
|
return mVerifyingUpdates.size() > 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean isVerifyingUpdate(String downloadId) {
|
|
return mVerifyingUpdates.contains(downloadId);
|
|
}
|
|
|
|
@Override
|
|
public boolean isInstallingUpdate() {
|
|
return UpdateInstaller.isInstalling() ||
|
|
ABUpdateInstaller.isInstallingUpdate(mContext);
|
|
}
|
|
|
|
@Override
|
|
public boolean isInstallingUpdate(String downloadId) {
|
|
return UpdateInstaller.isInstalling(downloadId) ||
|
|
ABUpdateInstaller.isInstallingUpdate(mContext, downloadId);
|
|
}
|
|
|
|
@Override
|
|
public boolean isInstallingABUpdate() {
|
|
return ABUpdateInstaller.isInstallingUpdate(mContext);
|
|
}
|
|
}
|