From f8daccffc061e2f05f6605913c19d4aa807eaddb Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 9 Nov 2020 15:37:31 +0100 Subject: initial auto-update implementation: introducing fatweb flavor, pgpverify go library and bitmask core library, basic update mechanism --- .../appUpdate/DownloadBroadcastReceiver.java | 114 ++++++++++ .../appUpdate/DownloadConnector.java | 116 +++++++++++ .../appUpdate/DownloadNotificationManager.java | 113 ++++++++++ .../appUpdate/DownloadService.java | 82 ++++++++ .../appUpdate/DownloadServiceCommand.java | 81 ++++++++ .../appUpdate/FileProviderUtil.java | 52 +++++ .../appUpdate/UpdateDownloadManager.java | 231 +++++++++++++++++++++ 7 files changed, 789 insertions(+) create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java (limited to 'app/src/fatweb/java/se.leap.bitmaskclient/appUpdate') diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java new file mode 100644 index 00000000..6613d394 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; + +import se.leap.bitmaskclient.Constants; + +import static android.app.Activity.RESULT_CANCELED; +import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.appUpdate.DownloadConnector.APP_TYPE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_PROGRESS; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_NEW_VERISON; +import static se.leap.bitmaskclient.appUpdate.DownloadService.PROGRESS_VALUE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOADED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_NOT_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; +import static se.leap.bitmaskclient.appUpdate.FileProviderUtil.getUriFor; + +public class DownloadBroadcastReceiver extends BroadcastReceiver { + + public static final String ACTION_DOWNLOAD = "se.leap.bitmaskclient.appUpdate.ACTION_DOWNLOAD"; + private static final String TAG = DownloadBroadcastReceiver.class.getSimpleName(); + + private DownloadNotificationManager notificationManager; + + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "DOWNLOAD ON RECEIVE!"); + String action = intent.getAction(); + if (action == null) { + return; + } + + if (notificationManager == null) { + notificationManager = new DownloadNotificationManager(context.getApplicationContext()); + } + + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY); + + switch (action) { + case BROADCAST_DOWNLOAD_SERVICE_EVENT: + switch (resultCode) { + case UPDATE_FOUND: + notificationManager.buildDownloadFoundNotification(); + break; + case UPDATE_NOT_FOUND: + if (resultData.getBoolean(NO_NEW_VERISON, false)) { + //TODO: Save in preferences date, retry in a week + } else if (resultData.getBoolean(DOWNLOAD_FAILED, false)) { + Toast.makeText(context.getApplicationContext(), "Update check failed.", Toast.LENGTH_LONG).show(); + } + break; + case UPDATE_DOWNLOADED: + notificationManager.cancelNotifications(); + Intent installIntent = new Intent(Intent.ACTION_VIEW); + File update = UpdateDownloadManager.getUpdateFile(context); + if (update.exists()) { + installIntent.setDataAndType(getUriFor(context, update), APP_TYPE); + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + context.startActivity(installIntent); + break; + case UPDATE_DOWNLOAD_FAILED: + notificationManager.cancelNotifications(); + Toast.makeText(context.getApplicationContext(), "Update download failed.", Toast.LENGTH_LONG).show(); + break; + case DOWNLOAD_PROGRESS: + int progress = resultData.getInt(PROGRESS_VALUE, 0); + notificationManager.buildDownloadUpdateProgress(progress); + break; + } + break; + + case ACTION_DOWNLOAD: + DownloadServiceCommand.execute(context.getApplicationContext(), DOWNLOAD_UPDATE); + break; + + default: + break; + } + + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java new file mode 100644 index 00000000..9427083d --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + + +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.Scanner; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.BufferedSource; +import okio.Okio; + + +/** + * This class encapsulates HTTP requests so that the results can be mocked + * and it's owning UpdateDownloadManager class logic can be unit tested properly + * + */ +public class DownloadConnector { + + private static final String TAG = DownloadConnector.class.getSimpleName(); + final static String APP_TYPE = "application/vnd.android.package-archive"; + final static String TEXT_FILE_TYPE = "application/text"; + + public interface DownloadProgress { + void onUpdate(int progress); + } + + static String requestTextFileFromServer(@NonNull String url, @NonNull OkHttpClient okHttpClient) { + try { + Request request = new Request.Builder() + .url(url) + .addHeader("Content-Type", TEXT_FILE_TYPE) + .build(); + + Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + return null; + } + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + return scanner.next(); + } + return null; + + } catch (Exception e) { + Log.d(TAG, "Text file download failed"); + } + + return null; + } + + static File requestFileFromServer(@NonNull String url, @NonNull OkHttpClient okHttpClient, File destFile, DownloadProgress callback) { + BufferedSink sink = null; + BufferedSource source = null; + try { + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .addHeader("Content-Type", APP_TYPE); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + ResponseBody body = response.body(); + InputStream in = body.byteStream(); + long contentLength = body.contentLength(); + source = body.source(); + sink = Okio.buffer(Okio.sink(destFile)); + Buffer sinkBuffer = sink.buffer(); + long totalBytesRead = 0; + int bufferSize = 8 * 1024; + long bytesRead; + while ((bytesRead = source.read(sinkBuffer, bufferSize)) != -1) { + sink.emit(); + totalBytesRead += bytesRead; + int progress = (int) ((totalBytesRead * 100) / contentLength); + callback.onUpdate(progress); + } + sink.flush(); + + return destFile; + + } catch (Exception e) { + Log.d(TAG, "File download failed"); + } + + return null; + } + +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java new file mode 100644 index 00000000..4f7f2883 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import se.leap.bitmaskclient.R; + +import static android.content.Intent.CATEGORY_DEFAULT; +import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD; + +public class DownloadNotificationManager { + private Context context; + private final static int DOWNLOAD_NOTIFICATION_ID = 1; + + public DownloadNotificationManager(@NonNull Context context) { + this.context = context; + } + + public void buildDownloadFoundNotification() { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(notificationManager); + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID); + notificationBuilder.setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL) + .setWhen(System.currentTimeMillis()) + .setSmallIcon(R.mipmap.ic_launcher) + .setTicker(context.getString(R.string.version_update_title, context.getString(R.string.app_name))) + .setContentTitle(context.getString(R.string.version_update_title, context.getString(R.string.app_name))) + .setContentText(context.getString(R.string.version_update_found)) + .setContentIntent(getDownloadIntent()); + notificationManager.notify(DOWNLOAD_NOTIFICATION_ID, notificationBuilder.build()); + } + + @TargetApi(26) + private void createNotificationChannel(NotificationManager notificationManager) { + CharSequence name = "Bitmask Updates"; + String description = "Informs about available updates"; + NotificationChannel channel = new NotificationChannel(DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + name, + NotificationManager.IMPORTANCE_LOW); + channel.setSound(null, null); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(channel); + } + + + + private PendingIntent getDownloadIntent() { + Intent downloadIntent = new Intent(context, DownloadBroadcastReceiver.class); + downloadIntent.setAction(ACTION_DOWNLOAD); + return PendingIntent.getBroadcast(context, 0, downloadIntent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + public void buildDownloadUpdateProgress(int progress) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(notificationManager); + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID); + notificationBuilder.setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL) + .setAutoCancel(false) + .setOngoing(true) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(context.getString(R.string.version_update_apk_description, context.getString(R.string.app_name))) + .setProgress(100, progress, false) + .setContentIntent(getDownloadIntent()); + notificationManager.notify(DOWNLOAD_NOTIFICATION_ID, notificationBuilder.build()); + } + + public void cancelNotifications() { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + notificationManager.cancel(DOWNLOAD_NOTIFICATION_ID); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java new file mode 100644 index 00000000..bc9adfc1 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import se.leap.bitmaskclient.OkHttpClientGenerator; + +public class DownloadService extends JobIntentService implements UpdateDownloadManager.DownloadServiceCallback { + + static final int JOB_ID = 161376; + static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "bitmask_download_service_news"; + + final public static String TAG = DownloadService.class.getSimpleName(), + PROGRESS_VALUE = "progressValue", + NO_NEW_VERISON = "noNewVersion", + DOWNLOAD_FAILED = "downloadFailed", + NO_PUB_KEY = "noPubKey", + VERIFICATION_ERROR = "verificationError"; + + final public static int + UPDATE_DOWNLOADED = 1, + UPDATE_DOWNLOAD_FAILED = 2, + UPDATE_FOUND = 3, + UPDATE_NOT_FOUND = 4, + DOWNLOAD_PROGRESS = 6; + + + private UpdateDownloadManager updateDownloadManager; + + + @Override + public void onCreate() { + super.onCreate(); + updateDownloadManager = initDownloadManager(); + } + + @Override + protected void onHandleWork(@NonNull Intent intent) { + updateDownloadManager.handleIntent(intent); + } + + /** + * Convenience method for enqueuing work in to this service. + */ + static void enqueueWork(Context context, Intent work) { + try { + DownloadService.enqueueWork(context, DownloadService.class, JOB_ID, work); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + private UpdateDownloadManager initDownloadManager() { + OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(null); + return new UpdateDownloadManager(this, clientGenerator, this); + } + + @Override + public void broadcastEvent(Intent intent) { + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java new file mode 100644 index 00000000..c4e809f2 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; +import android.os.ResultReceiver; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import se.leap.bitmaskclient.ProviderAPI; + +public class DownloadServiceCommand { + + public final static String + CHECK_VERSION_FILE = "checkVersionFile", + DOWNLOAD_UPDATE = "downloadUpdate"; + + private Context context; + private String action; + private ResultReceiver resultReceiver; + + private DownloadServiceCommand(@NotNull Context context, @NotNull String action) { + this(context.getApplicationContext(), action, null); + } + + private DownloadServiceCommand(@NotNull Context context, @NotNull String action, @Nullable ResultReceiver resultReceiver) { + super(); + this.context = context; + this.action = action; + this.resultReceiver = resultReceiver; + } + + + private Intent setUpIntent() { + Intent command = new Intent(context, ProviderAPI.class); + command.setAction(action); + if (resultReceiver != null) { + command.putExtra(ProviderAPI.RECEIVER_KEY, resultReceiver); + } + return command; + } + + private boolean isInitialized() { + return context != null; + } + + + private void execute() { + if (isInitialized()) { + Intent intent = setUpIntent(); + DownloadService.enqueueWork(context, intent); + } + } + + public static void execute(Context context, String action) { + DownloadServiceCommand command = new DownloadServiceCommand(context, action); + command.execute(); + } + + public static void execute(Context context, String action, ResultReceiver resultReceiver) { + DownloadServiceCommand command = new DownloadServiceCommand(context, action, resultReceiver); + command.execute(); + } + +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java new file mode 100644 index 00000000..756a3b99 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +import java.io.File; + +import se.leap.bitmaskclient.BuildConfig; + +/** + * From Signal + */ + +public class FileProviderUtil { + + private static final String AUTHORITY = BuildConfig.APPLICATION_ID +".fileprovider"; + + public static Uri getUriFor(@NonNull Context context, @NonNull File file) { + if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file); + else return Uri.fromFile(file); + } + + public static boolean isAuthority(@NonNull Uri uri) { + return AUTHORITY.equals(uri.getAuthority()); + } + + public static boolean delete(@NonNull Context context, @NonNull Uri uri) { + if (AUTHORITY.equals(uri.getAuthority())) { + return context.getContentResolver().delete(uri, null, null) > 0; + } + return new File(uri.getPath()).delete(); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java new file mode 100644 index 00000000..698a0d17 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.File; + +import okhttp3.OkHttpClient; +import pgpverify.Logger; +import pgpverify.PgpVerifier; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.OkHttpClientGenerator; +import se.leap.bitmaskclient.R; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_PROGRESS; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_NEW_VERISON; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_PUB_KEY; +import static se.leap.bitmaskclient.appUpdate.DownloadService.PROGRESS_VALUE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOADED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_NOT_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.VERIFICATION_ERROR; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; +import static se.leap.bitmaskclient.utils.FileHelper.readPublicKey; + +public class UpdateDownloadManager implements Logger, DownloadConnector.DownloadProgress { + + + private static final String TAG = UpdateDownloadManager.class.getSimpleName(); + + public interface DownloadServiceCallback { + void broadcastEvent(Intent intent); + } + + private Context context; + + private PgpVerifier pgpVerifier; + private DownloadServiceCallback serviceCallback; + OkHttpClientGenerator clientGenerator; + + + public UpdateDownloadManager(Context context, OkHttpClientGenerator clientGenerator, DownloadServiceCallback callback) { + this.context = context; + this.clientGenerator = clientGenerator; + pgpVerifier = new PgpVerifier(); + pgpVerifier.setLogger(this); + serviceCallback = callback; + } + + //pgpverify Logger interface + @Override + public void log(String s) { + + } + + @Override + public void onUpdate(int progress) { + Bundle resultData = new Bundle(); + resultData.putInt(PROGRESS_VALUE, progress); + broadcastEvent(DOWNLOAD_PROGRESS, resultData); + } + + public void handleIntent(Intent command) { + ResultReceiver receiver = null; + if (command.getParcelableExtra(RECEIVER_KEY) != null) { + receiver = command.getParcelableExtra(RECEIVER_KEY); + } + String action = command.getAction(); + + Bundle result = new Bundle(); + switch (action) { + case CHECK_VERSION_FILE: + result = checkVersionFile(result); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, UPDATE_FOUND, result); + } else { + sendToReceiverOrBroadcast(receiver, UPDATE_NOT_FOUND, result); + } + break; + case DOWNLOAD_UPDATE: + result = downloadUpdate(result); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, UPDATE_DOWNLOADED, result); + } else { + sendToReceiverOrBroadcast(receiver, UPDATE_DOWNLOAD_FAILED, result); + } + break; + + } + } + + public static File getUpdateFile(Context context) { + return new File(context.getExternalFilesDir(null) + "/" + context.getString(R.string.app_name) + "_update.apk"); + } + + private Bundle downloadUpdate(Bundle task) { + + String publicKey = readPublicKey(context); + if (isEmpty(publicKey)) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(NO_PUB_KEY, true); + return task; + } + + OkHttpClient client = clientGenerator.init(); + String signature = DownloadConnector.requestTextFileFromServer(BuildConfig.signature_url, client); + if (signature == null) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + return task; + } + + File destinationFile = getUpdateFile(context); + if (destinationFile.exists()) { + destinationFile.delete(); + } + + destinationFile = DownloadConnector.requestFileFromServer(BuildConfig.update_apk_url, client, destinationFile, this); + + if (destinationFile == null) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + return task; + } + + boolean successfulVerified = pgpVerifier.verify(signature, publicKey, destinationFile.getAbsolutePath()); + if (!successfulVerified) { + destinationFile.delete(); + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(VERIFICATION_ERROR, true); + return task; + } + + task.putBoolean(BROADCAST_RESULT_KEY, true); + return task; + } + + private static void clearPreviousDownloads(@NonNull Context context, String destinationFile) { + File directory = context.getExternalFilesDir(null); + + if (directory == null) { + Log.w(TAG, "Failed to read external files directory."); + return; + } + + for (File file : directory.listFiles()) { + if (file.getName().equals(destinationFile)) { + if (file.delete()) { + Log.d(TAG, "Deleted " + file.getName()); + } + } + } + } + + private Bundle checkVersionFile(Bundle task) { + OkHttpClient client = clientGenerator.init(); + String versionString = DownloadConnector.requestTextFileFromServer(BuildConfig.version_file_url, client); + + if (versionString != null) { + versionString = versionString.replace("\n", "").trim(); + } + + int version = -1; + try { + version = Integer.valueOf(versionString); + } catch (NumberFormatException e) { + e.printStackTrace(); + Log.e(TAG, "could not parse version code: " + versionString); + } + + if (version == -1) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + } else if (BuildConfig.VERSION_CODE >= version) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(NO_NEW_VERISON, true); + } else { + task.putBoolean(BROADCAST_RESULT_KEY, true); + } + return task; + } + + private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData) { + if (resultData == null || resultData == Bundle.EMPTY) { + resultData = new Bundle(); + } + if (receiver != null) { + receiver.send(resultCode, resultData); + } else { + broadcastEvent(resultCode, resultData); + } + } + + private void broadcastEvent(int resultCode , Bundle resultData) { + Intent intentUpdate = new Intent(BROADCAST_DOWNLOAD_SERVICE_EVENT); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); + intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); + serviceCallback.broadcastEvent(intentUpdate); + } + +} -- cgit v1.2.3