Project init

This commit is contained in:
Gabriele M
2017-07-02 17:46:36 +02:00
commit e7511d0041
25 changed files with 2108 additions and 0 deletions

37
Android.mk Normal file
View File

@@ -0,0 +1,37 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-design \
android-support-v4 \
android-support-v7-appcompat \
android-support-v7-cardview \
android-support-v7-preference \
android-support-v7-recyclerview \
okhttp
LOCAL_RESOURCE_DIR := \
$(TOP)/frameworks/support/design/res \
$(TOP)/frameworks/support/v7/appcompat/res \
$(TOP)/frameworks/support/v7/cardview/res \
$(TOP)/frameworks/support/v7/preference/res \
$(TOP)/frameworks/support/v7/recyclerview/res \
$(LOCAL_PATH)/res
LOCAL_AAPT_FLAGS := \
--auto-add-overlay \
--extra-packages android.support.design \
--extra-packages android.support.v7.appcompat \
--extra-packages android.support.v7.cardview \
--extra-packages android.support.v7.preference \
--extra-packages android.support.v7.recyclerview
LOCAL_PACKAGE_NAME := Updater
LOCAL_PRIVILEGED_MODULE := true
include $(BUILD_PACKAGE)

32
AndroidManifest.xml Normal file
View File

@@ -0,0 +1,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.lineageos.updater">
<uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light">
<activity
android:name=".UpdatesActivity"
android:label="@string/display_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.android.settings.SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name=".DownloadService" />
<receiver android:name=".UpdaterBroadcastReceiver" android:exported="false" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z" />
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</FrameLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</FrameLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/download_button" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_button" />
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

4
res/values/config.xml Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="download_in_cache">false</bool>
</resources>

38
res/values/strings.xml Normal file
View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Updater</string>
<string name="display_name">Updater</string>
<string name="download_path_cache" translatable="false">/cache/ota_package/</string>
<string name="download_path_data" translatable="false">/data/ota_package/</string>
<string name="conf_update_server_url_def" translatable="false">https://download.lineageos.org/api</string>
<string name="verification_failed_notification">Verification failed</string>
<string name="verifying_download_notification">Verifying download</string>
<string name="downloading_notification">Downloading</string>
<string name="download_paused_notification">Download paused</string>
<string name="download_paused_error_notification">Download error</string>
<string name="download_completed_notification">Download completed</string>
<string name="download_starting_notification">Starting download</string>
<string name="download_button">Download</string>
<string name="pause_button">Pause</string>
<string name="resume_button">Resume</string>
<string name="cancel_button">Delete</string>
<string name="install_button">Install</string>
</resources>

View File

