summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle71
-rw-r--r--app/src/fatweb/AndroidManifest.xml61
-rw-r--r--app/src/fatweb/assets/public.pgp458
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java102
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java120
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java145
-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.java37
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/InstallActivity.java83
-rw-r--r--app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java211
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java38
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java16
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Constants.java20
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java50
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/StartActivity.java12
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java24
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java28
-rw-r--r--app/src/main/res/values/strings.xml10
-rw-r--r--app/src/main/res/values/themes.xml2
-rw-r--r--app/src/main/res/xml/file_provider_paths.xml12
-rw-r--r--app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java18
-rw-r--r--app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java19
28 files changed, 1731 insertions, 31 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 7134e543..0b3403ff 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -27,8 +27,6 @@ android {
//This is the default donation URL and should be set to the donation page of LEAP
// and this should not be set/altered anywhere else.
buildConfigField 'String', 'default_donation_url', '"https://leap.se/en/about-us/donate"'
- //This is the donation URL and should be set to the relevant donation page.
- buildConfigField 'String', 'donation_url', 'null'
//The field to enable donations in the app.
buildConfigField 'boolean', 'enable_donation', 'true'
//The field to enable donation reminder popup in the app if enable_donation is set to 'false' this will be disabled.
@@ -37,7 +35,16 @@ android {
buildConfigField 'int', 'donation_reminder_duration', '30'
//skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead
buildConfigField 'boolean', 'priotize_anonymous_usage', 'false'
+
+ // static update url pointing to the latest stable release apk
+ buildConfigField "String", "update_apk_url", '"https://dl.bitmask.net/client/android/Bitmask-Android-latest.apk"'
+ // the the pgp signature file of the apk
+ buildConfigField "String", "signature_url", '"https://dl.bitmask.net/client/android/Bitmask-Android-latest.apk.sig"'
+ // the version file contains the versionCode of the latest release
+ buildConfigField "String", "version_file_url", '"https://dl.bitmask.net/client/android/versioncode.txt"'
+
//ignore the following configs, only used in custom flavor
+ buildConfigField 'String', 'donation_url', 'null'
buildConfigField "String", "customProviderUrl", '""'
buildConfigField "String", "customProviderIp", '""'
buildConfigField "String", "customProviderApiIp", '""'
@@ -108,6 +115,17 @@ android {
//skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead
buildConfigField 'boolean', 'priotize_anonymous_usage', 'true'
+ //Build Config Fields for automatic apk update checks
+
+ // static update url pointing to the latest stable release apk
+ def apkURL = '"https://dl.bitmask.net/RiseupVPN/android/RiseupVPN-Android-latest.apk"'
+ buildConfigField "String", "update_apk_url", apkURL
+ // the the pgp signature file of the apk
+ def signatureURL = '"https://dl.bitmask.net/RiseupVPN/android/RiseupVPN-Android-latest.apk.sig"'
+ buildConfigField "String", "signature_url", signatureURL
+ // the version file should contain a single line with the versionCode of the latest release
+ buildConfigField "String", "version_file_url", '"https://dl.bitmask.net/client/android/versioncode.txt"' //'"https://dl.bitmask.net/RiseupVPN/android/versioncode.txt"'
+
//Build Config Fields for default donation details
//This is the donation URL and should be set to the relevant donation page.
@@ -130,6 +148,13 @@ android {
}
+ fatweb {
+ dimension "abi"
+ ext {
+ abiVersionCode = 0
+ abiFilter = ""
+ }
+ }
fat {
dimension "abi"
@@ -232,6 +257,31 @@ android {
androidTest {
java.srcDirs += ['src/sharedTest/java']
}
+
+ fatweb {
+ java.srcDirs += ['src/fatweb/java']
+ }
+
+ fat {
+ java.srcDirs += ['src/notFatweb/java']
+ }
+
+ x86 {
+ java.srcDirs += ['src/notFatweb/java']
+ }
+
+ x86_64 {
+ java.srcDirs += ['src/notFatweb/java']
+ }
+
+ armv7 {
+ java.srcDirs += ['src/notFatweb/java']
+ }
+
+ arm64 {
+ java.srcDirs += ['src/notFatweb/java']
+ }
+
}
/**
@@ -241,6 +291,8 @@ android {
* --------------------
* customProductionFatDebug -> branded development build, includes all ABIs
* normalProductionFatDebug -> Bitmask development build, includes all ABIS
+ * customProductionFatwebDebug -> branded development build, includes all ABIs, for distribution through a download page
+ * normalProductionFatWebDebug -> Bitmask development build, includes all ABIS, for distribution through a download page
* customInsecureFatDebug -> branded development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs
* normalInsecureFatDebug -> Bitmask development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs
*
@@ -248,6 +300,7 @@ android {
* -----------------
* customProductionFatBeta -> branded build, includes all ABI's, Beta release
* customProductionFatRelease -> branded build, includes all ABI's, stable release (-> F-Droid, GPlay if not splitApk is set to true)
+ * customProductionFatwebRelease -> branded build, includes all ABI's, stable release (-> F-Droid, GPlay if not splitApk is set to true), for distribution through a download page
*
* Bitmask Beta releases:
* ----------------------
@@ -263,7 +316,8 @@ android {
* normalProductionArmv7Release -> Bitmask build, only for ABI armeabi-v7a, for GPlay releases with split apks (2 of 4)
* normalProductionX86Release -> Bitmask build, only for ABI x86, for GPlay releases with split apks (3 of 4)
* normalProductionX86_64Release -> Bitmask build, only for ABI x86 64 bit, for GPlay releases with split apks (4 of 4)
- * normalProductionFatRelease -> Bitmask build, including all ABIS, for official F-Droid repo and stable link on download page
+ * normalProductionFatRelease -> Bitmask build, including all ABIS, for official F-Droid repo
+ * normalProductionFatWebRelease -> Bitmask build, including all ABIS, for distribution through a download page
*/
variantFilter { variant ->
@@ -276,7 +330,8 @@ android {
if (((names.contains("insecure") && !names.contains("fat")) ||
(names.contains("insecure") && buildTypeName.contains("beta")) ||
(names.contains("insecure") && buildTypeName.contains("release")) ||
- (buildTypeName.contains("debug") && !names.contains("fat")) ||
+ (buildTypeName.contains("debug") && !(names.contains("fatweb") || names.contains("fat"))) ||
+ (names.contains("fatweb") && buildTypeName.contains("beta")) ||
(!supportsSplitApk && !names.contains("fat")))
) {
// Gradle ignores any variants that satisfy the conditions above.
@@ -330,7 +385,13 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
- implementation project(path: ':shapeshifter')
+ //implementation project(path: ':shapeshifter')
+ fatwebImplementation project(path: ':bitmask-web-core')
+ fatImplementation project(path: ':bitmask-core')
+ x86Implementation project(path: ':bitmask-core')
+ x86_64Implementation project(path: ':bitmask-core')
+ armv7Implementation project(path: ':bitmask-core')
+ arm64Implementation project(path: ':bitmask-core')
}
android.applicationVariants.all { variant ->
diff --git a/app/src/fatweb/AndroidManifest.xml b/app/src/fatweb/AndroidManifest.xml
new file mode 100644
index 00000000..28246e5f
--- /dev/null
+++ b/app/src/fatweb/AndroidManifest.xml
@@ -0,0 +1,61 @@
+<?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>
+ </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>
+ <activity
+ android:name=".appUpdate.InstallActivity"
+ android:theme="@style/invisibleTheme" />
+ </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..a4acc2aa
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java
@@ -0,0 +1,102 @@
+/**
+ * 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.os.Bundle;
+import android.widget.Toast;
+
+import se.leap.bitmaskclient.Constants;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.utils.PreferenceHelper;
+
+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.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.DownloadService.VERIFICATION_ERROR;
+import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE;
+
+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) {
+ 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)) {
+ PreferenceHelper.setLastAppUpdateCheck(context.getApplicationContext());
+ }
+ break;
+ case UPDATE_DOWNLOADED:
+ notificationManager.buildDownloadSuccessfulNotification();
+ break;
+ case UPDATE_DOWNLOAD_FAILED:
+ if (resultData.getBoolean(VERIFICATION_ERROR, false)) {
+ Toast.makeText(context.getApplicationContext(), context.getString(R.string.version_update_error_pgp_verification), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(context.getApplicationContext(), context.getString(R.string.version_update_error), Toast.LENGTH_LONG).show();
+ }
+ notificationManager.cancelNotifications();
+ 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..8723f515
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java
@@ -0,0 +1,120 @@
+/**
+ * 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();
+ public final static String APP_TYPE = "application/vnd.android.package-archive";
+ public 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;
+ BufferedSource source;
+ 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();
+ 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;
+ int lastProgress = 0;
+ while ((bytesRead = source.read(sinkBuffer, bufferSize)) != -1) {
+ sink.emit();
+ totalBytesRead += bytesRead;
+ int progress = (int) ((totalBytesRead * 100) / contentLength);
+ // debouncing callbacks
+ if (lastProgress < progress) {
+ lastProgress = progress;
+ 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..aaf487aa
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java
@@ -0,0 +1,145 @@
+/**
+ * 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 = initNotificationManager();
+ if (notificationManager == null) {
+ return;
+ }
+ NotificationCompat.Builder notificationBuilder = initNotificationBuilderDefaults();
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_about_36)
+ .setWhen(System.currentTimeMillis())
+ .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());
+ }
+
+ public void buildDownloadSuccessfulNotification() {
+ NotificationManager notificationManager = initNotificationManager();
+ if (notificationManager == null) {
+ return;
+ }
+ NotificationCompat.Builder notificationBuilder = initNotificationBuilderDefaults();
+ notificationBuilder
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
+ .setWhen(System.currentTimeMillis())
+ .setTicker(context.getString(R.string.version_update_title, context.getString(R.string.app_name)))
+ .setContentTitle(context.getString(R.string.version_update_download_title, context.getString(R.string.app_name)))
+ .setContentText(context.getString(R.string.version_update_download_description))
+ .setContentIntent(getInstallIntent());
+ notificationManager.notify(DOWNLOAD_NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ public void buildDownloadUpdateProgress(int progress) {
+ NotificationManager notificationManager = initNotificationManager();
+ if (notificationManager == null) {
+ return;
+ }
+
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID);
+ notificationBuilder
+ .setDefaults(Notification.DEFAULT_ALL)
+ .setAutoCancel(false)
+ .setOngoing(true)
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .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());
+ }
+
+ private NotificationManager initNotificationManager() {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ return null;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel(notificationManager);
+ }
+ return notificationManager;
+ }
+
+ @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 NotificationCompat.Builder initNotificationBuilderDefaults() {
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID);
+ notificationBuilder.
+ setDefaults(Notification.DEFAULT_ALL).
+ setAutoCancel(true);
+ return notificationBuilder;
+ }
+
+ private PendingIntent getDownloadIntent() {
+ Intent downloadIntent = new Intent(context, DownloadBroadcastReceiver.class);
+ downloadIntent.setAction(ACTION_DOWNLOAD);
+ return PendingIntent.getBroadcast(context, 0, downloadIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ private PendingIntent getInstallIntent() {
+ Intent installIntent = new Intent(context, InstallActivity.class);
+ return PendingIntent.getActivity(context, 0, installIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ 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..4966a863
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java
@@ -0,0 +1,37 @@
+/**
+ * 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;
+
+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);
+ }
+}
diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/InstallActivity.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/InstallActivity.java
new file mode 100644
index 00000000..6629425c
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/InstallActivity.java
@@ -0,0 +1,83 @@
+/**
+ * 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.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+
+import java.io.File;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.utils.PreferenceHelper;
+
+import static se.leap.bitmaskclient.Constants.REQUEST_CODE_REQUEST_UPDATE;
+import static se.leap.bitmaskclient.appUpdate.DownloadConnector.APP_TYPE;
+import static se.leap.bitmaskclient.appUpdate.FileProviderUtil.getUriFor;
+
+public class InstallActivity extends Activity {
+
+ private static final String TAG = InstallActivity.class.getSimpleName();
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestPermissionAndInstall();
+ }
+
+ private void requestPermissionAndInstall() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !this.getPackageManager().canRequestPackageInstalls()) {
+ startActivityForResult(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:"+getPackageName())),
+ REQUEST_CODE_REQUEST_UPDATE);
+ } else {
+ installUpdate();
+ }
+ }
+
+ protected void installUpdate() {
+ PreferenceHelper.restartOnUpdate(this.getApplicationContext(), true);
+
+ Intent installIntent = new Intent(Intent.ACTION_VIEW);
+ File update = UpdateDownloadManager.getUpdateFile(this.getApplicationContext());
+ if (update.exists()) {
+ installIntent.setDataAndType(getUriFor(this.getApplicationContext(), update), APP_TYPE);
+ installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ this.startActivity(installIntent);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_REQUEST_UPDATE) {
+ if (resultCode == RESULT_OK) {
+ installUpdate();
+ } else {
+ Toast.makeText(this, getString(R.string.version_update_error_permissions), Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+ }
+}
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..b79c2a91
--- /dev/null
+++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java
@@ -0,0 +1,211 @@
+/**
+ * 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 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 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.parseInt(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);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java
index bde5114b..437998e0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java
@@ -1,16 +1,42 @@
+/**
+ * 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;
import android.content.Context;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
-import androidx.multidex.MultiDexApplication;
+
import androidx.appcompat.app.AppCompatDelegate;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.multidex.MultiDexApplication;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
+import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver;
import se.leap.bitmaskclient.tethering.TetheringStateManager;
+import static android.content.Intent.CATEGORY_DEFAULT;
+import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD;
+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.PreferenceHelper.getSavedProviderFromSharedPreferences;
/**
@@ -22,6 +48,7 @@ public class BitmaskApp extends MultiDexApplication {
private final static String TAG = BitmaskApp.class.getSimpleName();
private RefWatcher refWatcher;
private ProviderObservable providerObservable;
+ private DownloadBroadcastReceiver downloadBroadcastReceiver;
@Override
@@ -41,6 +68,15 @@ public class BitmaskApp extends MultiDexApplication {
EipSetupObserver.init(this, preferences);
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
TetheringStateManager.getInstance().init(this);
+ if (BuildConfig.FLAVOR.contains("Fatweb")) {
+ downloadBroadcastReceiver = new DownloadBroadcastReceiver();
+ IntentFilter intentFilter = new IntentFilter(BROADCAST_DOWNLOAD_SERVICE_EVENT);
+ intentFilter.addAction(ACTION_DOWNLOAD);
+ intentFilter.addAction(CHECK_VERSION_FILE);
+ intentFilter.addAction(DOWNLOAD_UPDATE);
+ intentFilter.addCategory(CATEGORY_DEFAULT);
+ LocalBroadcastManager.getInstance(this.getApplicationContext()).registerReceiver(downloadBroadcastReceiver, intentFilter);
+ }
}
/**
diff --git a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java b/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java
index 0ef77e2b..4f27f88a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java
@@ -1,3 +1,19 @@
+/**
+ * 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;
import androidx.appcompat.app.AppCompatActivity;
diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java
index 6462b663..1d364074 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java
@@ -1,3 +1,19 @@
+/**
+ * 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;
import android.text.TextUtils;
@@ -20,6 +36,8 @@ public interface Constants {
String ALLOW_TETHERING_USB = "tethering_usb";
String SHOW_EXPERIMENTAL = "show_experimental";
String USE_IPv6_FIREWALL = "use_ipv6_firewall";
+ String RESTART_ON_UPDATE = "restart_on_update";
+ String LAST_UPDATE_CHECK = "last_update_check";
//////////////////////////////////////////////
@@ -31,6 +49,7 @@ public interface Constants {
int REQUEST_CODE_SWITCH_PROVIDER = 1;
int REQUEST_CODE_LOG_IN = 2;
int REQUEST_CODE_ADD_PROVIDER = 3;
+ int REQUEST_CODE_REQUEST_UPDATE = 4;
//////////////////////////////////////////////
@@ -105,6 +124,7 @@ public interface Constants {
String BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT = "BROADCAST.GATEWAY_SETUP_WATCHER_EVENT";
String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE";
String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY";
+ String BROADCAST_DOWNLOAD_SERVICE_EVENT = "BROADCAST.DOWNLOAD_SERVICE_EVENT";
//////////////////////////////////////////////
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
index 7504e0c0..e365c857 100644
--- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
@@ -1,3 +1,20 @@
+/**
+ * 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;
import android.content.BroadcastReceiver;
@@ -21,6 +38,7 @@ import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConnectionStatus;
import de.blinkt.openvpn.core.LogItem;
import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand;
import se.leap.bitmaskclient.eip.EIP;
import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.eip.EipStatus;
@@ -48,6 +66,7 @@ import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE
import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE;
/**
* Created by cyberta on 05.12.18.
@@ -58,6 +77,7 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe
//The real timout is 4*2s + 1*4s + 1*8s + 1*16s + 1*32s + 1*64s = 132 s;
private static final String TIMEOUT = "4";
+ private static final int UPDATE_CHECK_TIMEOUT = 1000*60*60*24*7;
private Context context;
private VpnProfile setupVpnProfile;
private String observedProfileFromVpnStatus;
@@ -292,12 +312,20 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe
//setupNClostestGateway > 0: at least one failed gateway -> did the provider change it's gateways?
ProviderAPICommand.execute(context, ProviderAPI.DOWNLOAD_SERVICE_JSON, provider);
}
+
+ if (shouldCheckAppUpdate()) {
+ DownloadServiceCommand.execute(context, CHECK_VERSION_FILE);
+ }
finishGatewaySetup(false);
} else if ("TCP_CONNECT".equals(state)) {
changingGateway.set(false);
}
}
+ private boolean shouldCheckAppUpdate() {
+ return System.currentTimeMillis() - PreferenceHelper.getLastAppUpdateCheck(context) >= UPDATE_CHECK_TIMEOUT;
+ }
+
private void selectNextGateway() {
changingGateway.set(true);
reconnectTry.set(0);
diff --git a/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java
index 3b67d96b..519e4fc2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java
+++ b/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java
@@ -1,7 +1,6 @@
package se.leap.bitmaskclient;
public interface FeatureVersionCode {
- int MULTIPLE_PROFILES = 132;
int RENAMED_EIP_IN_PREFERENCES = 132;
int GEOIP_SERVICE = 148;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
index 7b9874e0..576e76e0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
@@ -62,7 +62,7 @@ public class OkHttpClientGenerator {
Resources resources;
- public OkHttpClientGenerator(SharedPreferences preferences, Resources resources) {
+ public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) {
this.resources = resources;
}
@@ -74,22 +74,21 @@ public class OkHttpClientGenerator {
return initHttpClient(initError, caCert);
}
+ public OkHttpClient init() {
+ try {
+ return createClient(null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
+ if (resources == null) {
+ return null;
+ }
try {
- TLSCompatSocketFactory sslCompatFactory;
- ConnectionSpec spec = getConnectionSpec();
- OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
-
- if (!isEmpty(certificate)) {
- sslCompatFactory = new TLSCompatSocketFactory(certificate);
- } else {
- sslCompatFactory = new TLSCompatSocketFactory();
- }
- sslCompatFactory.initSSLSocketFactory(clientBuilder);
- clientBuilder.cookieJar(getCookieJar())
- .connectionSpecs(Collections.singletonList(spec));
- clientBuilder.dns(new DnsResolver());
- return clientBuilder.build();
+ return createClient(certificate);
} catch (IllegalArgumentException e) {
e.printStackTrace();
// TODO ca cert is invalid - show better error ?!
@@ -110,10 +109,31 @@ public class OkHttpClientGenerator {
} catch (IOException e) {
e.printStackTrace();
addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message));
+ } catch (Exception e) {
+ e.printStackTrace();
+ // unexpected exception, should never happen
+ // only to shorten the method signature createClient(String certificate)
}
return null;
}
+ private OkHttpClient createClient(String certificate) throws Exception {
+ TLSCompatSocketFactory sslCompatFactory;
+ ConnectionSpec spec = getConnectionSpec();
+ OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
+
+ if (!isEmpty(certificate)) {
+ sslCompatFactory = new TLSCompatSocketFactory(certificate);
+ } else {
+ sslCompatFactory = new TLSCompatSocketFactory();
+ }
+ sslCompatFactory.initSSLSocketFactory(clientBuilder);
+ clientBuilder.cookieJar(getCookieJar())
+ .connectionSpecs(Collections.singletonList(spec));
+ clientBuilder.dns(new DnsResolver());
+ return clientBuilder.build();
+ }
+
@NonNull
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
index 4058b824..bec16139 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -120,7 +120,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
private ProviderApiManager initApiManager() {
SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
- OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(preferences, getResources());
+ OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index 9937eeeb..1a679b1c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -133,9 +133,6 @@ public class StartActivity extends Activity{
* execute necessary upgrades for version change
*/
private void executeUpgrade() {
- if (hasNewFeature(FeatureVersionCode.MULTIPLE_PROFILES)) {
- // TODO prepare usage of multiple profiles
- }
if (hasNewFeature(FeatureVersionCode.RENAMED_EIP_IN_PREFERENCES)) {
String eipJson = preferences.getString(PROVIDER_KEY, null);
if (eipJson != null) {
@@ -183,9 +180,14 @@ public class StartActivity extends Activity{
if (getIntent() != null && getIntent().getBooleanExtra(EIP_RESTART_ON_BOOT, false)) {
EipCommand.startVPN(this.getApplicationContext(), true);
finish();
- return;
+ } else if (PreferenceHelper.getRestartOnUpdate(this.getApplicationContext())) {
+ PreferenceHelper.restartOnUpdate(this.getApplicationContext(), false);
+ EipCommand.startVPN(this.getApplicationContext(), false);
+ showMainActivity();
+ finish();
+ } else {
+ showMainActivity();
}
- showMainActivity();
}
} else {
configureLeapProvider();
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java
index 1ee32654..25450f56 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java
@@ -4,9 +4,9 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.ResultReceiver;
+
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
-import androidx.core.content.ContextCompat;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -38,7 +38,6 @@ public class EipCommand {
* @param resultReceiver The resultreceiver to reply to
*/
private static void execute(@NotNull Context context, @NotNull String action, @Nullable ResultReceiver resultReceiver, @Nullable Intent vpnIntent) {
- // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
if (vpnIntent == null) {
vpnIntent = new Intent();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java
index 2748c944..5a142d90 100644
--- a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java
@@ -53,6 +53,7 @@ import static se.leap.bitmaskclient.Constants.DEFAULT_BITMASK;
/**
* Stores constants, and implements auxiliary methods used across all Bitmask Android classes.
+ * Wraps BuildConfigFields for to support easier unit testing
*
* @author parmegv
* @author MeanderingCode
@@ -120,7 +121,7 @@ public class ConfigHelper {
try {
KeyFactory kf;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- kf = KeyFactory.getInstance("RSA", "BC");
+ kf = KeyFactory.getInstance("RSA", "BC");
} else {
kf = KeyFactory.getInstance("RSA");
}
@@ -201,4 +202,29 @@ public class ConfigHelper {
return (string1 == null && string2 == null) ||
(string1 != null && string1.equals(string2));
}
+
+ public static String getApkFileName() {
+ try {
+ return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getVersionFileName() {
+ try {
+ return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public static String getSignatureFileName() {
+ try {
+ return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java
index 1c3e1ebb..ebcc32ba 100644
--- a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java
@@ -1,8 +1,13 @@
package se.leap.bitmaskclient.utils;
+import android.content.Context;
+
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
/**
* Created by cyberta on 18.03.18.
@@ -19,4 +24,23 @@ public class FileHelper {
writer.close();
}
+ public static String readPublicKey(Context context) {
+ {
+ InputStream inputStream;
+ try {
+ inputStream = context.getAssets().open("public.pgp");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ reader.close();
+ return sb.toString();
+ } catch (IOException errabi) {
+ return null;
+ }
+ }
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
index cb2aeb26..5b62d0ff 100644
--- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
@@ -22,11 +22,13 @@ import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_WIFI;
import static se.leap.bitmaskclient.Constants.ALWAYS_ON_SHOW_DIALOG;
import static se.leap.bitmaskclient.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER;
import static se.leap.bitmaskclient.Constants.EXCLUDED_APPS;
+import static se.leap.bitmaskclient.Constants.LAST_UPDATE_CHECK;
import static se.leap.bitmaskclient.Constants.LAST_USED_PROFILE;
import static se.leap.bitmaskclient.Constants.PROVIDER_CONFIGURED;
import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.Constants.RESTART_ON_UPDATE;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.Constants.SHOW_EXPERIMENTAL;
import static se.leap.bitmaskclient.Constants.USE_IPv6_FIREWALL;
@@ -121,6 +123,22 @@ public class PreferenceHelper {
apply();
}
+ public static void setLastAppUpdateCheck(Context context) {
+ putLong(context, LAST_UPDATE_CHECK, System.currentTimeMillis());
+ }
+
+ public static long getLastAppUpdateCheck(Context context) {
+ return getLong(context, LAST_UPDATE_CHECK, 0);
+ }
+
+ public static void restartOnUpdate(Context context, boolean isEnabled) {
+ putBoolean(context, RESTART_ON_UPDATE, isEnabled);
+ }
+
+ public static boolean getRestartOnUpdate(Context context) {
+ return getBoolean(context, RESTART_ON_UPDATE, false);
+ }
+
public static boolean getUsePluggableTransports(Context context) {
return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false);
}
@@ -214,6 +232,16 @@ public class PreferenceHelper {
return preferences.getStringSet(EXCLUDED_APPS, new HashSet<>());
}
+ public static long getLong(Context context, String key, long defValue) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ return preferences.getLong(key, defValue);
+ }
+
+ public static void putLong(Context context, String key, long value) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ preferences.edit().putLong(key, value).apply();
+ }
+
public static String getString(Context context, String key, String defValue) {
SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
return preferences.getString(key, defValue);
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6d4c62e1..d047da21 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -142,4 +142,14 @@
<string name="root_permission_error">%s cannot execute features like VPN Hotspot or IPv6 firewall without root permissions.</string>
<string name="qs_enable_vpn">Start %s</string>
+ <string name="version_update_found">Tap here to start the download.</string>
+ <string name="version_update_title">A new %s version has been found.</string>
+ <string name="version_update_apk_description">Downloading a new %s version</string>
+ <string name="version_update_storage_access_required">Storage access is required to web update.</string>
+ <string name="version_update_storage_permission_denied">Storage permission request was denied.</string>
+ <string name="version_update_download_title">A new %s version has been downloaded.</string>
+ <string name="version_update_download_description">Tap here to install the update.</string>
+ <string name="version_update_error_pgp_verification">PGP verification error. Ignoring download.</string>
+ <string name="version_update_error">Update failed.</string>
+ <string name="version_update_error_permissions">No permissions to install app.</string>
</resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 7e98ccf4..ab489fc6 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -22,6 +22,8 @@
<style name="invisibleTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
<item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowDisablePreview">true</item>
+ <item name="android:windowFullscreen">true</item>
</style>
</resources>
diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml
new file mode 100644
index 00000000..5b9dd9fb
--- /dev/null
+++ b/app/src/main/res/xml/file_provider_paths.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+ <external-path
+ name="external"
+ path="." />
+ <external-files-path
+ name="external_files"
+ path="." />
+ <files-path
+ name="files"
+ path="." />
+</paths> \ No newline at end of file
diff --git a/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java
new file mode 100644
index 00000000..7adbb85d
--- /dev/null
+++ b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java
@@ -0,0 +1,18 @@
+package se.leap.bitmaskclient.appUpdate;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * DownloadBroadcastReceiver is only implemented in Fatweb builds
+ *
+ */
+public class DownloadBroadcastReceiver extends BroadcastReceiver {
+
+ public static final String ACTION_DOWNLOAD = "se.leap.bitmaskclient.appUpdate.ACTION_DOWNLOAD";
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ }
+}
diff --git a/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java
new file mode 100644
index 00000000..157f04fe
--- /dev/null
+++ b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java
@@ -0,0 +1,19 @@
+package se.leap.bitmaskclient.appUpdate;
+
+import android.content.Context;
+
+/**
+ * DownloadServiceCommand is only implemented in Fatweb builds
+ *
+ */
+public class DownloadServiceCommand {
+
+ public final static String
+ CHECK_VERSION_FILE = "checkVersionFile",
+ DOWNLOAD_UPDATE = "downloadUpdate";
+
+
+ public static void execute(Context context, String action) {
+ // DO NOTHING.
+ }
+}