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