@@ -0,0 +1,287 @@
/*
* 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.os.AsyncTask;
import android.os.SystemClock;
import android.util.Log;
import com.android.okhttp.Callback;
import com.android.okhttp.Interceptor;
import com.android.okhttp.MediaType;
import com.android.okhttp.OkHttpClient;
import com.android.okhttp.Response;
import com.android.okhttp.ResponseBody;
import com.android.okhttp.Request;
import com.android.okhttp.okio.BufferedSink;
import com.android.okhttp.okio.BufferedSource;
import com.android.okhttp.okio.Buffer;
import com.android.okhttp.okio.ForwardingSource;
import com.android.okhttp.okio.Okio;
import com.android.okhttp.okio.Source;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class DownloadClient {
private static final String TAG = "DownloadClient";
private static final Object DOWNLOAD_TAG = new Object();
public interface DownloadCallback {
void onResponse(int statusCode, String url, Headers headers);
void onSuccess(String body);
void onFailure();
}
public interface ProgressListener {
void update(long bytesRead, long contentLength, long speed, long eta, boolean done);
}
public class Headers {
private com.android.okhttp.Headers mHeaders;
private Headers(com.android.okhttp.Headers headers) {
mHeaders = headers;
}
public String get(String name) {
return mHeaders.get(name);
}
public Map<String, List<String>> getAll() {
return mHeaders.toMultimap();
}
}
private final OkHttpClient mClient = new OkHttpClient();
private long mResumeOffset = 0;
private DownloadClient() { }
public static void download(String url, DownloadCallback callback) {
DownloadClient downloadClient = new DownloadClient();
downloadClient.downloadInternal(url, callback);
}
public static DownloadClient downloadFile(String url, File destination,
DownloadCallback callback, ProgressListener progressListener) {
DownloadClient downloadClient = new DownloadClient();
downloadClient.downloadFileInternal(url, destination, callback, progressListener);
return downloadClient;
}
public static DownloadClient downloadFile(String url, File destination,
DownloadCallback callback) {
return downloadFile(url, destination, callback, null);
}
public static DownloadClient downloadFileResume(String url, File destination,
DownloadCallback callback, ProgressListener progressListener) {
DownloadClient downloadClient = new DownloadClient();
downloadClient.downloadFileResumeInternal(url, destination, callback, progressListener);
return downloadClient;
}
public void cancel() {
new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... unused) {
mClient.cancel(DOWNLOAD_TAG);
return null;
}
}.execute();
}
private void downloadInternal(String url, final DownloadCallback callback) {
final Request request = new Request.Builder()
.url(url)
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.d(TAG, "Download failed", e);
callback.onFailure();
}
@Override
public void onResponse(Response response) {
try {
callback.onResponse(response.code(), response.request().urlString(),
new Headers(response.headers()));
callback.onSuccess(response.body().string());
} catch (IOException e) {
onFailure(request, e);
}
}
});
}
private void downloadFileInternal(String url, final File destination,
final DownloadCallback callback, final ProgressListener progressListener) {
final Request request = new Request.Builder()
.url(url)
.tag(DOWNLOAD_TAG)
.build();
downloadFileInternalCommon(request, destination, callback, progressListener);
}
private void downloadFileResumeInternal(String url, final File destination,
final DownloadCallback callback, final ProgressListener progressListener) {
final Request.Builder requestBuilder = new Request.Builder()
.url(url)
.tag(DOWNLOAD_TAG);
long offset = destination.length();
requestBuilder.addHeader("Range", "bytes=" + offset + "-");
final Request request = requestBuilder.build();
downloadFileInternalCommon(request, destination, callback, progressListener);
}
private void downloadFileInternalCommon(final Request request, final File destination,
final DownloadCallback callback, final ProgressListener progressListener) {
mClient.networkInterceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
ProgressResponseBody progressResponseBody =
new ProgressResponseBody(originalResponse.body(), progressListener);
return originalResponse.newBuilder()
.body(progressResponseBody)
.build();
}
});
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.d(TAG, "Download failed", e);
callback.onFailure();
}
@Override
public void onResponse(Response response) {
Log.d(TAG, "Downloading");
final boolean resume = response.code() == 206;
if (resume) {
mResumeOffset = destination.length();
Log.d(TAG, "The server fulfilled the partial content request");
}
callback.onResponse(response.code(), response.request().urlString(),
new Headers(response.headers()));
try (BufferedSink sink = Okio.buffer(resume ?
Okio.appendingSink(destination) : Okio.sink(destination))) {
sink.writeAll(response.body().source());
Log.d(TAG, "Download complete");
sink.flush();
callback.onSuccess(null);
} catch (IOException e) {
onFailure(request, e);
}
}
});
}
private class ProgressResponseBody extends ResponseBody {
private final ResponseBody mResponseBody;
private final ProgressListener mProgressListener;
private BufferedSource mBufferedSource;
ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
mResponseBody = responseBody;
mProgressListener = progressListener;
}
@Override
public MediaType contentType() {
return mResponseBody.contentType();
}
@Override
public long contentLength() throws IOException {
return mResponseBody.contentLength();
}
@Override
public BufferedSource source() throws IOException {
if (mBufferedSource == null) {
mBufferedSource = Okio.buffer(source(mResponseBody.source()));
}
return mBufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
private long mTotalBytes = 0;
private long mTotalBytesRead = mResumeOffset;
private long mCurSampleBytes = 0;
private long mLastMillis = 0;
private long mSpeed = -1;
private long mEta = -1;
private void calculateSpeed() {
final long millis = SystemClock.elapsedRealtime();
final long delta = millis - mLastMillis;
if (delta > 500) {
final long curSpeed = ((mTotalBytesRead - mCurSampleBytes) * 1000) / delta;
if (mSpeed == -1) {
mSpeed = curSpeed;
} else {
mSpeed = ((mSpeed * 3) + curSpeed) / 4;
}
mLastMillis = millis;
mCurSampleBytes = mTotalBytesRead;
}
}
private void calculateEta() {
if (mSpeed > 0) {
mEta = (mTotalBytes - mTotalBytesRead) / mSpeed;
}
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
mTotalBytes = mResponseBody.contentLength() + mResumeOffset;
// read() returns the number of bytes read, or -1 if this source is exhausted.
mTotalBytesRead += bytesRead != -1 ? bytesRead : 0;
calculateSpeed();
calculateEta();
if (mProgressListener != null) {
mProgressListener.update(mTotalBytesRead, mTotalBytes,
mSpeed, mEta, bytesRead == -1);
}
return bytesRead;
}
};
}
}
}

View File

@@ -0,0 +1,357 @@
/*
* 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.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
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 DownloadController implements DownloadControllerInt {
public static final String PROGRESS_ACTION = "progress_action";
public static final String UPDATE_STATUS_ACTION = "update_status_change_action";
public static final String DOWNLOAD_ID_EXTRA = "download_id_extra";
private final String TAG = "DownloadController";
private static DownloadController sDownloadController;
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 DownloadController getInstance() {
return sDownloadController;
}
public static synchronized DownloadController newInstance(Context context) {
if (sDownloadController == null) {
sDownloadController = new DownloadController(context);
}
return sDownloadController;
}
private DownloadController(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);
}
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(UPDATE_STATUS_ACTION);
intent.putExtra(DOWNLOAD_ID_EXTRA, downloadId);
mBroadcastManager.sendBroadcast(intent);
}
private void notifyDownloadProgress(String downloadId) {
Intent intent = new Intent();
intent.setAction(PROGRESS_ACTION);
intent.putExtra(DOWNLOAD_ID_EXTRA, 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) {
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);
mUpdatesDbHelper.addUpdateWithOnConflict(update);
notifyUpdateChange(downloadId);
}
@Override
public void onSuccess(String body) {
UpdateDownload update = mDownloads.get(downloadId).mUpdate;
update.setStatus(UpdateStatus.VERIFYING);
notifyUpdateChange(downloadId);
Log.d(TAG, "Download complete");
if (!verifyDownload(update.getFile())) {
update.setPersistentStatus(UpdateStatus.Persistent.UNKNOWN);
mUpdatesDbHelper.removeUpdate(downloadId);
update.setProgress(0);
update.setStatus(UpdateStatus.VERIFICATION_FAILED);
} else {
update.setPersistentStatus(UpdateStatus.Persistent.VERIFIED);
mUpdatesDbHelper.changeUpdateStatus(update);
update.setStatus(UpdateStatus.VERIFIED);
}
mDownloads.get(downloadId).mDownloadClient = null;
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);
notifyDownloadProgress(downloadId);
}
}
};
}
private boolean verifyDownload(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, 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);
mUpdatesDbHelper.removeUpdate(update.getDownloadId());
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;
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 AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... voids) {
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);
return null;
}
}.execute();
}
@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) {
return mDownloads.get(downloadId).mUpdate;
}
@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;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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 java.util.List;
import java.util.Set;
public interface DownloadControllerInt {
boolean addUpdate(UpdateDownload update, boolean local);
List<UpdateDownload> getUpdates();
Set<String> getIds();
UpdateDownload getUpdate(String downloadId);
boolean startDownload(String downloadId);
boolean pauseDownload(String downloadId);
boolean cancelDownload(String downloadId);
boolean resumeDownload(String downloadId);
boolean isDownloading(String downloadId);
boolean hasActiveDownloads();
}

View File

@@ -0,0 +1,266 @@
/*
* 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.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
public class DownloadService extends Service {
private static final String TAG = "DownloadService";
private static final int NOTIFICATION_ID = 10;
private final IBinder mBinder = new LocalBinder();
private boolean mHasClients;
private BroadcastReceiver mBroadcastReceiver;
private NotificationCompat.Builder mNotificationBuilder;
private NotificationManager mNotificationManager;
private DownloadControllerInt mDownloadController;
@Override
public void onCreate() {
super.onCreate();
mDownloadController = DownloadController.newInstance(this);
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mNotificationBuilder = new NotificationCompat.Builder(this);
mNotificationBuilder.setSmallIcon(R.drawable.ic_system_update);
mNotificationBuilder.setShowWhen(false);
Intent notificationIntent = new Intent(this, UpdatesActivity.class);
PendingIntent intent = PendingIntent.getActivity(this, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
mNotificationBuilder.setContentIntent(intent);
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String downloadId = intent.getStringExtra(DownloadController.DOWNLOAD_ID_EXTRA);
if (DownloadController.UPDATE_STATUS_ACTION.equals(intent.getAction())) {
UpdateDownload update = mDownloadController.getUpdate(downloadId);
handleDownloadStatusChange(update);
} else if (DownloadController.PROGRESS_ACTION.equals(intent.getAction())) {
UpdateDownload update = mDownloadController.getUpdate(downloadId);
int progress = update.getProgress();
mNotificationBuilder.setProgress(100, progress, false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
}
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadController.PROGRESS_ACTION);
intentFilter.addAction(DownloadController.UPDATE_STATUS_ACTION);
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
}
@Override
public void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
super.onDestroy();
}
public class LocalBinder extends Binder {
public DownloadService getService() {
return DownloadService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
mHasClients = true;
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
mHasClients = false;
tryStopSelf();
return false;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
Log.d(TAG, "The service is being restarted, exit");
// The service is being restarted, exit without doing anything
stopForeground(STOP_FOREGROUND_DETACH);
mNotificationBuilder.setOngoing(false);
mNotificationManager.cancel(NOTIFICATION_ID);
stopSelf();
}
Log.d(TAG, "Service started");
return START_STICKY;
}
public DownloadControllerInt getDownloadController() {
return mDownloadController;
}
private void tryStopSelf() {
if (!mHasClients && !mDownloadController.hasActiveDownloads()) {
Log.d(TAG, "Service no longer needed, stopping");
stopSelf();
}
}
private void handleDownloadStatusChange(UpdateDownload update) {
switch (update.getStatus()) {
case DELETED: {
stopForeground(STOP_FOREGROUND_DETACH);
mNotificationBuilder.setOngoing(false);
mNotificationManager.cancel(NOTIFICATION_ID);
tryStopSelf();
break;
}
case STARTING: {
mNotificationBuilder.mActions.clear();
mNotificationBuilder.setProgress(0, 0, true);
String text = getString(R.string.download_starting_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(true);
startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
break;
}
case DOWNLOADING: {
String text = getString(R.string.downloading_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.addAction(com.android.internal.R.drawable.ic_media_pause,
getString(R.string.pause_button),
getPausePendingIntent(update.getDownloadId()));
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(true);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
break;
}
case PAUSED: {
stopForeground(STOP_FOREGROUND_DETACH);
mNotificationBuilder.mActions.clear();
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
String text = getString(R.string.download_paused_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.addAction(com.android.internal.R.drawable.ic_media_play,
getString(R.string.resume_button),
getResumePendingIntent(update.getDownloadId()));
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
break;
}
case PAUSED_ERROR: {
stopForeground(STOP_FOREGROUND_DETACH);
mNotificationBuilder.mActions.clear();
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
String text = getString(R.string.download_paused_error_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.addAction(com.android.internal.R.drawable.ic_media_play,
getString(R.string.resume_button),
getResumePendingIntent(update.getDownloadId()));
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
break;
}
case VERIFYING: {
mNotificationBuilder.setProgress(0, 0, true);
mNotificationBuilder.mActions.clear();
String text = getString(R.string.verifying_download_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
break;
}
case VERIFIED: {
stopForeground(STOP_FOREGROUND_DETACH);
mNotificationBuilder.setProgress(100, 100, false);
String text = getString(R.string.download_completed_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
tryStopSelf();
break;
}
case VERIFICATION_FAILED: {
stopForeground(STOP_FOREGROUND_DETACH);
String text = getString(R.string.verification_failed_notification);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.setBigContentTitle(text).bigText(update.getName());
mNotificationBuilder.setStyle(style);
mNotificationBuilder.setContentTitle(text);
mNotificationBuilder.setTicker(text);
mNotificationBuilder.setOngoing(false);
mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
tryStopSelf();
break;
}
}
}
private PendingIntent getResumePendingIntent(String downloadId) {
final Intent intent = new Intent(this, UpdaterBroadcastReceiver.class);
intent.setAction(UpdaterBroadcastReceiver.ACTION_DOWNLOAD);
intent.putExtra(UpdaterBroadcastReceiver.EXTRA_DOWNLOAD_ID, downloadId);
intent.putExtra(UpdaterBroadcastReceiver.EXTRA_DOWNLOAD_ACTION,
UpdaterBroadcastReceiver.RESUME);
return PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent getPausePendingIntent(String downloadId) {
final Intent intent = new Intent(this, UpdaterBroadcastReceiver.class);
intent.setAction(UpdaterBroadcastReceiver.ACTION_DOWNLOAD);
intent.putExtra(UpdaterBroadcastReceiver.EXTRA_DOWNLOAD_ID, downloadId);
intent.putExtra(UpdaterBroadcastReceiver.EXTRA_DOWNLOAD_ACTION,
UpdaterBroadcastReceiver.PAUSE);
return PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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;
public class Update implements Comparable<Update> {
private String mName;
private String mDownloadUrl;
private String mDownloadId;
private long mTimestamp;
private String mType;
private String mVersion;
public Update() {
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getDownloadId() {
return mDownloadId;
}
public void setDownloadId(String downloadId) {
mDownloadId = downloadId;
}
public long getTimestamp() {
return mTimestamp;
}
public void setTimestamp(long timestamp) {
mTimestamp = timestamp;
}
public String getType() {
return mType;
}
public void setType(String type) {
mType = type;
}
public String getVersion() {
return mVersion;
}
public void setVersion(String version) {
mVersion = version;
}
public String getDownloadUrl() {
return mDownloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
mDownloadUrl = downloadUrl;
}
@Override
public int compareTo(Update u) {
return mTimestamp < u.mTimestamp ? -1 : mTimestamp > u.mTimestamp ? 1 : 0;
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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 java.io.File;
public class UpdateDownload extends Update {
private UpdateStatus mStatus = UpdateStatus.UNKNOWN;
private int mPersistentStatus = UpdateStatus.Persistent.UNKNOWN;
private File mFile;
private long mFileSize;
private int mProgress;
private long mEta;
public UpdateStatus getStatus() {
return mStatus;
}
public void setStatus(UpdateStatus status) {
mStatus = status;
}
public int getPersistentStatus() {
return mPersistentStatus;
}
public void setPersistentStatus(int status) {
mPersistentStatus = status;
}
public File getFile() {
return mFile;
}
public void setFile(File file) {
mFile = file;
}
public long getFileSize() {
return mFileSize;
}
public void setFileSize(long fileSize) {
mFileSize = fileSize;
}
public int getProgress() {
return mProgress;
}
public void setProgress(int progress) {
mProgress = progress;
}
public long getEta() {
return mEta;
}
public void setEta(long eta) {
mEta = eta;
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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;
public enum UpdateStatus {
UNKNOWN,
STARTING,
DOWNLOADING,
DOWNLOADED,
PAUSED,
PAUSED_ERROR,
DELETED,
VERIFYING,
VERIFIED,
VERIFICATION_FAILED,
INSTALLING,
INSTALLED;
public static final class Persistent {
public static final int UNKNOWN = 0;
public static final int INCOMPLETE = 1;
public static final int VERIFIED = 2;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.util.Log;
public class UpdaterBroadcastReceiver extends BroadcastReceiver {
public static final String ACTION_DOWNLOAD =
"org.lineageos.updater.action.DOWNLOAD";
public static final String EXTRA_DOWNLOAD_ID =
"org.lineageos.updater.extra.DOWNLOAD_ID";
public static final String EXTRA_DOWNLOAD_ACTION =
"org.lineageos.updater.extra.DOWNLOAD_CHANGE";
public static final int PAUSE = 0;
public static final int RESUME = 1;
private final static String TAG = "BroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (ACTION_DOWNLOAD.equals(action)) {
if (intent.hasExtra(EXTRA_DOWNLOAD_ACTION) && intent.hasExtra(EXTRA_DOWNLOAD_ID)) {
DownloadController downloadController = DownloadController.getInstance();
if (downloadController == null) {
Log.e(TAG, "No download controller instance found");
return;
}
String downloadId = intent.getStringExtra(EXTRA_DOWNLOAD_ID);
int requestedAction = intent.getIntExtra(EXTRA_DOWNLOAD_ACTION, -1);
if (requestedAction == PAUSE) {
downloadController.pauseDownload(downloadId);
} else if (requestedAction == RESUME) {
downloadController.resumeDownload(downloadId);
} else {
Log.e(TAG, "Unknown action");
}
} else {
Log.e(TAG, "Missing extra data");
}
}
}
}

View File

@@ -0,0 +1,190 @@
/*
* 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.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SimpleItemAnimator;
import android.util.Log;
import org.json.JSONException;
import org.lineageos.updater.misc.Utils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UpdatesActivity extends AppCompatActivity {
private static final String TAG = "UpdatesActivity";
private DownloadService mDownloadService;
private BroadcastReceiver mBroadcastReceiver;
private UpdatesListAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_updates);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mAdapter = new UpdatesListAdapter(this);
recyclerView.setAdapter(mAdapter);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DownloadController.UPDATE_STATUS_ACTION.equals(intent.getAction())) {
mAdapter.notifyDataSetChanged();
} else if (DownloadController.PROGRESS_ACTION.equals(intent.getAction())) {
String downloadId = intent.getStringExtra(DownloadController.DOWNLOAD_ID_EXTRA);
mAdapter.notifyItemChanged(downloadId);
}
}
};
}
@Override
public void onStart() {
super.onStart();
Intent intent = new Intent(this, DownloadService.class);
startService(intent);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadController.UPDATE_STATUS_ACTION);
intentFilter.addAction(DownloadController.PROGRESS_ACTION);
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, intentFilter);
}
@Override
public void onStop() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
if (mDownloadService != null) {
unbindService(mConnection);
}
super.onStop();
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
DownloadService.LocalBinder binder = (DownloadService.LocalBinder) service;
mDownloadService = binder.getService();
mAdapter.setDownloadController(mDownloadService.getDownloadController());
getUpdatesList();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mAdapter.setDownloadController(null);
mDownloadService = null;
mAdapter.notifyDataSetChanged();
}
};
private void loadUpdatesList() throws IOException, JSONException {
// Process local files first. If they aren't valid, the controller will delete
// them from the database. If they are valid, they should be prioritized.
Log.d(TAG, "Getting updates from internal database");
DownloadControllerInt controller = mDownloadService.getDownloadController();
UpdatesDbHelper dbHelper = new UpdatesDbHelper(this);
for (UpdateDownload update : dbHelper.getUpdates()) {
controller.addUpdate(update, true);
}
Log.d(TAG, "Adding remote updates");
File jsonFile = Utils.getCachedUpdateList(this);
for (UpdateDownload update : Utils.parseJson(jsonFile, true)) {
controller.addUpdate(update, true);
}
List<String> updateIds = new ArrayList<>();
updateIds.addAll(controller.getIds());
Collections.sort(updateIds);
mAdapter.setData(updateIds);
mAdapter.notifyDataSetChanged();
}
private void getUpdatesList() {
File jsonFile = Utils.getCachedUpdateList(this);
if (jsonFile.exists()) {
try {
loadUpdatesList();
Log.d(TAG, "Cached list parsed");
} catch (IOException | JSONException e) {
Log.e(TAG, "Error while parsing json list", e);
}
} else {
downloadUpdatesList();
}
}
private void downloadUpdatesList() {
File jsonFile = Utils.getCachedUpdateList(this);
String url = Utils.getServerURL(this);
Log.d(TAG, "Checking " + url);
DownloadClient.downloadFile(url, jsonFile, new DownloadClient.DownloadCallback() {
@Override
public void onFailure() {
Log.e(TAG, "Could not download updates list");
}
@Override
public void onResponse(int statusCode, String url,
DownloadClient.Headers headers) {
}
@Override
public void onSuccess(String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Log.d(TAG, "List downloaded");
loadUpdatesList();
} catch (IOException | JSONException e) {
Log.e(TAG, "Could not read json", e);
}
}
});
}
});
}
}

View File

@@ -0,0 +1,207 @@
/*
* 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.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class UpdatesDbHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "updates.db";
public static class UpdateEntry implements BaseColumns {
public static final String TABLE_NAME = "updates";
public static final String COLUMN_NAME_STATUS = "status";
public static final String COLUMN_NAME_PATH = "path";
public static final String COLUMN_NAME_NAME = "name";
public static final String COLUMN_NAME_DOWNLOAD_ID = "download_id";
public static final String COLUMN_NAME_DOWNLOAD_URL = "download_url";
public static final String COLUMN_NAME_TIMESTAMP = "timestamp";
public static final String COLUMN_NAME_TYPE = "type";
public static final String COLUMN_NAME_VERSION = "version";
public static final String COLUMN_NAME_SIZE = "size";
}
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + UpdateEntry.TABLE_NAME + " (" +
UpdateEntry._ID + " INTEGER PRIMARY KEY," +
UpdateEntry.COLUMN_NAME_STATUS + " INTEGER," +
UpdateEntry.COLUMN_NAME_PATH + " TEXT," +
UpdateEntry.COLUMN_NAME_NAME + " TEXT," +
UpdateEntry.COLUMN_NAME_DOWNLOAD_ID + " TEXT NOT NULL UNIQUE," +
UpdateEntry.COLUMN_NAME_DOWNLOAD_URL + " TEXT," +
UpdateEntry.COLUMN_NAME_TIMESTAMP + " INTEGER," +
UpdateEntry.COLUMN_NAME_TYPE + " TEXT," +
UpdateEntry.COLUMN_NAME_VERSION + " TEXT," +
UpdateEntry.COLUMN_NAME_SIZE + " INTEGER)";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + UpdateEntry.TABLE_NAME;
public UpdatesDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
public long addUpdate(UpdateDownload update) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(UpdateEntry.COLUMN_NAME_STATUS, update.getPersistentStatus());
values.put(UpdateEntry.COLUMN_NAME_PATH, update.getFile().getAbsolutePath());
values.put(UpdateEntry.COLUMN_NAME_NAME, update.getName());
values.put(UpdateEntry.COLUMN_NAME_DOWNLOAD_ID, update.getDownloadId());
values.put(UpdateEntry.COLUMN_NAME_DOWNLOAD_URL, update.getDownloadUrl());
values.put(UpdateEntry.COLUMN_NAME_TIMESTAMP, update.getTimestamp());
values.put(UpdateEntry.COLUMN_NAME_TYPE, update.getType());
values.put(UpdateEntry.COLUMN_NAME_VERSION, update.getVersion());
values.put(UpdateEntry.COLUMN_NAME_SIZE, update.getFileSize());
return db.insert(UpdateEntry.TABLE_NAME, null, values);
}
public long addUpdateWithOnConflict(UpdateDownload update) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(UpdateEntry.COLUMN_NAME_STATUS, update.getPersistentStatus());
values.put(UpdateEntry.COLUMN_NAME_PATH, update.getFile().getAbsolutePath());
values.put(UpdateEntry.COLUMN_NAME_NAME, update.getName());
values.put(UpdateEntry.COLUMN_NAME_DOWNLOAD_ID, update.getDownloadId());
values.put(UpdateEntry.COLUMN_NAME_DOWNLOAD_URL, update.getDownloadUrl());
values.put(UpdateEntry.COLUMN_NAME_TIMESTAMP, update.getTimestamp());
values.put(UpdateEntry.COLUMN_NAME_TYPE, update.getType());
values.put(UpdateEntry.COLUMN_NAME_VERSION, update.getVersion());
values.put(UpdateEntry.COLUMN_NAME_SIZE, update.getFileSize());
return db.insertWithOnConflict(UpdateEntry.TABLE_NAME, null, values,
SQLiteDatabase.CONFLICT_IGNORE);
}
public boolean removeUpdate(String downloadId) {
SQLiteDatabase db = getWritableDatabase();
String selection = UpdateEntry.COLUMN_NAME_DOWNLOAD_ID + " = ?";
String[] selectionArgs = {downloadId};
return db.delete(UpdateEntry.TABLE_NAME, selection, selectionArgs) != 0;
}
public boolean removeUpdate(long rowId) {
SQLiteDatabase db = getWritableDatabase();
String selection = UpdateEntry._ID + " = " + rowId;
return db.delete(UpdateEntry.TABLE_NAME, selection, null) != 0;
}
public boolean changeUpdateStatus(UpdateDownload update) {
String selection = UpdateEntry.COLUMN_NAME_DOWNLOAD_ID + " = ?";
String[] selectionArgs = {update.getDownloadId()};
return changeUpdateStatus(selection, selectionArgs, update.getPersistentStatus());
}
public boolean changeUpdateStatus(long rowId, int status) {
String selection = UpdateEntry._ID + " = " + rowId;
return changeUpdateStatus(selection, null, status);
}
private boolean changeUpdateStatus(String selection, String[] selectionArgs,
int status) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(UpdateEntry.COLUMN_NAME_STATUS, status);
return db.update(UpdateEntry.TABLE_NAME, values, selection, selectionArgs) != 0;
}
public UpdateDownload getUpdate(long rowId) {
String selection = UpdateEntry._ID + " = " + rowId;
return getUpdate(selection, null);
}
public UpdateDownload getUpdate(String downloadId) {
String selection = UpdateEntry.COLUMN_NAME_DOWNLOAD_ID + " = ?";
String[] selectionArgs = {downloadId};
return getUpdate(selection, selectionArgs);
}
private UpdateDownload getUpdate(String selection, String[] selectionArgs) {
List<UpdateDownload> updates = getUpdates(selection, selectionArgs);
return updates != null ? updates.get(0) : null;
}
public List<UpdateDownload> getUpdates() {
return getUpdates(null, null);
}
public List<UpdateDownload> getUpdates(String selection, String[] selectionArgs) {
SQLiteDatabase db = getReadableDatabase();
String[] projection = {
UpdateEntry.COLUMN_NAME_PATH,
UpdateEntry.COLUMN_NAME_NAME,
UpdateEntry.COLUMN_NAME_DOWNLOAD_ID,
UpdateEntry.COLUMN_NAME_DOWNLOAD_URL,
UpdateEntry.COLUMN_NAME_TIMESTAMP,
UpdateEntry.COLUMN_NAME_TYPE,
UpdateEntry.COLUMN_NAME_VERSION,
UpdateEntry.COLUMN_NAME_STATUS,
UpdateEntry.COLUMN_NAME_SIZE,
};
String sort = UpdateEntry.COLUMN_NAME_TIMESTAMP + " DESC";
Cursor cursor = db.query(UpdateEntry.TABLE_NAME, projection, selection, selectionArgs,
null, null, sort);
List<UpdateDownload> updates = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
UpdateDownload update = new UpdateDownload();
int index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_PATH);
update.setFile(new File(cursor.getString(index)));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_NAME);
update.setName(cursor.getString(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_DOWNLOAD_ID);
update.setDownloadId(cursor.getString(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_DOWNLOAD_URL);
update.setDownloadUrl(cursor.getString(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_TIMESTAMP);
update.setTimestamp(cursor.getLong(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_TYPE);
update.setType(cursor.getString(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_VERSION);
update.setVersion(cursor.getString(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_STATUS);
update.setPersistentStatus(cursor.getInt(index));
index = cursor.getColumnIndex(UpdateEntry.COLUMN_NAME_SIZE);
update.setFileSize(cursor.getLong(index));
updates.add(update);
}
cursor.close();
}
return updates;
}
}

View File

@@ -0,0 +1,192 @@
/*
* 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.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.lineageos.updater.misc.Utils;
import java.io.IOException;
import java.util.List;
public class UpdatesListAdapter extends RecyclerView.Adapter<UpdatesListAdapter.ViewHolder> {
private static final String TAG = "UpdateListAdapter";
private List<String> mDownloadIds;
private DownloadControllerInt mDownloadController;
private Context mContext;
private enum Action {
DOWNLOAD,
PAUSE,
RESUME,
CANCEL,
INSTALL
}
public static class ViewHolder extends RecyclerView.ViewHolder {
private TextView mName;
private Button mButton1;
private Button mButton2;
private ProgressBar mProgressBar;
public ViewHolder(final View view) {
super(view);
mName = (TextView) view.findViewById(R.id.name);
mButton1 = (Button) view.findViewById(R.id.button1);
mButton2 = (Button) view.findViewById(R.id.button2);
mProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
}
}
public UpdatesListAdapter(Context context) {
mContext = context;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.update_item_view, viewGroup, false);
return new ViewHolder(view);
}
public void setDownloadController(DownloadControllerInt downloadController) {
mDownloadController = downloadController;
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(final ViewHolder viewHolder, int i) {
if (mDownloadController == null) {
viewHolder.mButton1.setEnabled(false);
viewHolder.mButton2.setEnabled(false);
return;
}
final String downloadId = mDownloadIds.get(i);
UpdateDownload update = mDownloadController.getUpdate(downloadId);
viewHolder.mName.setText(update.getName());
viewHolder.mProgressBar.setProgress(update.getProgress());
boolean indeterminate = update.getStatus() == UpdateStatus.STARTING ||
update.getStatus() == UpdateStatus.VERIFYING ||
update.getStatus() == UpdateStatus.INSTALLING;
viewHolder.mProgressBar.setIndeterminate(indeterminate);
if (mDownloadController.isDownloading(downloadId)) {
setButtonAction(viewHolder.mButton1, Action.PAUSE, downloadId, true);
viewHolder.mButton2.setEnabled(false);
} else {
// Allow one active download
boolean enabled = !mDownloadController.hasActiveDownloads();
int persistentStatus = update.getPersistentStatus();
if (persistentStatus == UpdateStatus.Persistent.INCOMPLETE) {
setButtonAction(viewHolder.mButton1, Action.RESUME, downloadId, enabled);
setButtonAction(viewHolder.mButton2, Action.CANCEL, downloadId, enabled);
} else if (persistentStatus == UpdateStatus.Persistent.VERIFIED) {
setButtonAction(viewHolder.mButton1, Action.INSTALL, downloadId, enabled);
setButtonAction(viewHolder.mButton2, Action.CANCEL, downloadId, enabled);
} else {
setButtonAction(viewHolder.mButton1, Action.DOWNLOAD, downloadId, enabled);
setButtonAction(viewHolder.mButton2, Action.CANCEL, downloadId, false);
}
}
}
@Override
public int getItemCount() {
return mDownloadIds == null ? 0 : mDownloadIds.size();
}
public void setData(List<String> downloadIds) {
mDownloadIds = downloadIds;
}
public void notifyItemChanged(String downloadId) {
notifyItemChanged(mDownloadIds.indexOf(downloadId));
}
private void setButtonAction(Button button, Action action, final String downloadId,
boolean enabled) {
switch (action) {
case DOWNLOAD:
button.setText(R.string.download_button);
button.setEnabled(enabled);
button.setOnClickListener(!enabled ? null : new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadController.startDownload(downloadId);
}
});
break;
case PAUSE:
button.setText(R.string.pause_button);
button.setEnabled(enabled);
button.setOnClickListener(!enabled ? null : new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadController.pauseDownload(downloadId);
}
});
break;
case RESUME:
button.setText(R.string.resume_button);
button.setEnabled(enabled);
button.setOnClickListener(!enabled ? null : new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadController.resumeDownload(downloadId);
}
});
break;
case CANCEL:
button.setText(R.string.cancel_button);
button.setEnabled(enabled);
button.setOnClickListener(!enabled ? null : new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadController.cancelDownload(downloadId);
}
});
break;
case INSTALL:
button.setText(R.string.install_button);
button.setEnabled(enabled);
button.setOnClickListener(!enabled ? null : new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
Utils.triggerUpdate(mContext,
mDownloadController.getUpdate(downloadId));
} catch (IOException e) {
Log.e(TAG, "Could not trigger the update", e);
// TODO: show error message
}
}
});
break;
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.misc;
import android.content.Context;
import android.os.SystemProperties;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.lineageos.updater.R;
import org.lineageos.updater.Update;
import org.lineageos.updater.UpdateDownload;
import org.lineageos.updater.UpdateStatus;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
public class Utils {
private static final String TAG = "Utils";
public static File getDownloadPath(Context context) {
boolean useCache = context.getResources().getBoolean(R.bool.download_in_cache);
int id = useCache ? R.string.download_path_cache : R.string.download_path_data;
return new File(context.getString(id));
}
public static File getCachedUpdateList(Context context) {
return new File(context.getCacheDir(), "updates.json");
}
// This should really return an Update object, but currently this only
// used to initialize UpdateDownload objects
private static UpdateDownload parseJsonUpdate(JSONObject object) throws JSONException {
UpdateDownload update = new UpdateDownload();
update.setTimestamp(object.getLong("datetime"));
update.setName(object.getString("filename"));
update.setDownloadId(object.getString("id"));
update.setType(object.getString("romtype"));
update.setDownloadUrl(object.getString("url"));
update.setVersion(object.getString("version"));
return update;
}
public static boolean isCompatible(Update update) {
if (update.getTimestamp() < SystemProperties.getLong("ro.build.date.utc", 0)) {
Log.d(TAG, update.getName() + " is older than current build");
return false;
}
if (!update.getType().equalsIgnoreCase(SystemProperties.get("ro.cm.releasetype"))) {
Log.d(TAG, update.getName() + " has type " + update.getType());
return false;
}
if (!update.getVersion().equalsIgnoreCase(SystemProperties.get("ro.cm.build.version"))) {
Log.d(TAG, update.getName() + " has version " + update.getVersion());
return false;
}
return true;
}
public static List<UpdateDownload> parseJson(File file, boolean compatibleOnly)
throws IOException, JSONException {
List<UpdateDownload> updates = new ArrayList<>();
String json = "";
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
for (String line; (line = br.readLine()) != null;) {
json += line;
}
}
JSONObject obj = new JSONObject(json);
JSONArray updatesList = obj.getJSONArray("response");
for (int i = 0; i < updatesList.length(); i++) {
if (updatesList.isNull(i)) {
continue;
}
try {
UpdateDownload update = parseJsonUpdate(updatesList.getJSONObject(i));
if (compatibleOnly && isCompatible(update)) {
updates.add(update);
} else {
Log.d(TAG, "Ignoring incompatible update " + update.getName());
}
} catch (JSONException e) {
Log.e(TAG, "Could not parse update object, index=" + i);
}
}
return updates;
}
public static String getServerURL(Context context) {
String serverUrl = SystemProperties.get("cm.updater.uri");
if (serverUrl.trim().isEmpty()) {
serverUrl = context.getString(R.string.conf_update_server_url_def);
}
String incrementalVersion = SystemProperties.get("ro.build.version.incremental");
String device = SystemProperties.get("ro.cm.device").toLowerCase();
String type = SystemProperties.get("ro.cm.releasetype").toLowerCase();
return serverUrl + "/v1/" + device + "/" + type + "/" + incrementalVersion;
}
public static void triggerUpdate(Context context, UpdateDownload update) throws IOException {
if (update.getStatus() == UpdateStatus.VERIFIED) {
android.os.RecoverySystem.installPackage(context, update.getFile());
}
}
}