Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ interface EventListener {
void onReactionClicked(DcMsg messageRecord);

void onStickerClicked(DcMsg messageRecord);

void onUpdateNowClicked(DcMsg messageRecord);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/thoughtcrime/securesms/ConversationItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
35 changes: 29 additions & 6 deletions src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
158 changes: 158 additions & 0 deletions src/main/java/org/thoughtcrime/securesms/updater/AppUpdate.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading