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.