From 205f3771d4a9f1142e0f225a407535ef5ee930bc Mon Sep 17 00:00:00 2001
From: Levi Bard <taktaktaktaktaktaktaktaktaktak@gmail.com>
Date: Sat, 23 Mar 2019 12:49:36 +0100
Subject: [PATCH] Add option to download all media for a toot with one tap.
 (#1121)

Addresses #966
---
 .../com/keylesspalace/tusky/BaseActivity.java | 45 ++++++++++++++++-
 .../keylesspalace/tusky/ViewMediaActivity.kt  | 49 +++++++------------
 .../tusky/fragment/SFragment.java             | 41 ++++++++++++++++
 .../tusky/interfaces/PermissionRequester.java |  5 ++
 app/src/main/res/menu/status_more.xml         |  3 ++
 app/src/main/res/values/strings.xml           |  2 +
 6 files changed, 113 insertions(+), 32 deletions(-)
 create mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java

diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
index 8f2a75e3..df069dc3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
@@ -19,11 +19,13 @@ import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.util.Log;
@@ -37,9 +39,11 @@ import com.keylesspalace.tusky.db.AccountEntity;
 import com.keylesspalace.tusky.db.AccountManager;
 import com.keylesspalace.tusky.di.Injectable;
 import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
+import com.keylesspalace.tusky.interfaces.PermissionRequester;
 import com.keylesspalace.tusky.util.ThemeUtils;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -48,6 +52,8 @@ import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
 import retrofit2.Call;
 
 public abstract class BaseActivity extends AppCompatActivity implements Injectable {
@@ -59,7 +65,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
     @Inject
     public AccountManager accountManager;
 
-    protected static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
+    protected static final int BUILD_VERSION_ANY = -1;
+    private static final int REQUESTER_NONE = Integer.MAX_VALUE;
+    private HashMap<Integer, PermissionRequester> requesters;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -98,6 +106,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
         }
 
         callList = new ArrayList<>();
+        requesters = new HashMap<>();
     }
 
     @Override
@@ -222,4 +231,38 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
             .setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
             .show();
     }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+        if (requesters.containsKey(requestCode)) {
+            PermissionRequester requester = requesters.remove(requestCode);
+            requester.onRequestPermissionsResult(permissions, grantResults);
+        }
+    }
+
+    public void requestPermissions(String[] permissions, int minimumBuildVersion, PermissionRequester requester) {
+        if (minimumBuildVersion == BUILD_VERSION_ANY || Build.VERSION.SDK_INT >= minimumBuildVersion) {
+            ArrayList<String> permissionsToRequest = new ArrayList<>();
+            for(String permission: permissions) {
+                if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+                    permissionsToRequest.add(permission);
+                }
+            }
+            if (permissionsToRequest.isEmpty()) {
+                int[] permissionsAlreadyGranted = new int[permissions.length];
+                for (int i = 0; i < permissionsAlreadyGranted.length; ++i)
+                    permissionsAlreadyGranted[i] = PackageManager.PERMISSION_GRANTED;
+                requester.onRequestPermissionsResult(permissions, permissionsAlreadyGranted);
+                return;
+            }
+
+            int newKey = requester == null ? REQUESTER_NONE : requesters.size();
+            if (newKey != REQUESTER_NONE) {
+                requesters.put(newKey, requester);
+            }
+            String[] permissionsCopy = new String[permissionsToRequest.size()];
+            permissionsToRequest.toArray(permissionsCopy);
+            ActivityCompat.requestPermissions(this, permissionsCopy, newKey);
+        }
+    }
 }
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
index a733b103..411ea10c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
@@ -31,8 +31,6 @@ import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.os.Environment
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
 import androidx.core.content.FileProvider
 import androidx.viewpager.widget.ViewPager
 import android.util.Log
@@ -142,7 +140,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
         toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
         toolbar.setOnMenuItemClickListener { item: MenuItem ->
             when (item.itemId) {
-                R.id.action_download -> downloadMedia()
+                R.id.action_download -> requestDownloadMedia()
                 R.id.action_open_status -> onOpenStatus()
                 R.id.action_share_media -> shareMedia()
                 R.id.action_copy_media_link -> copyLink()
@@ -188,36 +186,25 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
                 .start()
     }
 
-    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
-        when (requestCode) {
-            PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> {
-                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                    downloadMedia()
-                } else {
-                    showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { downloadMedia() }
-                }
-            }
-        }
+    private fun downloadMedia() {
+        val url = attachments!![viewPager.currentItem].attachment.url
+        val filename = Uri.parse(url).lastPathSegment
+        Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
+
+        val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+        val request = DownloadManager.Request(Uri.parse(url))
+        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
+                getString(R.string.app_name) + "/" + filename)
+        downloadManager.enqueue(request)
     }
 
