From 7c56f3135985cd77baddac47f7d1f17abed9bb91 Mon Sep 17 00:00:00 2001
From: amine younes <amine.younes-bouacida@savoirfairelinux.com>
Date: Mon, 10 Jun 2019 11:34:25 -0400
Subject: [PATCH] conversation : add android SAF download

+ let the user download a file either on his machine or on the cloud
+ update the AndroidFileUtils function copyFiletoUri in order to use completable
+ update ConversationFragment, ConversationPresenter and ConversationAdapter in consequence
+ add new strings that inform the user that the file has been saved or an error occured

Change-Id: I0abcebd74b11e83b12a5e0f23faa97d66e1823eb
Gitlab: #584
---
 .gitignore                                    |  1 +
 .../cx/ring/adapters/ConversationAdapter.java |  7 +-
 .../cx/ring/client/ConversationActivity.java  |  4 +-
 .../ring/fragments/ConversationFragment.java  | 75 +++++++++++++------
 .../java/cx/ring/utils/AndroidFileUtils.java  | 38 +++++++++-
 .../app/src/main/res/values/strings.xml       |  3 +-
 .../conversation/ConversationPresenter.java   | 29 +++----
 .../ring/conversation/ConversationView.java   |  4 +-
 8 files changed, 104 insertions(+), 57 deletions(-)

diff --git a/.gitignore b/.gitignore
index ca6836599..6b91a094d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
 env.sh
 obj/
 android-toolchain-*/
+.idea/
diff --git a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
index 18f688351..99ea1786b 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.java
@@ -311,12 +311,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationViewHo
 
         switch (item.getItemId()) {
             case R.id.conv_action_download: {
-                File downloadDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Ring");
-                downloadDir.mkdirs();
-                File newFile = new File(downloadDir, ((DataTransfer) conversationElement).getDisplayName());
-                if (newFile.exists())
-                    newFile.delete();
-                presenter.downloadFile((DataTransfer) conversationElement, newFile);
+                presenter.saveFile((DataTransfer) conversationElement);
                 break;
             }
             case R.id.conv_action_share: {
diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
index 0c0187a4d..cec3c0126 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
@@ -23,13 +23,13 @@ package cx.ring.client;
 import android.content.Intent;
 import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.Menu;
 
 import androidx.annotation.ColorInt;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
-import android.view.KeyEvent;
-import android.view.Menu;
 
 import butterknife.BindView;
 import butterknife.ButterKnife;
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
index 3f8cc908b..ae76b6f0e 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
@@ -31,15 +31,6 @@ import android.content.pm.PackageManager;
 import android.graphics.drawable.BitmapDrawable;
 import android.os.Bundle;
 import android.provider.MediaStore;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import androidx.core.content.FileProvider;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
@@ -56,6 +47,13 @@ import android.widget.Toast;
 
 import com.google.android.material.snackbar.Snackbar;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.FileProvider;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
@@ -110,6 +108,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     private static final int REQUEST_CODE_FILE_PICKER = 1000;
     private static final int REQUEST_PERMISSION_CAMERA = 1001;
     private static final int REQUEST_CODE_TAKE_PICTURE = 1002;
+    private static final int REQUEST_CODE_SAVE_FILE = 1003;
 
     private FragConversationBinding binding;
     private MenuItem mAudioCallBtn = null;
@@ -123,6 +122,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     private SharedPreferences mPreferences;
 
     private File mCurrentPhoto = null;
+    private String mCurrentFileAbsolutePath = null;
     private Disposable actionbarTarget = null;
     private static int position;
 
@@ -379,6 +379,27 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             mCurrentPhoto = null;
             startFileSend(sendFile(file));
         }
+        // File download trough SAF
+        else if(requestCode == ConversationFragment.REQUEST_CODE_SAVE_FILE
+                && resultCode == RESULT_OK){
+            if(resultData != null && resultData.getData() != null ) {
+                //Get the Uri of the file that was created by the app that received our intent
+                android.net.Uri createdUri = resultData.getData();
+
+                //Try to copy the data of the current file into the newly created one
+                File input = new File(mCurrentFileAbsolutePath);
+                if(requireContext().getContentResolver() != null)
+                    AndroidFileUtils.copyFileToUri(
+                            requireContext().getContentResolver(),input,createdUri).
+                            observeOn(AndroidSchedulers.mainThread()).
+                            subscribe(()->Toast.makeText(getContext(), R.string.file_saved_successfully,
+                                    Toast.LENGTH_SHORT).show(),
+                                    error->Toast.makeText(getContext(), R.string.generic_error,
+                                            Toast.LENGTH_SHORT).show());
+
+            }
+        }
+
     }
 
     @Override
