support 'Save to downloads' action for attachments

This commit is contained in:
kosyak 2023-12-18 04:32:23 +01:00
parent f9027fa085
commit 9467fc1789
7 changed files with 148 additions and 24 deletions

View file

@ -664,6 +664,56 @@ public class FileBackend {
} }
} }
public void copyAttachmentToDownloadsFolder(Message m) throws FileCopyException {
File input = mXmppConnectionService.getFileBackend().getFile(m);
File parentDirectory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File output = new File(parentDirectory, input.getName());
int counter = 1;
while (output.exists()) {
output = new File(parentDirectory, "(" + counter + ") " + input.getName());
counter++;
}
try {
output.createNewFile();
} catch (IOException e) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
}
try (final OutputStream os = new FileOutputStream(output);
final InputStream is =
mXmppConnectionService.getContentResolver().openInputStream(Uri.fromFile(input))) {
if (is == null) {
throw new FileCopyException(R.string.error_file_not_found);
}
try {
ByteStreams.copy(is, os);
} catch (IOException e) {
throw new FileWriterException(output);
}
try {
os.flush();
} catch (IOException e) {
throw new FileWriterException(output);
}
updateMediaScanner(output);
} catch (final FileNotFoundException e) {
cleanup(output);
throw new FileCopyException(R.string.error_file_not_found);
} catch (final FileWriterException e) {
cleanup(output);
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (final SecurityException | IllegalStateException e) {
cleanup(output);
throw new FileCopyException(R.string.error_security_exception);
} catch (final IOException e) {
cleanup(output);
throw new FileCopyException(R.string.error_io_exception);
}
}
private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,

View file

