format some more files

This commit is contained in:
adbenitez
2026-03-16 16:57:36 +01:00
parent 44441f1ff9
commit ea522be8f4
2 changed files with 273 additions and 210 deletions
@@ -12,17 +12,19 @@ import android.provider.Settings;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.LocalHelpActivity;
@@ -38,83 +40,79 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class DcHelper {
private static final String TAG = DcHelper.class.getSimpleName();
private static final String TAG = DcHelper.class.getSimpleName();
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
public static final String CONFIG_DISPLAY_NAME = "displayname";
public static final String CONFIG_SELF_STATUS = "selfstatus";
public static final String CONFIG_SELF_AVATAR = "selfavatar";
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
public static final String CONFIG_BCC_SELF = "bcc_self";
public static final String CONFIG_SHOW_EMAILS = "show_emails";
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
public static final String CONFIG_PROXY_URL = "proxy_url";
public static final String CONFIG_PRIVATE_TAG = "private_tag";
public static final String CONFIG_STATS_SENDING = "stats_sending";
public static final String CONFIG_STATS_ID = "stats_id";
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
public static final String CONFIG_DISPLAY_NAME = "displayname";
public static final String CONFIG_SELF_STATUS = "selfstatus";
public static final String CONFIG_SELF_AVATAR = "selfavatar";
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
public static final String CONFIG_BCC_SELF = "bcc_self";
public static final String CONFIG_SHOW_EMAILS = "show_emails";
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
public static final String CONFIG_PROXY_URL = "proxy_url";
public static final String CONFIG_PRIVATE_TAG = "private_tag";
public static final String CONFIG_STATS_SENDING = "stats_sending";
public static final String CONFIG_STATS_ID = "stats_id";
public static DcContext getContext(@NonNull Context context) {
return ApplicationContext.getInstance(context).getDcContext();
}
public static DcContext getContext(@NonNull Context context) {
return ApplicationContext.getInstance(context).getDcContext();
}
public static Rpc getRpc(@NonNull Context context) {
return ApplicationContext.getInstance(context).getRpc();
}
public static Rpc getRpc(@NonNull Context context) {
return ApplicationContext.getInstance(context).getRpc();
}
public static DcAccounts getAccounts(@NonNull Context context) {
return ApplicationContext.getInstance(context).getDcAccounts();
}
public static DcAccounts getAccounts(@NonNull Context context) {
return ApplicationContext.getInstance(context).getDcAccounts();
}
public static DcEventCenter getEventCenter(@NonNull Context context) {
return ApplicationContext.getInstance(context).getEventCenter();
}
public static DcEventCenter getEventCenter(@NonNull Context context) {
return ApplicationContext.getInstance(context).getEventCenter();
}
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
return ApplicationContext.getInstance(context).getNotificationCenter();
}
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
return ApplicationContext.getInstance(context).getNotificationCenter();
}
public static boolean isConfigured(Context context) {
DcContext dcContext = getContext(context);
return dcContext.isConfigured() == 1;
}
public static boolean isConfigured(Context context) {
DcContext dcContext = getContext(context);
return dcContext.isConfigured() == 1;
}
public static int getInt(Context context, String key) {
DcContext dcContext = getContext(context);
return dcContext.getConfigInt(key);
}
public static int getInt(Context context, String key) {
DcContext dcContext = getContext(context);
return dcContext.getConfigInt(key);
}
public static String get(Context context, String key) {
DcContext dcContext = getContext(context);
return dcContext.getConfig(key);
}
public static String get(Context context, String key) {
DcContext dcContext = getContext(context);
return dcContext.getConfig(key);
}
@Deprecated public static int getInt(Context context, String key, int defaultValue) {
return getInt(context, key);
}
@Deprecated
public static int getInt(Context context, String key, int defaultValue) {
return getInt(context, key);
}
@Deprecated public static String get(Context context, String key, String defaultValue) {
return get(context, key);
}
@Deprecated
public static String get(Context context, String key, String defaultValue) {
return get(context, key);
}
public static void set(Context context, String key, String value) {
DcContext dcContext = getContext(context);
dcContext.setConfig(key, value);
}
public static void set(Context context, String key, String value) {
DcContext dcContext = getContext(context);
dcContext.setConfig(key, value);
}
public static void setStockTranslations(Context context) {
DcContext dcContext = getContext(context);
// the integers are defined in the core and used only here, an enum or sth. like that won't have a big benefit
// the integers are defined in the core and used only here, an enum or sth. like that won't have
// a big benefit
dcContext.setStockTranslation(1, context.getString(R.string.chat_no_messages));
dcContext.setStockTranslation(2, context.getString(R.string.self));
dcContext.setStockTranslation(3, context.getString(R.string.draft));
@@ -133,7 +131,8 @@ public class DcHelper {
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
dcContext.setStockTranslation(71, context.getString(R.string.device_talk_welcome_message2));
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
dcContext.setStockTranslation(
73, context.getString(R.string.systemmsg_subject_for_new_contact));
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
dcContext.setStockTranslation(85, context.getString(R.string.devicemsg_bad_time));
@@ -160,9 +159,11 @@ public class DcHelper {
dcContext.setStockTranslation(119, context.getString(R.string.qrshow_join_contact_hint));
// HACK: svg does not handle entities correctly and shows `"` as the text `quot;`.
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does not affect so many characters)
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does
// not affect so many characters)
// cmp. https://github.com/deltachat/deltachat-android/issues/2187
dcContext.setStockTranslation(120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
dcContext.setStockTranslation(
120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
dcContext.setStockTranslation(121, context.getString(R.string.connectivity_not_connected));
dcContext.setStockTranslation(124, context.getString(R.string.group_name_changed_by_you));
dcContext.setStockTranslation(125, context.getString(R.string.group_name_changed_by_other));
@@ -179,9 +180,11 @@ public class DcHelper {
dcContext.setStockTranslation(136, context.getString(R.string.location_enabled_by_you));
dcContext.setStockTranslation(137, context.getString(R.string.location_enabled_by_other));
dcContext.setStockTranslation(138, context.getString(R.string.ephemeral_timer_disabled_by_you));
dcContext.setStockTranslation(139, context.getString(R.string.ephemeral_timer_disabled_by_other));
dcContext.setStockTranslation(
139, context.getString(R.string.ephemeral_timer_disabled_by_other));
dcContext.setStockTranslation(140, context.getString(R.string.ephemeral_timer_seconds_by_you));
dcContext.setStockTranslation(141, context.getString(R.string.ephemeral_timer_seconds_by_other));
dcContext.setStockTranslation(
141, context.getString(R.string.ephemeral_timer_seconds_by_other));
dcContext.setStockTranslation(144, context.getString(R.string.ephemeral_timer_1_hour_by_you));
dcContext.setStockTranslation(145, context.getString(R.string.ephemeral_timer_1_hour_by_other));
dcContext.setStockTranslation(146, context.getString(R.string.ephemeral_timer_1_day_by_you));
@@ -189,7 +192,8 @@ public class DcHelper {
dcContext.setStockTranslation(148, context.getString(R.string.ephemeral_timer_1_week_by_you));
dcContext.setStockTranslation(149, context.getString(R.string.ephemeral_timer_1_week_by_other));
dcContext.setStockTranslation(150, context.getString(R.string.ephemeral_timer_minutes_by_you));
dcContext.setStockTranslation(151, context.getString(R.string.ephemeral_timer_minutes_by_other));
dcContext.setStockTranslation(
151, context.getString(R.string.ephemeral_timer_minutes_by_other));
dcContext.setStockTranslation(152, context.getString(R.string.ephemeral_timer_hours_by_you));
dcContext.setStockTranslation(153, context.getString(R.string.ephemeral_timer_hours_by_other));
dcContext.setStockTranslation(154, context.getString(R.string.ephemeral_timer_days_by_you));
@@ -199,11 +203,14 @@ public class DcHelper {
dcContext.setStockTranslation(158, context.getString(R.string.ephemeral_timer_1_year_by_you));
dcContext.setStockTranslation(159, context.getString(R.string.ephemeral_timer_1_year_by_other));
dcContext.setStockTranslation(162, context.getString(R.string.multidevice_qr_subtitle));
dcContext.setStockTranslation(163, context.getString(R.string.multidevice_transfer_done_devicemsg));
dcContext.setStockTranslation(170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
dcContext.setStockTranslation(
163, context.getString(R.string.multidevice_transfer_done_devicemsg));
dcContext.setStockTranslation(
170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
dcContext.setStockTranslation(172, context.getString(R.string.chat_new_group_hint));
dcContext.setStockTranslation(173, context.getString(R.string.member_x_added));
dcContext.setStockTranslation(174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
dcContext.setStockTranslation(
174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
dcContext.setStockTranslation(176, context.getString(R.string.reaction_by_you));
dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other));
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
@@ -225,28 +232,34 @@ public class DcHelper {
dcContext.setStockTranslation(234, context.getString(R.string.audio_call));
dcContext.setStockTranslation(235, context.getString(R.string.video_call));
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
dcContext.setStockTranslation(
241, context.getString(R.string.chat_description_changed_by_other));
}
public static File getImexDir() {
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by
// the user,
// eg. "Download Managers" are nearly always installed.
// CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
// When the user shares a file to another app or opens a file in another app, it is added here.
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the user-visible filename).
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the
// user-visible filename).
public static final HashMap<String, String> sharedFiles = new HashMap<>();
public static void openForViewOrShare(Context activity, int msg_id, String cmd) {
DcContext dcContext = getContext(activity);
if(!(activity instanceof Activity)) {
if (!(activity instanceof Activity)) {
// would be nicer to accepting only Activity objects,
// however, typically in Android just Context objects are passed around (as this normally does not make a difference).
// Accepting only Activity here would force callers to cast, which would easily result in even more ugliness.
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG).show();
// however, typically in Android just Context objects are passed around (as this normally does
// not make a difference).
// Accepting only Activity here would force callers to cast, which would easily result in even
// more ugliness.
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG)
.show();
return;
}
@@ -257,7 +270,9 @@ public class DcHelper {
try {
File file = new File(path);
if (!file.exists()) {
Toast.makeText(activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG).show();
Toast.makeText(
activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG)
.show();
return;
}
@@ -267,7 +282,14 @@ public class DcHelper {
// Build a Uri that will later be passed to AttachmentsContentProvider.openFile().
// The last part needs to be `filename`, i.e. the original, user-visible name of the file,
// so that the external apps show the name of the file correctly.
uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(file.getName()) + "/" + Uri.encode(filename));
uri =
Uri.parse(
"content://"
+ BuildConfig.APPLICATION_ID
+ ".attachments/"
+ Uri.encode(file.getName())
+ "/"
+ Uri.encode(filename));
// As different Android version handle uris in putExtra differently,
// we also check on our own that the file was actually shared.
@@ -275,7 +297,9 @@ public class DcHelper {
sharedFiles.put(file.getName(), mimeType);
} else {
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
uri =
FileProvider.getUriForFile(
activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
@@ -292,76 +316,89 @@ public class DcHelper {
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.putExtra(Intent.EXTRA_TEXT, msg.getText());
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
activity.startActivity(
Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
}
} catch (RuntimeException e) {
Toast.makeText(activity, String.format("%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType), Toast.LENGTH_LONG).show();
Toast.makeText(
activity,
String.format(
"%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType),
Toast.LENGTH_LONG)
.show();
Log.i(TAG, "opening of external activity failed.", e);
}
}
public static void sendToChat(Context activity, byte[] data, String mimeType, String fileName, String text) {
Intent intent = new Intent(activity, ShareActivity.class);
intent.setAction(Intent.ACTION_SEND);
ShareUtil.setIsFromWebxdc(intent, true);
public static void sendToChat(
Context activity, byte[] data, String mimeType, String fileName, String text) {
Intent intent = new Intent(activity, ShareActivity.class);
intent.setAction(Intent.ACTION_SEND);
ShareUtil.setIsFromWebxdc(intent, true);
if (data != null) {
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_STREAM, uri);
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
if (data != null) {
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_STREAM, uri);
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
}
if (text != null) {
intent.putExtra(Intent.EXTRA_TEXT, text);
if (data == null) {
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
}
}
if (text != null) {
intent.putExtra(Intent.EXTRA_TEXT, text);
if (data == null) {
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
}
}
activity.startActivity(intent);
activity.startActivity(intent);
}
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 (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()))));
return;
}
activity.startActivity(intent);
}
public static String checkMime(String path, String mimeType) {
if(mimeType == null || mimeType.equals("application/octet-stream")) {
if (mimeType == null || mimeType.equals("application/octet-stream")) {
path = path.replaceAll(" ", "");
String extension = MediaUtil.getFileExtensionFromUrl(path);
String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if(newType != null) return newType;
if (newType != null) return newType;
}
return mimeType;
}
/**
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename
* and the given extension.
* In many cases, since we're using setFileAndDeduplicate now, this wouldn't be necessary anymore
* and we could just create a file with a random filename,
* but there are a few usages that still need the current behavior (like `openMaps()`).
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename and
* the given extension. In many cases, since we're using setFileAndDeduplicate now, this wouldn't
* be necessary anymore and we could just create a file with a random filename, but there are a
* few usages that still need the current behavior (like `openMaps()`).
*/
public static String getBlobdirFile(DcContext dcContext, String filename, String ext) {
filename = FileUtils.sanitizeFilename(filename);
ext = FileUtils.sanitizeFilename(ext);
String outPath = null;
for (int i = 0; i < 1000; i++) {
String test = dcContext.getBlobdir() + "/" + filename + (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i)) + ext;
String test =
dcContext.getBlobdir()
+ "/"
+ filename
+ (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i))
+ ext;
if (!new File(test).exists()) {
outPath = test;
break;
}
}
if(outPath==null) {
if (outPath == null) {
// should not happen
outPath = dcContext.getBlobdir() + "/" + Math.random();
}
@@ -369,19 +406,22 @@ public class DcHelper {
}
public static String getBlobdirFile(DcContext dcContext, String path) {
String filename = path.substring(path.lastIndexOf('/')+1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
String filename =
path.substring(
path.lastIndexOf('/')
+ 1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
String ext = "";
int point = filename.indexOf('.');
if(point!=-1) {
if (point != -1) {
ext = filename.substring(point);
filename = filename.substring(0, point);
}
return getBlobdirFile(dcContext, filename, ext);
}
@NonNull
public static ThreadRecord getThreadRecord(Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
public static ThreadRecord getThreadRecord(
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
int chatId = chat.getId();
String body = summary.getText1();
@@ -394,14 +434,23 @@ public class DcHelper {
long date = summary.getTimestamp();
int unreadCount = getContext(context).getFreshMsgCount(chatId);
return new ThreadRecord(body, recipient, date,
unreadCount, chatId,
chat.getVisibility(), chat.isSendingLocations(), chat.isMuted(), chat.isContactRequest(), summary);
return new ThreadRecord(
body,
recipient,
date,
unreadCount,
chatId,
chat.getVisibility(),
chat.isSendingLocations(),
chat.isMuted(),
chat.isContactRequest(),
summary);
}
public static boolean isNetworkConnected(Context context) {
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnected()) {
return true;
@@ -414,105 +463,110 @@ public class DcHelper {
/**
* Gets a string you can show to the user with basic information about connectivity.
*
* @param context
* @param connectedString Usually "Connected", but when using this as the title in
* ConversationListActivity, we want to write "Delta Chat"
* or the user's display name there instead.
* ConversationListActivity, we want to write "Delta Chat" or the user's display name there
* instead.
* @return
*/
public static String getConnectivitySummary(Context context, String connectedString) {
int connectivity = getContext(context).getConnectivity();
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
return connectedString;
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
return context.getString(R.string.connectivity_updating);
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
return context.getString(R.string.connectivity_connecting);
} else {
return context.getString(R.string.connectivity_not_connected);
}
int connectivity = getContext(context).getConnectivity();
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
return connectedString;
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
return context.getString(R.string.connectivity_updating);
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
return context.getString(R.string.connectivity_connecting);
} else {
return context.getString(R.string.connectivity_not_connected);
}
}
public static void showProtectionEnabledDialog(Context context) {
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
}
public static void showInvalidUnencryptedDialog(Context context) {
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
.setNegativeButton(R.string.qrscan_title, (d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
.setNegativeButton(
R.string.qrscan_title,
(d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
}
public static void openHelp(Context context, String section) {
Intent intent = new Intent(context, LocalHelpActivity.class);
if (section != null) { intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section); }
if (section != null) {
intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section);
}
context.startActivity(intent);
}
/**
* For the PGP-Contacts migration, things can go wrong.
* The migration happens when the account is setup, at which point no events can be sent yet.
* So, instead, if something goes wrong, it's returned by getLastError().
* This function shows the error message to the user.
* <p>
* A few releases after the PGP-contacts migration (which happened in 2025-05),
* we can remove this function again.
*/
public static void maybeShowMigrationError(Context context) {
try {
String lastError = DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
/**
* For the PGP-Contacts migration, things can go wrong. The migration happens when the account is
* setup, at which point no events can be sent yet. So, instead, if something goes wrong, it's
* returned by getLastError(). This function shows the error message to the user.
*
* <p>A few releases after the PGP-contacts migration (which happened in 2025-05), we can remove
* this function again.
*/
public static void maybeShowMigrationError(Context context) {
try {
String lastError =
DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
if (lastError != null && !lastError.isEmpty()) {
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
if (lastError != null && !lastError.isEmpty()) {
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
String subject = "Delta Chat failed to update";
String email = "delta@merlinux.eu";
String subject = "Delta Chat failed to update";
String email = "delta@merlinux.eu";
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.error_x, lastError))
.setNeutralButton(R.string.global_menu_edit_copy_desktop, (d, which) -> {
Util.writeTextToClipboard(context, lastError);
})
.setPositiveButton(R.string.menu_send, (d, which) -> {
Intent sharingIntent = new Intent(
Intent.ACTION_SENDTO, Uri.fromParts(
"mailto", email, null
)
);
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.error_x, lastError))
.setNeutralButton(
R.string.global_menu_edit_copy_desktop,
(d, which) -> {
Util.writeTextToClipboard(context, lastError);
})
.setPositiveButton(
R.string.menu_send,
(d, which) -> {
Intent sharingIntent =
new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", email, null));
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {email});
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
Log.w(TAG, "No email client found to send crash report");
sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
}
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
Log.w(TAG, "No email client found to send crash report");
sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
}
Intent chooser =
Intent.createChooser(sharingIntent, "Send using...");
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
Intent chooser = Intent.createChooser(sharingIntent, "Send using...");
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
context.startActivity(chooser);
})
.setCancelable(false)
.show();
}
} catch (RpcException e) {
e.printStackTrace();
}
context.startActivity(chooser);
})
.setCancelable(false)
.show();
}
} catch (RpcException e) {
e.printStackTrace();
}
}
}
@@ -67,11 +67,12 @@ public class PersistentBlobProvider {
private PersistentBlobProvider() {}
public Uri create(
@NonNull Context context,
@NonNull File inputFile,
@NonNull String mimeType,
@Nullable String fileName,
@Nullable Long fileSize) throws IOException {
@NonNull Context context,
@NonNull File inputFile,
@NonNull String mimeType,
@Nullable String fileName,
@Nullable Long fileSize)
throws IOException {
if (!inputFile.exists()) {
throw new IOException("Input file does not exist: " + inputFile.getAbsolutePath());
@@ -102,22 +103,30 @@ public class PersistentBlobProvider {
Util.copy(input, output);
} finally {
if (output != null) {
try { output.close(); } catch (IOException e) { Log.w(TAG, e); }
try {
output.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
if (input != null) {
try { input.close(); } catch (IOException e) { Log.w(TAG, e); }
try {
input.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}
final Uri uniqueUri =
CONTENT_URI
.buildUpon()
.appendPath(mimeType)
.appendPath(fileName)
.appendEncodedPath(String.valueOf(fileSize))
.appendEncodedPath(String.valueOf(System.currentTimeMillis()))
.build();
CONTENT_URI
.buildUpon()
.appendPath(mimeType)
.appendPath(fileName)
.appendEncodedPath(String.valueOf(fileSize))
.appendEncodedPath(String.valueOf(System.currentTimeMillis()))
.build();
return ContentUris.withAppendedId(uniqueUri, id);
}