@@ -725,20 +746,6 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         }
     }
 
-    @Override
-    public void displayCompletedDownload(DataTransfer transfer, File destination) {
-        DownloadManager downloadManager = (DownloadManager) requireContext().getSystemService(Context.DOWNLOAD_SERVICE);
-        if (downloadManager != null) {
-            downloadManager.addCompletedDownload(transfer.getDisplayName(),
-                    transfer.getDisplayName(),
-                    true,
-                    AndroidFileUtils.getMimeType(destination.getAbsolutePath()),
-                    destination.getAbsolutePath(),
-                    destination.length(),
-                    true);
-        }
-    }
-
     public void handleShareIntent(Intent intent) {
         String type = intent.getType();
         if (type == null) {
@@ -757,4 +764,26 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
             startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri).flatMapCompletable(this::sendFile));
         }
     }
+
+    /**
+     * Creates an intent using Android Storage Access Framework
+     * This intent is then received by applications that can handle it like
+     * Downloads or Google drive
+     * @param file DataTransfer of the file that is going to be stored
+     * @param currentFileAbsolutePath absolute path of the file we want to save
+     */
+    public void startSaveFile(DataTransfer file, String currentFileAbsolutePath){
+        //Get the current file absolute path and store it
+        mCurrentFileAbsolutePath = currentFileAbsolutePath;
+
+        //Use Android Storage File Access to download the file
+        Intent downloadFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+        downloadFileIntent.setType(AndroidFileUtils.getMimeTypeFromExtension(file.getExtension()));
+
+        downloadFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
+        downloadFileIntent.putExtra(Intent.EXTRA_TITLE,file.getDisplayName());
+
+        startActivityForResult(downloadFileIntent, ConversationFragment.REQUEST_CODE_SAVE_FILE);
+    }
+
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
index e8b9c5760..473eb101f 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
@@ -182,15 +182,20 @@ public class AndroidFileUtils {
 
     public static String getMimeType(String filename) {
         int pos = filename.lastIndexOf(".");
+        String fileExtension = null;
         if (pos >= 0) {
-            String fileExtension = MimeTypeMap.getFileExtensionFromUrl(filename.substring(pos));
-            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase());
+            fileExtension = MimeTypeMap.getFileExtensionFromUrl(filename.substring(pos));
+        }
+        return getMimeTypeFromExtension(fileExtension);
+    }
+
+    public static String getMimeTypeFromExtension(String ext) {
+            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
             if (!TextUtils.isEmpty(mimeType))
                 return mimeType;
-            if (fileExtension.contentEquals("gz")) {
+            if (ext.contentEquals("gz")) {
                 return "application/gzip";
             }
-        }
         return "application/octet-stream";
     }
 
@@ -242,6 +247,31 @@ public class AndroidFileUtils {
         }).subscribeOn(Schedulers.io());
     }
 
