diff --git a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
index 7516c9bc4a..733d9317f8 100644
--- a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -37,5 +37,7 @@ interface EventListener {
void onReactionClicked(DcMsg messageRecord);
void onStickerClicked(DcMsg messageRecord);
+
+ void onUpdateNowClicked(DcMsg messageRecord);
}
}
diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java
index 0f1e932854..dc4301a426 100644
--- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java
@@ -70,6 +70,7 @@
import org.thoughtcrime.securesms.reactions.ReactionsDetailsFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.relay.EditRelayActivity;
+import org.thoughtcrime.securesms.updater.AppUpdate;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
@@ -935,6 +936,9 @@ public void onItemClick(DcMsg messageRecord) {
DozeReminder.dozeReminderTapped(getContext());
} else if (StatsSending.isStatsSendingDeviceMsg(getContext(), messageRecord)) {
StatsSending.statsDeviceMsgTapped(getActivity());
+ } else if (AppUpdate.isUpdateDeviceMsg(
+ getContext(), getListAdapter().getChat(), messageRecord)) {
+ AppUpdate.updateDeviceMsgTapped(getActivity());
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) {
if (messageRecord.getParent() != null) {
// if the parent webxdc message still exists
@@ -1060,6 +1064,11 @@ public void onStickerClicked(DcMsg messageRecord) {
.setNegativeButton(R.string.cancel, null)
.show();
}
+
+ @Override
+ public void onUpdateNowClicked(DcMsg messageRecord) {
+ AppUpdate.updateDeviceMsgTapped(getActivity());
+ }
}
private class ActionModeCallback implements ActionMode.Callback {
diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java
index 6581aedd21..fae67e2d03 100644
--- a/src/main/java/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/main/java/org/thoughtcrime/securesms/ConversationItem.java
@@ -74,6 +74,7 @@
import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.updater.AppUpdate;
import org.thoughtcrime.securesms.util.Linkifier;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
@@ -519,6 +520,19 @@ private void setBodyText(DcMsg messageRecord) {
passthroughClickListener.onClick(view);
}
});
+ } else if (AppUpdate.isUpdateDeviceMsg(context, dcChat, messageRecord)) {
+ showFullButton.setVisibility(View.GONE);
+ msgActionButton.setVisibility(View.VISIBLE);
+ msgActionButton.setEnabled(true);
+ msgActionButton.setText(R.string.update_now);
+ msgActionButton.setOnClickListener(
+ view -> {
+ if (eventListener != null && batchSelected.isEmpty()) {
+ eventListener.onUpdateNowClicked(messageRecord);
+ } else {
+ passthroughClickListener.onClick(view);
+ }
+ });
} else {
msgActionButton.setVisibility(View.GONE);
showFullButton.setVisibility(View.GONE);
diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java
index b12a7b2a01..6be3f755fd 100644
--- a/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java
+++ b/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java
@@ -45,6 +45,7 @@
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.updater.AppUpdate;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -192,6 +193,7 @@ protected Void doInBackground(Context... params) {
if (DozeReminder.isEligible(context)) {
DozeReminder.addDozeReminderDeviceMsg(context);
}
+ AppUpdate.reconcile(context);
FcmReceiveService.waitForRegisterFinished();
} catch (Exception e) {
e.printStackTrace();
diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java
index 49e1024ed4..b0d2c18590 100644
--- a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java
+++ b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.connect;
import android.app.Activity;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
@@ -13,6 +14,7 @@
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import chat.delta.rpc.Rpc;
@@ -355,14 +357,35 @@ public static void sendToChat(
activity.startActivity(intent);
}
+ public static boolean canInstallApks(Activity activity) {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+ || activity.getPackageManager().canRequestPackageInstalls();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static void requestInstallApksPermission(Activity activity) {
+ activity.startActivity(
+ new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
+ .setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
+ }
+
+ public static void installApk(Activity activity, Uri apkUri) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ try {
+ startActivity(activity, intent);
+ } catch (ActivityNotFoundException e) {
+ Log.i(TAG, "no installer found", e);
+ Toast.makeText(activity, R.string.no_app_to_handle_data, Toast.LENGTH_LONG).show();
+ }
+ }
+
private static void startActivity(Activity activity, Intent intent) {
// request for permission to install apks on API 26+ if intent mimetype is an apk
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
- && "application/vnd.android.package-archive".equals(intent.getType())
- && !activity.getPackageManager().canRequestPackageInstalls()) {
- activity.startActivity(
- new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
- .setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
+ if ("application/vnd.android.package-archive".equals(intent.getType())
+ && !canInstallApks(activity)) {
+ requestInstallApksPermission(activity);
return;
}
activity.startActivity(intent);
diff --git a/src/main/java/org/thoughtcrime/securesms/updater/AppUpdate.java b/src/main/java/org/thoughtcrime/securesms/updater/AppUpdate.java
new file mode 100644
index 0000000000..9322581724
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/updater/AppUpdate.java
@@ -0,0 +1,158 @@
+package org.thoughtcrime.securesms.updater;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+import androidx.fragment.app.FragmentActivity;
+import com.b44t.messenger.DcChat;
+import com.b44t.messenger.DcContact;
+import com.b44t.messenger.DcContext;
+import com.b44t.messenger.DcMsg;
+import java.io.File;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.thoughtcrime.securesms.BuildConfig;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.connect.DcHelper;
+import org.thoughtcrime.securesms.util.Prefs;
+
+public class AppUpdate {
+ private static final String TAG = "AppUpdate";
+
+ private static final String APP_KEY = "deltachat";
+
+ static final String APK_FILENAME = "deltachat-update.apk";
+
+ private static final String MOCK_LATEST_VERSIONS_JSON =
+ "{"
+ + " \"deltachat\": {"
+ + " \"gplay\": {"
+ + " \"version_integer\": 755,"
+ + " \"version_string\": \"2.57.0\","
+ + " \"release_timestamp\": 1782499168,"
+ + " \"download_url\": \"https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk\""
+ + " }"
+ + " }"
+ + "}";
+
+ private static volatile JSONObject cachedLatestVersions;
+
+ public static class LatestVersion {
+ public final int versionInteger;
+ public final String versionString;
+ public final long releaseTimestamp;
+ public final String downloadUrl;
+
+ LatestVersion(
+ int versionInteger, String versionString, long releaseTimestamp, String downloadUrl) {
+ this.versionInteger = versionInteger;
+ this.versionString = versionString;
+ this.releaseTimestamp = releaseTimestamp;
+ this.downloadUrl = downloadUrl;
+ }
+ }
+
+ /** Mock function. */
+ public static JSONObject getLatestVersions() {
+ JSONObject cached = cachedLatestVersions;
+ if (cached != null) return cached;
+ try {
+ cached = new JSONObject(MOCK_LATEST_VERSIONS_JSON);
+ cachedLatestVersions = cached;
+ return cached;
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public static LatestVersion getLatestForThisBuild() {
+ JSONObject all = getLatestVersions();
+ if (all == null) return null;
+ JSONObject app = all.optJSONObject(APP_KEY);
+ if (app == null) return null;
+ JSONObject flavor = app.optJSONObject(BuildConfig.FLAVOR);
+ if (flavor == null) return null;
+ try {
+ return new LatestVersion(
+ flavor.getInt("version_integer"),
+ flavor.getString("version_string"),
+ flavor.getLong("release_timestamp"),
+ flavor.getString("download_url"));
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ private static int getTargetVersion() {
+ LatestVersion latest = getLatestForThisBuild();
+ if (latest == null || latest.versionInteger <= BuildConfig.VERSION_CODE) return 0;
+ return latest.versionInteger;
+ }
+
+ public static synchronized void reconcile(Context context) {
+ try {
+ int target = getTargetVersion();
+ if (Prefs.getUpdateMsgVersion(context) == target) return;
+
+ deleteStoredMsg(context);
+
+ if (target == 0) {
+ deleteDownloadedApk(context);
+ Prefs.setUpdateDeviceMsg(context, 0, 0, 0);
+ return;
+ }
+
+ DcContext dcContext = DcHelper.getContext(context);
+ DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
+ msg.setText(context.getString(R.string.update_available_msg));
+ int msgId = dcContext.addDeviceMsg(null, msg);
+ Prefs.setUpdateDeviceMsg(context, target, dcContext.getAccountId(), msgId);
+ } catch (Exception e) {
+ Log.e(TAG, "reconcile() failed", e);
+ }
+ }
+
+ private static void deleteStoredMsg(Context context) {
+ int msgId = Prefs.getUpdateMsgId(context);
+ if (msgId == 0) return;
+ try {
+ DcContext dcContext =
+ DcHelper.getAccounts(context).getAccount(Prefs.getUpdateMsgAccountId(context));
+ if (!dcContext.isOk()) return;
+ DcMsg msg = dcContext.getMsg(msgId);
+ if (msg.isOk() && msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE) {
+ dcContext.deleteMsgs(new int[] {msgId});
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "could not delete update device message", e);
+ }
+ }
+
+ static File getApkDir(Context context) {
+ return context.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ }
+
+ private static void deleteDownloadedApk(Context context) {
+ File dir = getApkDir(context);
+ if (dir == null) return;
+ File apk = new File(dir, APK_FILENAME);
+ if (apk.exists() && !apk.delete()) {
+ Log.w(TAG, "could not delete " + apk);
+ }
+ }
+
+ public static boolean isUpdateDeviceMsg(Context context, DcChat dcChat, DcMsg msg) {
+ if (msg == null || dcChat == null) return false;
+ int msgId = Prefs.getUpdateMsgId(context);
+ if (msgId == 0) return false;
+ if (Prefs.getUpdateMsgVersion(context) <= BuildConfig.VERSION_CODE) return false;
+ if (dcChat.getAccountId() != Prefs.getUpdateMsgAccountId(context)) return false;
+ return msg.getId() == msgId && msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE;
+ }
+
+ public static void updateDeviceMsgTapped(FragmentActivity activity) {
+ LatestVersion latest = getLatestForThisBuild();
+ if (latest == null || activity == null) return;
+ AppUpdateDialogFragment.show(activity, latest);
+ }
+}
diff --git a/src/main/java/org/thoughtcrime/securesms/updater/AppUpdateDialogFragment.java b/src/main/java/org/thoughtcrime/securesms/updater/AppUpdateDialogFragment.java
new file mode 100644
index 0000000000..73c0dd2c8f
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/updater/AppUpdateDialogFragment.java
@@ -0,0 +1,369 @@
+package org.thoughtcrime.securesms.updater;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import java.io.File;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.connect.DcHelper;
+
+public class AppUpdateDialogFragment extends DialogFragment {
+ private static final String TAG = "AppUpdateDialog";
+
+ private static final String ARG_URL = "url";
+ private static final String ARG_VERSION = "version";
+ private static final String KEY_STATE = "state";
+ private static final String KEY_DOWNLOAD_ID = "download_id";
+ private static final String KEY_ERROR_RES = "error_res";
+
+ private enum State {
+ CHECK_PERMISSION,
+ PERMISSION_NEEDED,
+ DOWNLOADING,
+ ERROR
+ }
+
+ private static final int POLL_INTERVAL_MS = 500;
+ private static final int MAX_POLL_FAILURES = 3;
+ private static final String DEST_FILENAME = "deltachat-update.apk";
+
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ private DownloadManager downloadManager;
+ private String versionString;
+
+ private State state = State.CHECK_PERMISSION;
+ private long downloadId = -1;
+ private int pollFailures;
+ private @StringRes int errorRes;
+
+ private TextView messageText;
+ private View progressContainer;
+ private ProgressBar progressBar;
+ private TextView progressText;
+
+ private Runnable pollRunnable;
+
+ public static void show(FragmentActivity activity, AppUpdate.LatestVersion latest) {
+ AppUpdateDialogFragment f = new AppUpdateDialogFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_URL, latest.downloadUrl);
+ args.putString(ARG_VERSION, latest.versionString);
+ f.setArguments(args);
+ f.show(activity.getSupportFragmentManager(), "app_update_dialog");
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ downloadManager =
+ (DownloadManager)
+ requireContext().getApplicationContext().getSystemService(Context.DOWNLOAD_SERVICE);
+ versionString = requireArguments().getString(ARG_VERSION, "");
+
+ if (savedInstanceState != null) {
+ state = stateFromName(savedInstanceState.getString(KEY_STATE));
+ downloadId = savedInstanceState.getLong(KEY_DOWNLOAD_ID, -1);
+ errorRes = savedInstanceState.getInt(KEY_ERROR_RES, 0);
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = requireActivity().getLayoutInflater().inflate(R.layout.app_update_dialog, null);
+ messageText = view.findViewById(R.id.update_message_text);
+ progressContainer = view.findViewById(R.id.update_progress_container);
+ progressBar = view.findViewById(R.id.update_progress_bar);
+ progressText = view.findViewById(R.id.update_progress_text);
+
+ return new AlertDialog.Builder(requireContext())
+ .setView(view)
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ render();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ advanceState();
+ render();
+ startPolling();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopPolling();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(KEY_STATE, state.name());
+ outState.putLong(KEY_DOWNLOAD_ID, downloadId);
+ outState.putInt(KEY_ERROR_RES, errorRes);
+ }
+
+ @Override
+ public void onDestroy() {
+ stopPolling();
+ super.onDestroy();
+ }
+
+ private void advanceState() {
+ if (state != State.CHECK_PERMISSION && state != State.PERMISSION_NEEDED) return;
+
+ Activity activity = getActivity();
+ if (activity == null) return;
+
+ if (!DcHelper.canInstallApks(activity)) {
+ state = State.PERMISSION_NEEDED;
+ return;
+ }
+ state = State.DOWNLOADING;
+ if (downloadId == -1) {
+ startDownload();
+ }
+ }
+
+ private void startDownload() {
+ Context appContext = requireContext().getApplicationContext();
+ File dir = AppUpdate.getApkDir(appContext);
+ if (dir == null || downloadManager == null) {
+ showError(R.string.download_failed);
+ return;
+ }
+
+ File stale = new File(dir, AppUpdate.APK_FILENAME);
+ if (stale.exists() && !stale.delete()) {
+ Log.w(TAG, "could not delete file " + stale);
+ }
+
+ try {
+ DownloadManager.Request request =
+ new DownloadManager.Request(Uri.parse(requireArguments().getString(ARG_URL)));
+ request.setTitle(getString(R.string.update_downloading, versionString));
+ request.setDestinationInExternalFilesDir(
+ appContext, Environment.DIRECTORY_DOWNLOADS, AppUpdate.APK_FILENAME);
+ downloadId = downloadManager.enqueue(request);
+ } catch (Exception e) {
+ Log.e(TAG, "failed to start download", e);
+ showError(R.string.download_failed);
+ }
+ }
+
+ private void render() {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ if (dialog == null) return;
+ Button positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
+ if (positive == null || negative == null) return;
+
+ setCancelable(state == State.PERMISSION_NEEDED || state == State.ERROR);
+
+ switch (state) {
+ case PERMISSION_NEEDED:
+ messageText.setVisibility(View.VISIBLE);
+ messageText.setText(R.string.update_install_permission_explain);
+ progressContainer.setVisibility(View.GONE);
+ positive.setVisibility(View.VISIBLE);
+ positive.setText(R.string.open_settings);
+ positive.setOnClickListener(
+ v -> {
+ Activity activity = getActivity();
+ if (activity != null) {
+ DcHelper.requestInstallApksPermission(activity);
+ }
+ });
+ negative.setVisibility(View.VISIBLE);
+ negative.setText(R.string.cancel);
+ negative.setOnClickListener(v -> dismissAllowingStateLoss());
+ break;
+
+ case DOWNLOADING:
+ messageText.setVisibility(View.VISIBLE);
+ messageText.setText(getString(R.string.update_downloading, versionString));
+ progressContainer.setVisibility(View.VISIBLE);
+ positive.setVisibility(View.GONE);
+ negative.setVisibility(View.VISIBLE);
+ negative.setText(R.string.cancel);
+ negative.setOnClickListener(
+ v -> {
+ cancelDownload();
+ dismissAllowingStateLoss();
+ });
+ break;
+
+ case ERROR:
+ messageText.setVisibility(View.VISIBLE);
+ messageText.setText(errorRes != 0 ? errorRes : R.string.error);
+ progressContainer.setVisibility(View.GONE);
+ positive.setVisibility(View.VISIBLE);
+ positive.setText(R.string.ok);
+ positive.setOnClickListener(v -> dismissAllowingStateLoss());
+ negative.setVisibility(View.GONE);
+ break;
+
+ default:
+ messageText.setVisibility(View.GONE);
+ progressContainer.setVisibility(View.GONE);
+ positive.setVisibility(View.GONE);
+ negative.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ private void showError(@StringRes int messageRes) {
+ stopPolling();
+ state = State.ERROR;
+ errorRes = messageRes;
+ render();
+ }
+
+ private void failDownload(@StringRes int messageRes) {
+ cancelDownload();
+ showError(messageRes);
+ }
+
+ private void startPolling() {
+ stopPolling();
+ if (state != State.DOWNLOADING || downloadId == -1) return;
+ pollFailures = 0;
+ pollRunnable = this::poll;
+ handler.post(pollRunnable);
+ }
+
+ private void stopPolling() {
+ if (pollRunnable != null) {
+ handler.removeCallbacks(pollRunnable);
+ pollRunnable = null;
+ }
+ }
+
+ private void poll() {
+ if (state != State.DOWNLOADING || downloadId == -1) return;
+ Cursor cursor = null;
+ try {
+ cursor = downloadManager.query(new DownloadManager.Query().setFilterById(downloadId));
+ if (cursor == null || !cursor.moveToFirst()) {
+ failDownload(R.string.download_failed);
+ return;
+ }
+ pollFailures = 0;
+ int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+ if (status == DownloadManager.STATUS_SUCCESSFUL) {
+ String localUri =
+ cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI));
+ String localPath = localUri != null ? Uri.parse(localUri).getPath() : null;
+ if (localPath == null) {
+ failDownload(R.string.download_failed);
+ } else {
+ onDownloadSucceeded(localPath);
+ }
+ return;
+ } else if (status == DownloadManager.STATUS_FAILED
+ || status == DownloadManager.STATUS_PAUSED) {
+ failDownload(R.string.download_failed);
+ return;
+ }
+ updateProgress(
+ cursor.getLong(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)),
+ cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)));
+ } catch (Exception e) {
+ Log.e(TAG, "poll failed", e);
+ pollFailures++;
+ if (pollFailures >= MAX_POLL_FAILURES) {
+ failDownload(R.string.download_failed);
+ return;
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+
+ if (pollRunnable != null) {
+ handler.postDelayed(pollRunnable, POLL_INTERVAL_MS);
+ }
+ }
+
+ private void updateProgress(long downloaded, long total) {
+ if (progressBar == null) return;
+ if (total > 0) {
+ int percent = (int) (downloaded * 100 / total);
+ progressBar.setIndeterminate(false);
+ progressBar.setMax(100);
+ progressBar.setProgress(percent);
+ progressText.setText(getString(R.string.progress_percent, percent));
+ } else {
+ progressBar.setIndeterminate(true);
+ progressText.setText("");
+ }
+ }
+
+ private void onDownloadSucceeded(String localPath) {
+ stopPolling();
+
+ Context context = getContext();
+ Activity activity = getActivity();
+ if (context == null || activity == null) return;
+
+ PackageInfo info = context.getPackageManager().getPackageArchiveInfo(localPath, 0);
+ if (info == null || !context.getPackageName().equals(info.packageName)) {
+ new File(localPath).delete();
+ showError(R.string.update_apk_mismatch);
+ return;
+ }
+
+ Uri apkUri = downloadManager.getUriForDownloadedFile(downloadId);
+ if (apkUri == null) {
+ showError(R.string.download_failed);
+ return;
+ }
+ DcHelper.installApk(activity, apkUri);
+ dismissAllowingStateLoss();
+ }
+
+ private void cancelDownload() {
+ stopPolling();
+ if (downloadId != -1) {
+ downloadManager.remove(downloadId);
+ downloadId = -1;
+ }
+ }
+
+ private static State stateFromName(String name) {
+ if (name != null) {
+ try {
+ return State.valueOf(name);
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+ return State.CHECK_PERMISSION;
+ }
+}
diff --git a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java
index dc5194d1fb..954d66c388 100644
--- a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java
+++ b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java
@@ -43,6 +43,9 @@ public class Prefs {
private static final String ENTER_SENDS_PREF = "pref_enter_sends";
private static final String PROMPTED_DOZE_MSG_ID_PREF = "pref_prompted_doze_msg_id";
private static final String STATS_DEVICE_MSG_ID_PREF = "pref_stats_device_msg_id";
+ private static final String UPDATE_MSG_VERSION_PREF = "pref_update_msg_version";
+ private static final String UPDATE_MSG_ACCOUNT_ID_PREF = "pref_update_msg_account_id";
+ private static final String UPDATE_MSG_ID_PREF = "pref_update_msg_id";
public static final String DOZE_ASKED_DIRECTLY = "pref_doze_asked_directly";
public static final String ASKED_FOR_NOTIFICATION_PERMISSION =
"pref_asked_for_notification_permission";
@@ -181,6 +184,27 @@ public static int getStatsDeviceMsgId(Context context) {
return getIntegerPreference(context, STATS_DEVICE_MSG_ID_PREF, 0);
}
+ public static void setUpdateDeviceMsg(Context context, int version, int accountId, int msgId) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putInt(UPDATE_MSG_VERSION_PREF, version)
+ .putInt(UPDATE_MSG_ACCOUNT_ID_PREF, accountId)
+ .putInt(UPDATE_MSG_ID_PREF, msgId)
+ .apply();
+ }
+
+ public static int getUpdateMsgVersion(Context context) {
+ return getIntegerPreference(context, UPDATE_MSG_VERSION_PREF, 0);
+ }
+
+ public static int getUpdateMsgId(Context context) {
+ return getIntegerPreference(context, UPDATE_MSG_ID_PREF, 0);
+ }
+
+ public static int getUpdateMsgAccountId(Context context) {
+ return getIntegerPreference(context, UPDATE_MSG_ACCOUNT_ID_PREF, 0);
+ }
+
public static boolean isPushEnabled(Context context) {
return BuildConfig.USE_PLAY_SERVICES;
}
diff --git a/src/main/res/layout/app_update_dialog.xml b/src/main/res/layout/app_update_dialog.xml
new file mode 100644
index 0000000000..499c68bc36
--- /dev/null
+++ b/src/main/res/layout/app_update_dialog.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index d649665942..1911f400ed 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -251,6 +251,8 @@
Magenta
White
+ %1$d%%
+
Zoom
Extra small
Small
@@ -1265,4 +1267,9 @@
What\'s new?\n\n💯 End-to-end encryption is reliable and forever now. Padlocks 🔒 are gone!\n\n✉️ Classic email without end-to-end encryption is marked with a letter symbol\n\n😻 New enhanced profile screen for all your contacts\n\n🔲 New button for quick access to apps used in a chat\n\n❤️ Please donate to help us remain independent and continue to bring improvements: %1$s
+ 🎉 New version available. Tap this message to update and enjoy the new features
+ Downloading %1$s …
+ The downloaded apk does not match this app and will not be installed
+ Update Now
+ To install the update, please first allow Delta Chat to install apps in the system settings.