summaryrefslogtreecommitdiff
path: root/app/src/fatweb
diff options
context:
space:
mode:
authorcyBerta <cyberta@riseup.net>2020-11-09 15:37:31 +0100
committercyBerta <cyberta@riseup.net>2020-11-09 15:37:31 +0100
commitf8daccffc061e2f05f6605913c19d4aa807eaddb (patch)
treea9789cd103807debb302d838dc5aef81fc2a4bb6 /app/src/fatweb
parent9510a267ac90d74fc47977958a67b4e0bd0b5708 (diff)
initial auto-update implementation: introducing fatweb flavor, pgpverify go library and bitmask core library, basic update mechanism
Diffstat (limited to 'app/src/fatweb')
-rw-r--r--app/src/fatweb/AndroidManifest.xml62
-rw-r--r--app/src/fatweb/assets/public.pgp458
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java114
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java116
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java113
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java82
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java81
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java52
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java231
9 files changed, 1309 insertions, 0 deletions
diff --git a/app/src/fatweb/AndroidManifest.xml b/app/src/fatweb/AndroidManifest.xml
new file mode 100644
index 00000000..5188b05b
--- /dev/null
+++ b/app/src/fatweb/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 The Android Open Source 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="se.leap.bitmaskclient">
+
+ <!-- if you want to run test, this permissions are needed. Gradle will get rid of them once we implement it. -->
+ <!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> -->
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
+
+ <application>
+ <service
+ android:name=".appUpdate.DownloadService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
+ <!-- other intent filters are added on runtime -->
+ <receiver android:name=".appUpdate.DownloadBroadcastReceiver" android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_INSTALL"/>
+ <data android:scheme="package"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_INSTALL"/>
+ <data android:scheme="package"/>
+ </intent-filter>
+ </receiver>
+
+ <provider android:name="androidx.core.content.FileProvider"
+ android:authorities="${applicationId}.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+
+ <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths" />
+
+ </provider>
+ </application>
+
+
+
+
+
+</manifest>
diff --git a/app/src/fatweb/assets/public.pgp b/app/src/fatweb/assets/public.pgp
new file mode 100644
index 00000000..6964df42
--- /dev/null
+++ b/app/src/fatweb/assets/public.pgp
@@ -0,0 +1,458 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+=9gOD
+-----END PGP PUBLIC KEY BLOCK-----
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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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);
+ }
+
+}