Files
lineageos_updater/src/org/lineageos/updater/controller/UpdaterController.java
Gabriele M f0940dafca Verify package when resuming a completed download
It's possible to resume already completed downloads. When this
happens, starts verifying the package. Otherwise we won't be
able to resume the download since the server will likely reply
with 416.
2017-07-04 19:05:04 +02:00

400 lines
15 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.os.PowerManager;
import android.os.SystemClock;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.lineageos.updater.DownloadClient;
import org.lineageos.updater.UpdateDownload;
import org.lineageos.updater.UpdateStatus;
import org.lineageos.updater.UpdatesDbHelper;
import org.lineageos.updater.misc.Utils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class UpdaterController implements UpdaterControllerInt {
public static final String ACTION_DOWNLOAD_PROGRESS = "action_download_progress";
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 LocalBroadcastManager mBroadcastManager;
private final UpdatesDbHelper mUpdatesDbHelper;
private final PowerManager.WakeLock mWakeLock;
private final File mDownloadRoot;
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);
for (UpdateDownload update : mUpdatesDbHelper.getUpdates()) {
addUpdate(update, true);
}
}
private class DownloadEntry {
final UpdateDownload mUpdate;
DownloadClient mDownloadClient;
private DownloadEntry(UpdateDownload update) {
mUpdate = update;
}
}
private Map<String, DownloadEntry> mDownloads = new HashMap<>();
private 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) {
Intent intent = new Intent();
intent.setAction(ACTION_DOWNLOAD_PROGRESS);
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
mBroadcastManager.sendBroadcast(intent);
}
private void tryReleaseWakelock() {
if (!hasActiveDownloads()) {
mWakeLock.release();
}
}
private DownloadClient.DownloadCallback getDownloadCallback(final String downloadId) {
return new DownloadClient.DownloadCallback() {
@Override
public void onResponse(int statusCode, String url, DownloadClient.Headers headers) {
final UpdateDownload update = mDownloads.get(downloadId).mUpdate;
String contentLenght = headers.get("Content-Length");
if (contentLenght != null) {
try {
long size = Long.parseLong(contentLenght);
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);
}
}).start();
notifyUpdateChange(downloadId);
}
@Override
public void onSuccess(String body) {
Log.d(TAG, "Download complete");
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
update.setStatus(UpdateStatus.VERIFYING);
mDownloads.get(downloadId).mDownloadClient = null;
verifyUpdateAsync(downloadId);
notifyUpdateChange(downloadId);
tryReleaseWakelock();
}
@Override
public void onFailure() {
// The client is null if we intentionally stopped the download
boolean cancelled = mDownloads.get(downloadId).mDownloadClient == null;
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
if (cancelled) {
Log.d(TAG, "Download cancelled");
update.setStatus(UpdateStatus.PAUSED);
// Already notified
} else {
Log.e(TAG, "Download failed");
mDownloads.get(downloadId).mDownloadClient = null;
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) {
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;
getUpdate(downloadId).setProgress(progress);
getUpdate(downloadId).setEta(eta);
getUpdate(downloadId).setSpeed(speed);
notifyDownloadProgress(downloadId);
}
}
};
}
private void verifyUpdateAsync(final String downloadId) {
new Thread(new Runnable() {
@Override
public void run() {
UpdateDownload 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);
}
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(UpdateDownload 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 {
int progress = Math.round(
update.getFile().length() * 100 / update.getFileSize());
update.setProgress(progress);
}
break;
}
return true;
}
@Override
public boolean addUpdate(UpdateDownload update) {
return addUpdate(update, false);
}
private boolean addUpdate(final UpdateDownload update, boolean local) {
Log.d(TAG, "Adding download: " + update.getDownloadId());
if (mDownloads.containsKey(update.getDownloadId())) {
Log.e(TAG, "Download (" + update.getDownloadId() + ") already added");
return false;
}
if (!fixUpdateStatus(update) && local) {
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
new Thread(new Runnable() {
@Override
public void run() {
mUpdatesDbHelper.removeUpdate(update.getDownloadId());
}
}).start();
Log.d(TAG, update.getDownloadId() + " had an invalid status and is local");
return false;
}
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;
}
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
File destination = new File(mDownloadRoot, update.getName());
update.setFile(destination);
mDownloads.get(downloadId).mDownloadClient =
DownloadClient.downloadFile(update.getDownloadUrl(),
update.getFile(),
getDownloadCallback(downloadId),
getProgressListener(downloadId));
update.setStatus(UpdateStatus.STARTING);
notifyUpdateChange(downloadId);
mWakeLock.acquire();
return true;
}
@Override
public boolean resumeDownload(String downloadId) {
Log.d(TAG, "Resuming " + downloadId);
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
return false;
}
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
File file = update.getFile();
if (file.exists() && file.length() == update.getFileSize()) {
Log.d(TAG, "File already downloaded, starting verification");
update.setStatus(UpdateStatus.VERIFYING);
verifyUpdateAsync(downloadId);
notifyUpdateChange(downloadId);
} else {
mDownloads.get(downloadId).mDownloadClient =
DownloadClient.downloadFileResume(update.getDownloadUrl(),
update.getFile(),
getDownloadCallback(downloadId),
getProgressListener(downloadId));
update.setStatus(UpdateStatus.STARTING);
notifyUpdateChange(downloadId);
mWakeLock.acquire();
}
return true;
}
@Override
public boolean pauseDownload(String downloadId) {
Log.d(TAG, "Pausing " + downloadId);
if (!isDownloading(downloadId)) {
return false;
}
// First remove the client and then cancel the download so that when the download
// fails, we know it was intentional
DownloadClient downloadClient = mDownloads.get(downloadId).mDownloadClient;
mDownloads.get(downloadId).mDownloadClient = null;
downloadClient.cancel();
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
update.setStatus(UpdateStatus.PAUSED);
notifyUpdateChange(downloadId);
return true;
}
private void deleteUpdateAsync(final String downloadId) {
new Thread(new Runnable() {
@Override
public void run() {
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
File file = update.getFile();
if (file.exists() && !file.delete()) {
Log.e(TAG, "Could not delete " + file.getAbsolutePath());
}
mUpdatesDbHelper.removeUpdate(downloadId);
}
}).start();
}
@Override
public boolean cancelDownload(String downloadId) {
Log.d(TAG, "Cancelling " + downloadId);
if (!mDownloads.containsKey(downloadId) || isDownloading(downloadId)) {
return false;
}
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
update.setStatus(UpdateStatus.DELETED);
update.setProgress(0);
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
deleteUpdateAsync(downloadId);
notifyUpdateChange(downloadId);
return true;
}
@Override
public Set<String> getIds() {
return mDownloads.keySet();
}
@Override
public List<UpdateDownload> getUpdates() {
List<UpdateDownload> updates = new ArrayList<>();
for (DownloadEntry entry : mDownloads.values()) {
updates.add(entry.mUpdate);
}
return updates;
}
@Override
public UpdateDownload getUpdate(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() {
for (DownloadEntry entry : mDownloads.values()) {
if (entry.mDownloadClient != null) {
return true;
}
}
return false;
}
}