+    /**
+     * Copies a file to a predefined Uri destination
+     * Uses the underlying copyFile(InputStream,OutputStream)
+     * @param cr content resolver
+     * @param input the file we want to copy
+     * @param outUri the uri destination
+     * @return success value
+     */
+    public static Completable copyFileToUri(ContentResolver cr, File input, Uri outUri){
+        return Completable.fromAction(() -> {
+            InputStream inputStream = null;
+            OutputStream outputStream = null;
+            try {
+                inputStream = new FileInputStream(input);
+                outputStream = cr.openOutputStream(outUri);
+                FileUtils.copyFile(inputStream, outputStream);
+            } finally {
+                if (outputStream != null)
+                    outputStream.close();
+                if (inputStream != null)
+                    inputStream.close();
+            }
+        }).subscribeOn(Schedulers.io());
+    }
+
     public static File getConversationFile(Context context, Uri uri, String conversationId, String name) throws IOException {
         File file = getConversationPath(context, conversationId, name);
         FileOutputStream output = new FileOutputStream(file);
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 38ff8327e..157733aba 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -275,10 +275,11 @@ along with this program; if not, write to the Free Software
     <string name="file_transfer_status_invalid_pathname">invalid pathname</string>
     <string name="file_transfer_status_unjoinable_peer">unjoinable peer</string>
     <string name="file_saved_in">File saved in %s</string>
+    <string name="file_saved_successfully">File saved successfully</string>
     <string name="no_space_left_on_device">No space left on device</string>
     <string name="title_media_viewer">Media viewer</string>
     <string name="menu_file_open">Open file</string>
-    <string name="menu_file_download">Download file</string>
+    <string name="menu_file_download">Save file</string>
     <string name="menu_file_delete">Delete file</string>
     <string name="menu_file_share">Share file</string>
     <string name="menu_message_copy">Copy</string>
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
index 9b66e9857..ead9ea809 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
@@ -232,25 +232,16 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mConversationFacade.sendFile(mAccountId, mContactRingId, file).subscribe();
     }
 
-    public void downloadFile(final DataTransfer transfer, final File dest) {
-        mCompositeDisposable.add(
-                Single.fromCallable(() -> {
-                    if (!transfer.isComplete())
-                        throw new IllegalStateException();
-                    File file = getDeviceRuntimeService().getConversationPath(transfer.getPeerId(), transfer.getStoragePath());
-                    if (FileUtils.copyFile(file, dest)) {
-                        Log.w(TAG, "Copied file to " + dest.getAbsolutePath() + " (" + FileUtils.readableFileSize(file.length()) + ")");
-                        return dest;
-                    }
-                    throw new IOException();
-                })
-                .subscribeOn(Schedulers.io())
-                .observeOn(mUiScheduler)
-                .subscribe(file -> {
-                    getView().displayCompletedDownload(transfer, file);
-                }, error -> {
-                    Log.e(TAG, "Can't download file " + dest, error);
-                }));
+    /**
+     * Gets the absolute path of the file dataTransfer and sends both the DataTransfer and the
+     * found path to the ConversationView in order to start saving the file
+     * @param transfer DataTransfer of the file
+     */
+    public void saveFile(DataTransfer transfer) {
+       String fileAbsolutePath =  getDeviceRuntimeService().
+                getConversationPath(transfer.getPeerId(), transfer.getStoragePath())
+               .getAbsolutePath();
+        getView().startSaveFile(transfer,fileAbsolutePath);
     }
 
     public void shareFile(DataTransfer file) {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
index 458f54bcd..801ed5fd9 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
@@ -43,8 +43,6 @@ public interface ConversationView extends BaseView {
 
     void displayErrorToast(int error);
 
-    void displayCompletedDownload(DataTransfer transfer, File destination);
-
     void hideNumberSpinner();
 
     void clearMsgEdit();
@@ -78,4 +76,6 @@ public interface ConversationView extends BaseView {
     void removeElement(ConversationElement e);
 
     void setConversationColor(int integer);
+
+    void startSaveFile(DataTransfer currentFile, String fileAbsolutePath);
 }
-- 
GitLab