-    private fun downloadMedia() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-                ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
-                != PackageManager.PERMISSION_GRANTED) {
-            ActivityCompat.requestPermissions(this,
-                    arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
-                    PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE)
-        } else {
-            val url = attachments!![viewPager.currentItem].attachment.url
-            val filename = Uri.parse(url).lastPathSegment
-            val toastText = String.format(resources.getString(R.string.download_image), filename)
-            Toast.makeText(applicationContext, toastText, Toast.LENGTH_SHORT).show()
-
-            val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
-            val request = DownloadManager.Request(Uri.parse(url))
-            request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
-                    getString(R.string.app_name) + "/" + filename)
-            downloadManager.enqueue(request)
+    private fun requestDownloadMedia() {
+        requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), Build.VERSION_CODES.M) { _, grantResults ->
+            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                downloadMedia()
+            } else {
+                showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() }
+            }
         }
     }
 
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index 88d613c5..be675571 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -15,15 +15,22 @@
 
 package com.keylesspalace.tusky.fragment;
 
+import android.Manifest;
+import android.app.DownloadManager;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.Intent;
 
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
 import android.text.Spanned;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
+import android.widget.Toast;
 
 import com.keylesspalace.tusky.BaseActivity;
 import com.keylesspalace.tusky.BottomSheetActivity;
@@ -69,6 +76,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
     protected abstract void onReblog(final boolean reblog, final int position);
 
     private BottomSheetActivity bottomSheetActivity;
+    private Status pendingDownloadStatus;
 
     @Inject
     public MastodonApi mastodonApi;
@@ -158,6 +166,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
         // Give a different menu depending on whether this is the user's own toot or not.
         if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) {
             popup.inflate(R.menu.status_more);
+            Menu menu = popup.getMenu();
+            menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty());
         } else {
             popup.inflate(R.menu.status_more_for_user);
             Menu menu = popup.getMenu();
@@ -236,6 +246,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
                     showOpenAsDialog(statusUrl, item.getTitle());
                     return true;
                 }
+                case R.id.status_download_media: {
+                    requestDownloadAllMedia(status);
+                    return true;
+                }
                 case R.id.status_mute: {
                     timelineCases.mute(accountId);
                     return true;
@@ -342,4 +356,31 @@ public abstract class SFragment extends BaseFragment implements Injectable {
         BaseActivity activity = (BaseActivity)getActivity();
         activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
     }
+
+    private void downloadAllMedia(Status status) {
+        pendingDownloadStatus = null;
+        Toast.makeText(getContext(), R.string.downloading_media, Toast.LENGTH_SHORT).show();
+        for(Attachment attachment: status.getAttachments()) {
+            String url = attachment.getUrl();
+            Uri uri = Uri.parse(url);
+            String filename = uri.getLastPathSegment();
+
+            DownloadManager downloadManager = (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
+            DownloadManager.Request request = new DownloadManager.Request(uri);
+            request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
+            downloadManager.enqueue(request);
+        }
+    }
+
+    private void requestDownloadAllMedia(Status status) {
+        pendingDownloadStatus = status;
+        String[] permissions = new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE };
+        ((BaseActivity)getActivity()).requestPermissions(permissions, Build.VERSION_CODES.M, (permissions1, grantResults) -> {
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                downloadAllMedia(status);
+            } else {
+                Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show();
+            }
+        });
+    }
 }
diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java
new file mode 100644
index 00000000..ca83e085
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java
@@ -0,0 +1,5 @@
+package com.keylesspalace.tusky.interfaces;
+
+public interface PermissionRequester {
+    void onRequestPermissionsResult(String[] permissions, int[] grantResults);
+}
\ No newline at end of file
diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml
index e9f78fc0..525ce90a 100644
--- a/app/src/main/res/menu/status_more.xml
+++ b/app/src/main/res/menu/status_more.xml
@@ -18,6 +18,9 @@
     <item
         android:id="@+id/status_open_as"
         android:title="@string/action_open_as" />
+    <item
+        android:id="@+id/status_download_media"
+        android:title="@string/download_media" />
     <item
         android:id="@+id/status_mute"
         android:title="@string/action_mute" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c70f15ad..19731fbd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -133,6 +133,8 @@
     <string name="action_copy_link">Copy the link</string>
     <string name="action_open_as">Open as %s</string>
     <string name="action_share_as">Share as …</string>
+    <string name="download_media">Download media</string>
+    <string name="downloading_media">Downloading media</string>
 
     <string name="send_status_link_to">Share toot URL to…</string>
     <string name="send_status_content_to">Share toot to…</string>