@ -212,7 +212,7 @@ public class XmppConnectionService extends Service {
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
private final static Executor COPY_TO_DOWNLOAD_EXECUTOR = Executors.newSingleThreadExecutor();
private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor();
private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression"); private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter"); private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
@ -540,6 +540,17 @@ public class XmppConnectionService extends Service {
return this.restoredFromDatabaseLatch.getCount() == 0; return this.restoredFromDatabaseLatch.getCount() == 0;
} }
public void copyAttachmentToDownloadsFolder(Message m, final UiCallback<Integer> callback) {
COPY_TO_DOWNLOAD_EXECUTOR.execute(() -> {
try {
fileBackend.copyAttachmentToDownloadsFolder(m);
callback.success(-1);
} catch (FileBackend.FileCopyException e) {
callback.error(-1, e.getResId());
}
});
}
public PgpEngine getPgpEngine() { public PgpEngine getPgpEngine() {
if (!Config.supportOpenPgp()) { if (!Config.supportOpenPgp()) {
return null; return null;

View file

@ -18,6 +18,8 @@ import android.app.FragmentManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@ -28,6 +30,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.VibrationEffect; import android.os.VibrationEffect;
@ -40,7 +43,6 @@ import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.util.Range;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
@ -53,8 +55,6 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.CycleInterpolator;
import android.view.ViewParent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView; import android.widget.AbsListView;
@ -64,7 +64,6 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ListView; import android.widget.ListView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast; import android.widget.Toast;
@ -77,8 +76,6 @@ import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import com.google.common.base.Optional; import com.google.common.base.Optional;
@ -86,19 +83,18 @@ import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -150,7 +146,6 @@ import eu.siacs.conversations.ui.widget.HighlighterView;
import eu.siacs.conversations.ui.widget.TabLayout; import eu.siacs.conversations.ui.widget.TabLayout;
import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.NickValidityChecker;
@ -172,19 +167,6 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public class ConversationFragment extends XmppFragment public class ConversationFragment extends XmppFragment
implements EditMessage.KeyboardListener, implements EditMessage.KeyboardListener,
MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureLongClicked,
@ -1729,6 +1711,7 @@ public class ConversationFragment extends XmppFragment
MenuItem shareWith = menu.findItem(R.id.share_with); MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem copyUrl = menu.findItem(R.id.copy_url);
MenuItem saveToDownloads = menu.findItem(R.id.save_to_downloads);
MenuItem downloadFile = menu.findItem(R.id.download_file); MenuItem downloadFile = menu.findItem(R.id.download_file);
MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
MenuItem deleteFile = menu.findItem(R.id.delete_file); MenuItem deleteFile = menu.findItem(R.id.delete_file);
@ -1792,6 +1775,7 @@ public class ConversationFragment extends XmppFragment
|| t instanceof HttpDownloadConnection) { || t instanceof HttpDownloadConnection) {
copyUrl.setVisible(true); copyUrl.setVisible(true);
} }
if (m.isFileOrImage() && deleted && m.hasFileOnRemoteHost()) { if (m.isFileOrImage() && deleted && m.hasFileOnRemoteHost()) {
downloadFile.setVisible(true); downloadFile.setVisible(true);
downloadFile.setTitle( downloadFile.setTitle(
@ -1814,10 +1798,14 @@ public class ConversationFragment extends XmppFragment
|| !path.startsWith("/") || !path.startsWith("/")
|| FileBackend.inConversationsDirectory(requireActivity(), path)) { || FileBackend.inConversationsDirectory(requireActivity(), path)) {
deleteFile.setVisible(true); deleteFile.setVisible(true);
String fileDescriptorString = UIHelper.getFileDescriptionString(activity, m);
deleteFile.setTitle( deleteFile.setTitle(
activity.getString( activity.getString(
R.string.delete_x_file, R.string.delete_x_file,
UIHelper.getFileDescriptionString(activity, m))); fileDescriptorString));
saveToDownloads.setVisible(true);
} }
} }
if (showError) { if (showError) {
@ -1869,6 +1857,9 @@ public class ConversationFragment extends XmppFragment
case R.id.delete_file: case R.id.delete_file:
deleteFile(selectedMessage); deleteFile(selectedMessage);
return true; return true;
case R.id.save_to_downloads:
saveToDownloads(selectedMessage);
return true;
case R.id.show_error_message: case R.id.show_error_message:
showErrorMessage(selectedMessage); showErrorMessage(selectedMessage);
return true; return true;
@ -2635,6 +2626,24 @@ public class ConversationFragment extends XmppFragment
builder.create().show(); builder.create().show();
} }
private void saveToDownloads(final Message message) {
activity.xmppConnectionService.copyAttachmentToDownloadsFolder(message, new UiCallback<>() {
@Override
public void success(Integer object) {
runOnUiThread(() -> Toast.makeText(activity, R.string.save_to_downloads_success, Toast.LENGTH_LONG).show());
}
@Override
public void error(int errorCode, Integer object) {
runOnUiThread(() -> Toast.makeText(activity, object, Toast.LENGTH_LONG).show());
}
@Override
public void userInputRequired(PendingIntent pi, Integer object) {
}
});
}
private void resendMessage(final Message message) { private void resendMessage(final Message message) {
if (message.isFileOrImage()) { if (message.isFileOrImage()) {
if (!(message.getConversation() instanceof Conversation)) { if (!(message.getConversation() instanceof Conversation)) {

View file

@ -29,6 +29,7 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
@ -41,6 +42,7 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Toast;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
@ -56,6 +58,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageSearchTask; import eu.siacs.conversations.services.MessageSearchTask;
import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
@ -67,6 +70,7 @@ import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.FtsUtils; import eu.siacs.conversations.utils.FtsUtils;
import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard;
@ -140,6 +144,26 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
MenuItem copy = menu.findItem(R.id.copy_message); MenuItem copy = menu.findItem(R.id.copy_message);
MenuItem quote = menu.findItem(R.id.quote_message); MenuItem quote = menu.findItem(R.id.quote_message);
MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem copyUrl = menu.findItem(R.id.copy_url);
MenuItem saveToDownloads = menu.findItem(R.id.save_to_downloads);
final boolean deleted = message.isDeleted();
final boolean waitingOfferedSending =
message.getStatus() == Message.STATUS_WAITING
|| message.getStatus() == Message.STATUS_UNSEND
|| message.getStatus() == Message.STATUS_OFFERED;
final boolean cancelable =
(message.getTransferable() != null && !deleted) || waitingOfferedSending && message.needsUploading();
if (message.isFileOrImage() && !deleted && !cancelable) {
final String path = message.getRelativeFilePath();
if (path == null
|| !path.startsWith("/")
|| FileBackend.inConversationsDirectory(this, path)) {
saveToDownloads.setVisible(true);
}
}
if (message.isGeoUri()) { if (message.isGeoUri()) {
copy.setVisible(false); copy.setVisible(false);
quote.setVisible(false); quote.setVisible(false);
@ -171,6 +195,23 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
case R.id.copy_message: case R.id.copy_message:
ShareUtil.copyToClipboard(this, message); ShareUtil.copyToClipboard(this, message);
break; break;
case R.id.save_to_downloads:
xmppConnectionService.copyAttachmentToDownloadsFolder(message, new UiCallback<>() {
@Override
public void success(Integer object) {
runOnUiThread(() -> Toast.makeText(SearchActivity.this, R.string.save_to_downloads_success, Toast.LENGTH_LONG).show());
}
@Override
public void error(int errorCode, Integer object) {
runOnUiThread(() -> Toast.makeText(SearchActivity.this, object, Toast.LENGTH_LONG).show());
}
@Override
public void userInputRequired(PendingIntent pi, Integer object) {
}
});
break;
case R.id.copy_url: case R.id.copy_url:
ShareUtil.copyUrlToClipboard(this, message); ShareUtil.copyUrlToClipboard(this, message);
break; break;

View file

@ -42,6 +42,12 @@
android:id="@+id/copy_url" android:id="@+id/copy_url"
android:title="@string/copy_original_url" android:title="@string/copy_original_url"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/save_to_downloads"
android:title="@string/save_to_downloads"
android:visible="false" />
<item <item
android:id="@+id/show_error_message" android:id="@+id/show_error_message"
android:title="@string/show_error_message" android:title="@string/show_error_message"

View file

@ -46,4 +46,9 @@
<item <item
android:id="@+id/copy_url" android:id="@+id/copy_url"
android:title="@string/copy_original_url"/> android:title="@string/copy_original_url"/>
<item
android:id="@+id/save_to_downloads"
android:title="@string/save_to_downloads"
android:visible="false" />
</menu> </menu>

View file

@ -315,6 +315,8 @@
<string name="quote">Quote</string> <string name="quote">Quote</string>
<string name="paste_as_quote">Paste as quote</string> <string name="paste_as_quote">Paste as quote</string>
<string name="copy_original_url">Copy original URL</string> <string name="copy_original_url">Copy original URL</string>
<string name="save_to_downloads">Save to Downloads</string>
<string name="save_to_downloads_success">File successfully saved to Downloads folder</string>
<string name="send_again">Send again</string> <string name="send_again">Send again</string>
<string name="file_url">File URL</string> <string name="file_url">File URL</string>
<string name="url_copied_to_clipboard">Copied URL to clipboard</string> <string name="url_copied_to_clipboard">Copied URL to clipboard</string>