Skip to content

Commit

Permalink
On-demand export of attachments to external storage (close #1291)
Browse files Browse the repository at this point in the history
Signed-off-by: Daniele Ricci <[email protected]>
  • Loading branch information
daniele-athome committed Apr 5, 2020
1 parent b90a239 commit 23143a7
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 11 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- Sharing to groups (#864)
- Migrate to AndroidX (#1269)
- Support for Android 10 (#1282)
- Media is no longer saved on public storage (#1291)
- On-demand export of media to public storage (#1291)
- Improved multiselection experience
- Revamped settings appearance
- Removed Google Play donation
Expand Down
87 changes: 84 additions & 3 deletions app/src/main/java/org/kontalk/ui/AbstractComposeFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.greenrobot.eventbus.ThreadMode;
import org.jivesoftware.smackx.chatstates.ChatState;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
Expand Down Expand Up @@ -424,6 +425,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
MenuItem openMenu = menu.findItem(R.id.menu_open);
MenuItem dlMenu = menu.findItem(R.id.menu_download);
MenuItem cancelDlMenu = menu.findItem(R.id.menu_cancel_download);
MenuItem saveAttachment = menu.findItem(R.id.menu_save_attachment);

// initial status
deleteMenu.setVisible(true);
Expand All @@ -435,6 +437,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
openMenu.setVisible(false);
dlMenu.setVisible(false);
cancelDlMenu.setVisible(false);
saveAttachment.setVisible(false);

boolean singleItem = (mCheckedItemCount == 1);
if (singleItem) {
Expand Down Expand Up @@ -483,6 +486,7 @@ else if (attachment instanceof AudioComponent)

openMenu.setTitle(resId);
openMenu.setVisible(true);
saveAttachment.setVisible(true);
}

// message has a fetch url - add download control entry
Expand Down Expand Up @@ -583,6 +587,13 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
mode.finish();
return true;
}

case R.id.menu_save_attachment: {
CompositeMessage msg = getCheckedItem();
saveAttachments(msg);
mode.finish();
return true;
}
}
return false;
}
Expand Down Expand Up @@ -1161,6 +1172,77 @@ private void stopDownload(CompositeMessage msg) {
}
}

private void saveAttachments(CompositeMessage msg) {
if (SystemUtils.supportsScopedStorage() || Permissions.canWriteExternalStorage(getContext())) {
doSaveAttachments(msg);
}
else {
Permissions.requestWriteExternalStorage(this, getString(R.string.err_storage_denied));
Preferences.setPermissionAsked(Manifest.permission.WRITE_EXTERNAL_STORAGE);
Preferences.setPermissionAsked(Manifest.permission.READ_EXTERNAL_STORAGE);
}
}

private void doSaveAttachments(CompositeMessage msg) {
int totalAttachments = 0;
int savedAttachments = 0;
for (MessageComponent component : msg.getComponents()) {
if (component instanceof AttachmentComponent) {
totalAttachments++;

AttachmentComponent attachment = (AttachmentComponent) component;
Uri fileUri = attachment.getLocalUri();
if (fileUri == null) {
Log.d(TAG, "attachment not downloaded, skipping");
continue;
}
if (!"file".equals(fileUri.getScheme())) {
Log.w(TAG, "attachment comes from unknown source, skipping: " + fileUri);
continue;
}

File file = new File(fileUri.getPath());

MediaStorage.MediaStoreType mediaType;
if (attachment instanceof ImageComponent) {
mediaType = MediaStorage.MediaStoreType.IMAGE;
}
else if (attachment instanceof AudioComponent) {
// FIXME audios are only recordings for now, but they won't be forever
mediaType = MediaStorage.MediaStoreType.RECORDING;
}
else if (attachment instanceof VCardComponent) {
mediaType = MediaStorage.MediaStoreType.OTHER;
}
else /*if (attachment instanceof DefaultAttachmentComponent)*/ {
mediaType = MediaStorage.MediaStoreType.OTHER;
}
try {
MediaStorage.publishMedia(getContext(), file, mediaType);
savedAttachments++;
}
catch (IOException e) {
Log.w(TAG, "unable to save attachment: " + file, e);
}
}
}

String message;
if (savedAttachments == 0 && totalAttachments == 1) {
message = getString(R.string.err_storage_attachment_single);
}
else if (totalAttachments > 1 && savedAttachments < totalAttachments) {
message = getString(R.string.err_storage_attachment_some);
}
else {
message = getResources()
.getQuantityString(R.plurals.msg_storage_attachment_saved,
savedAttachments);
}

Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
}

private void openFile(CompositeMessage msg) {
AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);

Expand Down Expand Up @@ -1541,9 +1623,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
catch (Exception e) {
Log.w(TAG, "unable to publish photo to media store", e);
Toast.makeText(getContext(),
// TODO i18n
"Unable to save your photo to the gallery.", Toast.LENGTH_LONG).show();
Toast.makeText(getContext(), R.string.err_storage_photo_gallery,
Toast.LENGTH_LONG).show();
}
}

Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/org/kontalk/util/MediaStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public enum MediaStoreType {
private static final String DOCUMENTS_ROOT;
private static final String DOCUMENTS_PUBLIC_RELATIVE_PATH;
private static final File DOCUMENTS_PUBLIC_PATH;

static {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
DOCUMENTS_ROOT = Environment.DIRECTORY_DOCUMENTS;
Expand Down
14 changes: 11 additions & 3 deletions app/src/main/java/org/kontalk/util/Permissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,17 @@ public static void requestWriteExternalStorage(Activity activity, String rationa

@SuppressLint("InlinedApi")
public static void requestWriteExternalStorage(Fragment fragment, String rationale) {
EasyPermissions.requestPermissions(fragment, rationale, RC_WRITE_EXT_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (EasyPermissions.permissionPermanentlyDenied(fragment, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
new AppSettingsDialog.Builder(fragment)
.setRationale(rationale)
.build()
.show();
}
else {
EasyPermissions.requestPermissions(fragment, rationale, RC_WRITE_EXT_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}

public static boolean canUseCamera(Context context) {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/drawable-anydpi/ic_menu_save.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
Binary file added app/src/main/res/drawable-hdpi/ic_menu_save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-mdpi/ic_menu_save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-xhdpi/ic_menu_save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable-xxhdpi/ic_menu_save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions app/src/main/res/menu/compose_message_ctx.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@

<!-- actions -->
<item android:id="@+id/menu_delete" android:title="@string/delete_message" android:icon="@drawable/ic_menu_delete" app:showAsAction="ifRoom"/>

<!-- menu -->
<item android:id="@+id/menu_reply" android:title="@string/reply" android:icon="@drawable/ic_menu_reply" app:showAsAction="ifRoom"/>
<item android:id="@+id/menu_retry" android:title="@string/resend" android:icon="@drawable/ic_menu_send" app:showAsAction="ifRoom"/>
<item android:id="@+id/menu_share" android:title="@string/share" android:icon="@drawable/ic_menu_share" app:showAsAction="ifRoom"/>
<item android:id="@+id/menu_save_attachment" android:title="@string/save_attachment" android:icon="@drawable/ic_menu_save" app:showAsAction="ifRoom"/>

<!-- menu -->
<item android:id="@+id/menu_copy_text" android:title="@string/copy_message_text" android:icon="@drawable/ic_menu_copy" app:showAsAction="never"/>
<item android:id="@+id/menu_open" android:title="@string/open_file" android:icon="@drawable/ic_menu_open" app:showAsAction="never"/>
<item android:id="@+id/menu_download" android:title="@string/download_file" android:icon="@drawable/ic_menu_download" app:showAsAction="never"/>
Expand Down
12 changes: 10 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<string name="msg_authenticated">Registered to the network!</string>
<string name="share">Share</string>
<string name="resend">Send again</string>
<string name="save_attachment">Save attachment</string>
<string name="copy_message_text">Copy text</string>
<string name="delete_message">Delete message</string>
<string name="confirm_delete_all">Delete all chats</string>
Expand Down Expand Up @@ -687,8 +688,15 @@

<string name="err_audio_or_storage_denied">We need access to the microphone for recording your voice and access to external storage to save your recordings.</string>

<string name="err_storage_denied">Kontalk needs access to local storage to store media.\nYou can allow access to storage from Android settings.</string>
<string name="err_storage_denied_interactive">Kontalk needs access to local storage to store media. We will now ask for permission.</string>
<string name="err_storage_denied">Kontalk needs access to local storage to store media.</string>

<string name="err_storage_photo_gallery">Unable to save your photo to the gallery.</string>
<string name="err_storage_attachment_single">Unable to save attachment.</string>
<string name="err_storage_attachment_some">Some attachments were not saved due to errors.</string>
<plurals name="msg_storage_attachment_saved">
<item quantity="one">Attachment saved to external storage.</item>
<item quantity="other">Attachments saved to external storage.</item>
</plurals>

<string name="err_location_denied">We need access to your location if you want to share it.</string>

Expand Down

0 comments on commit 23143a7

Please sign in to comment.