diff --git a/build.gradle b/build.gradle
index 66297a578c1f365e1586e21b339c66e131a2a859..437740c02c77225f88c0e47194a6438b53b39bbe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -95,6 +95,7 @@ dependencies {
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.github.ipld:java-cid:v1.3.1'
implementation 'com.splitwise:tokenautocomplete:3.0.2'
implementation 'me.saket:better-link-movement-method:2.2.0'
diff --git a/src/cheogram/res/values/strings.xml b/src/cheogram/res/values/strings.xml
index d4bc883598daf9b0b2d61f4d067b5ab49140155c..f30ca68a50e84a3e0839d28eadac6565cd81b844 100644
--- a/src/cheogram/res/values/strings.xml
+++ b/src/cheogram/res/values/strings.xml
@@ -28,4 +28,6 @@
Show only this thread
Use Phone Accounts for Incoming Calls
Incoming calls from phone numbers may ring with your system dialler instead of this app\'s notification settings
+ Save as Sticker
+ Sticker Name
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index e43755c90e913cc3565c1e852f868052f74d4d66..081c7f31770c891193b3ea46cc546b4837a4aed0 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -1188,6 +1188,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return size == null ? 0 : size;
}
+ public String getName() {
+ Element file = getFileElement();
+ if (file == null) return null;
+
+ return file.findChildContent("name", file.getNamespace());
+ }
+
public Element toSims() {
if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
sims.setAttribute("type", "data");
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 810a8f2b4a6839731e5b57c2a50be9971a385f5b..2748fac69fdc991827a9d1f33fc19f7fdf9ba4a7 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -35,6 +35,7 @@ import android.util.LruCache;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
+import androidx.documentfile.provider.DocumentFile;
import androidx.exifinterface.media.ExifInterface;
import com.cheogram.android.BobTransfer;
@@ -673,6 +674,34 @@ public class FileBackend {
return FileUtils.getPath(mXmppConnectionService, uri);
}
+ public void copyFileToDocumentFile(Context ctx, File file, DocumentFile df, String name) throws FileCopyException {
+ Log.d(
+ Config.LOGTAG,
+ "copy file (" + file + ") to " + df + " / " + name);
+ final DocumentFile dff = df.createFile(MimeUtils.guessMimeTypeFromUri(ctx, getUriForFile(ctx, file)), name);
+ try (final InputStream is = new FileInputStream(file);
+ final OutputStream os =
+ mXmppConnectionService.getContentResolver().openOutputStream(dff.getUri())) {
+ if (is == null) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ }
+ try {
+ ByteStreams.copy(is, os);
+ os.flush();
+ } catch (IOException e) {
+ throw new FileWriterException(file);
+ }
+ } catch (final FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (final FileWriterException e) {
+ throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
+ } catch (final SecurityException | IllegalStateException e) {
+ throw new FileCopyException(R.string.error_security_exception);
+ } catch (final IOException e) {
+ throw new FileCopyException(R.string.error_io_exception);
+ }
+ }
+
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
Log.d(
Config.LOGTAG,
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 0357d6e8164a87997209d0b09f235d8054107cb7..5d57403a4edb8469225143e3200dab52ac2c9001 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -23,7 +23,9 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Environment;
import android.os.Handler;
+import android.os.storage.StorageManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
@@ -62,6 +64,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil;
+import androidx.documentfile.provider.DocumentFile;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -72,6 +75,7 @@ import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
+import java.io.File;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -167,6 +171,7 @@ public class ConversationFragment extends XmppFragment
public static final int REQUEST_COMMIT_ATTACHMENTS = 0x0212;
public static final int REQUEST_START_AUDIO_CALL = 0x213;
public static final int REQUEST_START_VIDEO_CALL = 0x214;
+ public static final int REQUEST_SAVE_STICKER = 0x215;
public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
@@ -206,6 +211,8 @@ public class ConversationFragment extends XmppFragment
private ConversationsActivity activity;
private boolean reInitRequiredOnStart = true;
private int identiconWidth = -1;
+ private File savingAsSticker = null;
+ private String savingAsStickerName = null;
private final OnClickListener clickToMuc =
new OnClickListener() {
@@ -986,6 +993,24 @@ public class ConversationFragment extends XmppFragment
private void handlePositiveActivityResult(int requestCode, final Intent data) {
switch (requestCode) {
+ case REQUEST_SAVE_STICKER:
+ final DocumentFile df = DocumentFile.fromTreeUri(activity, data.getData());
+ final File f = savingAsSticker;
+ final String existingName = savingAsStickerName;
+ savingAsSticker = null;
+ savingAsStickerName = null;
+ activity.quickEdit(existingName, R.string.sticker_name, (name) -> {
+ try {
+ activity.xmppConnectionService.getFileBackend().copyFileToDocumentFile(activity, f, df, name);
+ } catch (final FileBackend.FileCopyException e) {
+ Toast.makeText(activity, e.getResId(), Toast.LENGTH_SHORT).show();
+ return null;
+ }
+
+ Toast.makeText(activity, "Sticker saved", Toast.LENGTH_SHORT).show();
+ return null;
+ });
+ break;
case REQUEST_TRUST_KEYS_TEXT:
sendMessage();
break;
@@ -1405,6 +1430,7 @@ public class ConversationFragment extends XmppFragment
MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
+ MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker);
MenuItem downloadFile = menu.findItem(R.id.download_file);
MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
MenuItem deleteFile = menu.findItem(R.id.delete_file);
@@ -1467,6 +1493,7 @@ public class ConversationFragment extends XmppFragment
if (path == null
|| !path.startsWith("/")
|| FileBackend.inConversationsDirectory(requireActivity(), path)) {
+ saveAsSticker.setVisible(true);
deleteFile.setVisible(true);
deleteFile.setTitle(
activity.getString(
@@ -1523,6 +1550,9 @@ public class ConversationFragment extends XmppFragment
case R.id.copy_url:
ShareUtil.copyUrlToClipboard(activity, selectedMessage);
return true;
+ case R.id.save_as_sticker:
+ saveAsSticker(selectedMessage);
+ return true;
case R.id.download_file:
startDownloadable(selectedMessage);
return true;
@@ -2296,6 +2326,32 @@ public class ConversationFragment extends XmppFragment
builder.create().show();
}
+ private void saveAsSticker(final Message m) {
+ String existingName = m.getFileParams() != null && m.getFileParams().getName() != null ? m.getFileParams().getName() : "";
+ existingName = existingName.lastIndexOf(".") == -1 ? existingName : existingName.substring(0, existingName.lastIndexOf("."));
+ saveAsSticker(activity.xmppConnectionService.getFileBackend().getFile(m), existingName);
+ }
+
+ private void saveAsSticker(final File file, final String name) {
+ savingAsSticker = file;
+ savingAsStickerName = name;
+
+ Intent intent = ((StorageManager) activity.getSystemService(Context.STORAGE_SERVICE)).getPrimaryStorageVolume().createOpenDocumentTreeIntent();
+
+ SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity);
+ final String dir = p.getString("sticker_directory", "Stickers");
+ if (dir.startsWith("content://")) {
+ intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(dir));
+ } else {
+ new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir + "/User Pack").mkdirs();
+ Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
+ intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(uri.toString().replace("/root/", "/document/") + "%3APictures%2F" + dir));
+ }
+
+ Toast.makeText(activity, "Choose a sticker pack to add this sticker to", Toast.LENGTH_SHORT).show();
+ startActivityForResult(Intent.createChooser(intent, "Choose sticker pack"), REQUEST_SAVE_STICKER);
+ }
+
private void deleteFile(final Message message) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setNegativeButton(R.string.cancel, null);
diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml
index 34b8724adf5df6c1ff96ff06ba7d4ef4632fceb5..693674571af7e1dc7464f6936bc3011c9039410e 100644
--- a/src/main/res/menu/message_context.xml
+++ b/src/main/res/menu/message_context.xml
@@ -42,6 +42,10 @@
android:id="@+id/copy_url"
android:title="@string/copy_original_url"
android:visible="false" />
+