diff --git a/.gitignore b/.gitignore index ca6836599226eb0b8c3fc91b81320c143443829f..6b91a094ddc598f42be58e047b484adc972f9a56 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 18f688351fb4801527da0df2ad50d9f8fa05fc70..99ea1786b16c64ab4e9908ba086f46948273aa49 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 0c0187a4df45a29ec9a0b2b0b0e12288f782da78..cec3c01262d27678403db8d4bbe26f31cbd7272a 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 3f8cc908b100ebf93e732962d6f70a8fcec13097..ae76b6f0e7727487d4847d6b15341ccdc1bb3752 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 e8b9c576022b8da3ddaf9b0e7caa52ec4a565987..473eb101ff4688c2bacaafa8ccff4a4abc096554 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 38ff8327e0d68f981f3746eb86dadc04425d3621..157733aba31867e7ff4d47795701f6ff84ace9ad 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 9b66e985755786ab79bcb36a24c3c801a165a84e..ead9ea8091115345e98c58f21c4ac3bc98265faa 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 458f54bcdf7a2b90523b6eef083d60ba307936a5..801ed5fd996de2c710149985b55899c292f27737 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); }