mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 04:25:18 +00:00
Merge pull request #335 from metaphore/store-permission-refactoring
File Storage Refactoring
This commit is contained in:
commit
2991bd37c7
Binary file not shown.
Before Width: | Height: | Size: 245 B |
Binary file not shown.
Before Width: | Height: | Size: 181 B |
Binary file not shown.
Before Width: | Height: | Size: 325 B |
Binary file not shown.
Before Width: | Height: | Size: 499 B |
Binary file not shown.
Before Width: | Height: | Size: 681 B |
10
res/drawable/ic_baseline_folder_24.xml
Normal file
10
res/drawable/ic_baseline_folder_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
|
||||
</vector>
|
@ -33,7 +33,8 @@
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:src="@drawable/ic_folder_white_48dp"/>
|
||||
android:tint="@android:color/white"
|
||||
android:src="@drawable/ic_baseline_folder_24"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediapicker_folder_item_title"
|
||||
|
@ -215,7 +215,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
|
||||
this.avatar.setOnClickListener(view -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(this::startAvatarSelection)
|
||||
.execute());
|
||||
|
||||
|
@ -98,7 +98,6 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
||||
.onAllGranted(() -> {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -324,47 +323,48 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint({"InlinedApi","StaticFieldLeak"})
|
||||
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
|
||||
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||
final Context context = getContext();
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
|
||||
R.string.MediaOverviewActivity_collecting_attachments,
|
||||
R.string.please_wait) {
|
||||
@Override
|
||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(
|
||||
context,
|
||||
R.string.MediaOverviewActivity_collecting_attachments,
|
||||
R.string.please_wait) {
|
||||
@Override
|
||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
||||
|
||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getFileName()));
|
||||
}
|
||||
}
|
||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
||||
super.onPostExecute(attachments);
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context,
|
||||
attachments.size());
|
||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
||||
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
|
||||
actionMode.finish();
|
||||
}
|
||||
}.execute();
|
||||
})
|
||||
.execute();
|
||||
@Override
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
||||
super.onPostExecute(attachments);
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
|
||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
||||
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
|
||||
actionMode.finish();
|
||||
}
|
||||
}.execute();
|
||||
})
|
||||
.execute();
|
||||
}, mediaRecords.size());
|
||||
}
|
||||
|
||||
|
@ -16,17 +16,16 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
@ -341,22 +340,23 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
@SuppressLint("InlinedApi")
|
||||
private void saveToDisk() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
if (mediaItem == null) return;
|
||||
|
||||
if (mediaItem != null) {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(
|
||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
|
@ -268,7 +268,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeBackupDetection() {
|
||||
if (!Permissions.hasAll(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
if (!Permissions.hasAll(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
|
||||
return;
|
||||
}
|
||||
|
@ -172,7 +172,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
|
@ -164,7 +164,6 @@ public class WebRtcCallActivity extends Activity {
|
||||
if (event != null) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString()),
|
||||
R.drawable.ic_mic_white_48dp, R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
|
@ -110,7 +110,7 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
||||
updateHeight();
|
||||
|
||||
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
recentRail.setVisibility(View.VISIBLE);
|
||||
loaderManager.restartLoader(1, null, recentRail);
|
||||
} else {
|
||||
|
@ -2564,7 +2564,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onRecorderPermissionRequired() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
|
||||
.execute();
|
||||
@ -2765,7 +2764,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onClick(View v) {
|
||||
Permissions.with(ConversationActivity.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
|
@ -85,6 +85,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@ -658,23 +659,30 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
||||
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
|
||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
||||
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
|
||||
.toList();
|
||||
if (!Util.isEmpty(attachments)) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
return;
|
||||
}
|
||||
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
List<SaveAttachmentTask.Attachment> attachments =
|
||||
Stream.of(message.getSlideDeck().getSlides())
|
||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
||||
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
|
||||
.toList();
|
||||
if (!Util.isEmpty(attachments)) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "No slide with attachable media found, failing nicely.");
|
||||
Toast.makeText(getActivity(),
|
||||
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
Log.w(TAG, "No slide with attachable media found, failing nicely.");
|
||||
Toast.makeText(getActivity(),
|
||||
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
||||
Toast.LENGTH_LONG).show();
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
if (Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
PROJECTION, null, null,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
|
||||
|
@ -26,6 +26,7 @@ import java.util.Locale;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API.
|
||||
public class LocalBackupJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "LocalBackupJob";
|
||||
|
@ -251,7 +251,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
// Ask for an optional camera permission.
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAnyResult {
|
||||
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
|
||||
}
|
||||
|
@ -12,14 +12,12 @@ public class MediaFolder {
|
||||
private final String title;
|
||||
private final int itemCount;
|
||||
private final String bucketId;
|
||||
private final FolderType folderType;
|
||||
|
||||
MediaFolder(@NonNull Uri thumbnailUri, @NonNull String title, int itemCount, @NonNull String bucketId, @NonNull FolderType folderType) {
|
||||
MediaFolder(@NonNull Uri thumbnailUri, @NonNull String title, int itemCount, @NonNull String bucketId) {
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
this.title = title;
|
||||
this.itemCount = itemCount;
|
||||
this.bucketId = bucketId;
|
||||
this.folderType = folderType;
|
||||
}
|
||||
|
||||
Uri getThumbnailUri() {
|
||||
@ -38,10 +36,6 @@ public class MediaFolder {
|
||||
return bucketId;
|
||||
}
|
||||
|
||||
FolderType getFolderType() {
|
||||
return folderType;
|
||||
}
|
||||
|
||||
enum FolderType {
|
||||
NORMAL, CAMERA
|
||||
}
|
||||
|
@ -75,7 +75,6 @@ class MediaPickerFolderAdapter extends RecyclerView.Adapter<MediaPickerFolderAda
|
||||
void bind(@NonNull MediaFolder folder, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
title.setText(folder.getTitle());
|
||||
count.setText(String.valueOf(folder.getItemCount()));
|
||||
icon.setImageResource(folder.getFolderType() == MediaFolder.FolderType.CAMERA ? R.drawable.ic_camera_alt_white_24dp : R.drawable.ic_folder_white_48dp);
|
||||
|
||||
glideRequests.load(folder.getThumbnailUri())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
|
@ -5,7 +5,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore.Images;
|
||||
import android.provider.MediaStore.Video;
|
||||
import android.provider.OpenableColumns;
|
||||
@ -33,11 +32,6 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handles the retrieval of media present on the user's device.
|
||||
* @deprecated Usage of this class is unsafe on Android API 30 and up,
|
||||
* the public external directory is no longer exposed to the apps.
|
||||
* <p><b>
|
||||
* The functionality of this class should be refactored to use
|
||||
* <a href="https://developer.android.com/reference/android/provider/MediaStore">MediaStore</a>.
|
||||
*/
|
||||
class MediaRepository {
|
||||
|
||||
@ -82,30 +76,17 @@ class MediaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
String cameraBucketId = imageFolders.getCameraBucketId() != null ? imageFolders.getCameraBucketId() : videoFolders.getCameraBucketId();
|
||||
FolderData cameraFolder = cameraBucketId != null ? folders.remove(cameraBucketId) : null;
|
||||
List<MediaFolder> mediaFolders = Stream.of(folders.values()).map(folder -> new MediaFolder(folder.getThumbnail(),
|
||||
List<MediaFolder> mediaFolders = Stream.of(folders.values()).map(folder -> new MediaFolder(folder.getThumbnail(),
|
||||
folder.getTitle(),
|
||||
folder.getCount(),
|
||||
folder.getBucketId(),
|
||||
MediaFolder.FolderType.NORMAL))
|
||||
folder.getBucketId()))
|
||||
.sorted((o1, o2) -> o1.getTitle().toLowerCase().compareTo(o2.getTitle().toLowerCase()))
|
||||
.toList();
|
||||
|
||||
Uri allMediaThumbnail = imageFolders.getThumbnailTimestamp() > videoFolders.getThumbnailTimestamp() ? imageFolders.getThumbnail() : videoFolders.getThumbnail();
|
||||
|
||||
if (allMediaThumbnail != null) {
|
||||
int allMediaCount = Stream.of(mediaFolders).reduce(0, (count, folder) -> count + folder.getItemCount());
|
||||
|
||||
if (cameraFolder != null) {
|
||||
allMediaCount += cameraFolder.getCount();
|
||||
}
|
||||
|
||||
mediaFolders.add(0, new MediaFolder(allMediaThumbnail, context.getString(R.string.MediaRepository_all_media), allMediaCount, Media.ALL_MEDIA_BUCKET_ID, MediaFolder.FolderType.NORMAL));
|
||||
}
|
||||
|
||||
if (cameraFolder != null) {
|
||||
mediaFolders.add(0, new MediaFolder(cameraFolder.getThumbnail(), cameraFolder.getTitle(), cameraFolder.getCount(), cameraFolder.getBucketId(), MediaFolder.FolderType.CAMERA));
|
||||
mediaFolders.add(0, new MediaFolder(allMediaThumbnail, context.getString(R.string.MediaRepository_all_media), allMediaCount, Media.ALL_MEDIA_BUCKET_ID));
|
||||
}
|
||||
|
||||
return mediaFolders;
|
||||
@ -113,8 +94,6 @@ class MediaRepository {
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) {
|
||||
String cameraPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + "Camera";
|
||||
String cameraBucketId = null;
|
||||
Uri globalThumbnail = null;
|
||||
long thumbnailTimestamp = 0;
|
||||
Map<String, FolderData> folders = new HashMap<>();
|
||||
@ -135,10 +114,6 @@ class MediaRepository {
|
||||
folder.incrementCount();
|
||||
folders.put(bucketId, folder);
|
||||
|
||||
if (cameraBucketId == null && path.startsWith(cameraPath)) {
|
||||
cameraBucketId = bucketId;
|
||||
}
|
||||
|
||||
if (timestamp > thumbnailTimestamp) {
|
||||
globalThumbnail = thumbnail;
|
||||
thumbnailTimestamp = timestamp;
|
||||
@ -146,7 +121,7 @@ class MediaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
return new FolderResult(cameraBucketId, globalThumbnail, thumbnailTimestamp, folders);
|
||||
return new FolderResult(globalThumbnail, thumbnailTimestamp, folders);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@ -163,7 +138,8 @@ class MediaRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrienation) {
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) {
|
||||
//TODO Constrain media file size to match the Loki protocol limit.
|
||||
List<Media> media = new LinkedList<>();
|
||||
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
||||
String[] selectionArgs = new String[] { bucketId };
|
||||
@ -171,7 +147,7 @@ class MediaRepository {
|
||||
|
||||
String[] projection;
|
||||
|
||||
if (hasOrienation) {
|
||||
if (hasOrientation) {
|
||||
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
} else {
|
||||
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
@ -187,7 +163,7 @@ class MediaRepository {
|
||||
Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(Images.Media._ID)));
|
||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN));
|
||||
int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
|
||||
@ -284,26 +260,19 @@ class MediaRepository {
|
||||
}
|
||||
|
||||
private static class FolderResult {
|
||||
private final String cameraBucketId;
|
||||
private final Uri thumbnail;
|
||||
private final long thumbnailTimestamp;
|
||||
private final Map<String, FolderData> folderData;
|
||||
|
||||
private FolderResult(@Nullable String cameraBucketId,
|
||||
@Nullable Uri thumbnail,
|
||||
private FolderResult(@Nullable Uri thumbnail,
|
||||
long thumbnailTimestamp,
|
||||
@NonNull Map<String, FolderData> folderData)
|
||||
{
|
||||
this.cameraBucketId = cameraBucketId;
|
||||
this.thumbnail = thumbnail;
|
||||
this.thumbnailTimestamp = thumbnailTimestamp;
|
||||
this.folderData = folderData;
|
||||
}
|
||||
|
||||
@Nullable String getCameraBucketId() {
|
||||
return cameraBucketId;
|
||||
}
|
||||
|
||||
@Nullable Uri getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.Manifest;
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -133,7 +135,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
||||
cameraButton = findViewById(R.id.mediasend_camera_button);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||
|
||||
@ -375,7 +377,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
private void navigateToCamera() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
|
@ -371,37 +371,25 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public static void selectDocument(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
|
||||
.execute();
|
||||
selectMediaType(activity, "*/*", null, requestCode);
|
||||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||
// .onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body, transport), requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectAudio(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
|
||||
.execute();
|
||||
selectMediaType(activity, "audio/*", null, requestCode);
|
||||
}
|
||||
|
||||
public static void selectContactInfo(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
||||
@ -414,7 +402,6 @@ public class AttachmentManager {
|
||||
/* Loki - Enable again once we have location sharing
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
@ -444,7 +431,6 @@ public class AttachmentManager {
|
||||
public void capturePhoto(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
@ -469,6 +455,7 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||
//TODO Constrain media file size to match the Loki protocol limit.
|
||||
final Intent intent = new Intent();
|
||||
intent.setType(type);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.permissions;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
@ -63,9 +62,8 @@ public class Permissions {
|
||||
private @DrawableRes int[] rationalDialogHeader;
|
||||
private String rationaleDialogMessage;
|
||||
|
||||
private boolean ifNecesary;
|
||||
|
||||
private boolean condition = true;
|
||||
private int minSdkVersion = 0;
|
||||
private int maxSdkVersion = Integer.MAX_VALUE;
|
||||
|
||||
PermissionsBuilder(PermissionObject permissionObject) {
|
||||
this.permissionObject = permissionObject;
|
||||
@ -76,17 +74,6 @@ public class Permissions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder ifNecessary() {
|
||||
this.ifNecesary = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder ifNecessary(boolean condition) {
|
||||
this.ifNecesary = true;
|
||||
this.condition = condition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
|
||||
this.rationalDialogHeader = headers;
|
||||
this.rationaleDialogMessage = message;
|
||||
@ -133,11 +120,29 @@ public class Permissions {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Min Android SDK version to request the permissions for (inclusive).
|
||||
*/
|
||||
public PermissionsBuilder minSdkVersion(int minSdkVersion) {
|
||||
this.minSdkVersion = minSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max Android SDK version to request the permissions for (inclusive).
|
||||
*/
|
||||
public PermissionsBuilder maxSdkVersion(int maxSdkVersion) {
|
||||
this.maxSdkVersion = maxSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener,
|
||||
someGrantedListener, someDeniedListener, somePermanentlyDeniedListener);
|
||||
|
||||
if (ifNecesary && (permissionObject.hasAll(requestedPermissions) || !condition)) {
|
||||
boolean targetSdk = Build.VERSION.SDK_INT >= minSdkVersion && Build.VERSION.SDK_INT <= maxSdkVersion;
|
||||
|
||||
if (!targetSdk || permissionObject.hasAll(requestedPermissions)) {
|
||||
executePreGrantedPermissionsRequest(request);
|
||||
} else if (rationaleDialogMessage != null && rationalDialogHeader != null) {
|
||||
executePermissionsRequestWithRationale(request);
|
||||
|
@ -139,7 +139,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!((SwitchPreferenceCompat)preference).isChecked()) {
|
||||
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
@ -160,7 +159,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
Log.i(TAG, "Queing backup...");
|
||||
ApplicationContext.getInstance(getContext())
|
||||
|
@ -55,7 +55,6 @@ public class WelcomeActivity extends BaseActionBarActivity {
|
||||
private void onContinueClicked() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.activity_landing_permission_dialog_message), R.drawable.ic_baseline_folder_48)
|
||||
.onAnyResult(() -> {
|
||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||
|
@ -16,6 +16,7 @@ import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API.
|
||||
public class BackupUtil {
|
||||
|
||||
private static final String TAG = BackupUtil.class.getSimpleName();
|
||||
|
@ -35,8 +35,7 @@ public class CommunicationActions {
|
||||
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.toShortString()),
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera),
|
||||
R.drawable.ic_mic_white_48dp,
|
||||
R.drawable.ic_videocam_white_48dp)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.toShortString()))
|
||||
|
@ -1,234 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> {
|
||||
private static final String TAG = SaveAttachmentTask.class.getSimpleName();
|
||||
|
||||
static final int SUCCESS = 0;
|
||||
private static final int FAILURE = 1;
|
||||
private static final int WRITE_ACCESS_FAILURE = 2;
|
||||
|
||||
private final WeakReference<Context> contextReference;
|
||||
|
||||
private final int attachmentCount;
|
||||
|
||||
public SaveAttachmentTask(Context context) {
|
||||
this(context, 1);
|
||||
}
|
||||
|
||||
public SaveAttachmentTask(Context context, int count) {
|
||||
super(context,
|
||||
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
|
||||
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
|
||||
this.contextReference = new WeakReference<>(context);
|
||||
this.attachmentCount = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pair<Integer, String> doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||
if (attachments == null || attachments.length == 0) {
|
||||
throw new AssertionError("must pass in at least one attachment");
|
||||
}
|
||||
|
||||
try {
|
||||
Context context = contextReference.get();
|
||||
String directory = null;
|
||||
|
||||
if (context == null) {
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
if (attachment != null) {
|
||||
directory = saveAttachment(context, attachment);
|
||||
if (directory == null) return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
|
||||
else return new Pair<>(SUCCESS, directory);
|
||||
} catch (NoExternalStorageException|IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String saveAttachment(Context context, Attachment attachment)
|
||||
throws NoExternalStorageException, IOException
|
||||
{
|
||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||
String fileName = attachment.fileName;
|
||||
|
||||
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
|
||||
fileName = sanitizeOutputFileName(fileName);
|
||||
|
||||
File outputDirectory = createOutputDirectoryFromContentType(contentType);
|
||||
File mediaFile = createOutputFile(outputDirectory, fileName);
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri);
|
||||
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
OutputStream outputStream = new FileOutputStream(mediaFile);
|
||||
Util.copy(inputStream, outputStream);
|
||||
|
||||
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
|
||||
new String[]{contentType}, null);
|
||||
|
||||
return outputDirectory.getName();
|
||||
}
|
||||
|
||||
private File createOutputDirectoryFromContentType(@NonNull String contentType)
|
||||
throws NoExternalStorageException
|
||||
{
|
||||
File outputDirectory;
|
||||
|
||||
if (contentType.startsWith("video/")) {
|
||||
outputDirectory = ExternalStorageUtil.getVideoDir(getContext());
|
||||
} else if (contentType.startsWith("audio/")) {
|
||||
outputDirectory = ExternalStorageUtil.getAudioDir(getContext());
|
||||
} else if (contentType.startsWith("image/")) {
|
||||
outputDirectory = ExternalStorageUtil.getImageDir(getContext());
|
||||
} else {
|
||||
outputDirectory = ExternalStorageUtil.getDownloadDir(getContext());
|
||||
}
|
||||
|
||||
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
|
||||
return outputDirectory;
|
||||
}
|
||||
|
||||
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
|
||||
String base = "signal-" + dateFormatter.format(timestamp);
|
||||
|
||||
if (extension == null) extension = "attach";
|
||||
|
||||
return base + "." + extension;
|
||||
}
|
||||
|
||||
private String sanitizeOutputFileName(@NonNull String fileName) {
|
||||
return new File(fileName).getName();
|
||||
}
|
||||
|
||||
private File createOutputFile(@NonNull File outputDirectory, @NonNull String fileName)
|
||||
throws IOException
|
||||
{
|
||||
String[] fileParts = getFileNameParts(fileName);
|
||||
String base = fileParts[0];
|
||||
String extension = fileParts[1];
|
||||
|
||||
File outputFile = new File(outputDirectory, base + "." + extension);
|
||||
|
||||
int i = 0;
|
||||
while (outputFile.exists()) {
|
||||
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
||||
}
|
||||
|
||||
if (outputFile.isHidden()) {
|
||||
throw new IOException("Specified name would not be visible");
|
||||
}
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
private String[] getFileNameParts(String fileName) {
|
||||
String[] result = new String[2];
|
||||
String[] tokens = fileName.split("\\.(?=[^\\.]+$)");
|
||||
|
||||
result[0] = tokens[0];
|
||||
|
||||
if (tokens.length > 1) result[1] = tokens[1];
|
||||
else result[1] = "";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Pair<Integer, String> result) {
|
||||
super.onPostExecute(result);
|
||||
final Context context = contextReference.get();
|
||||
if (context == null) return;
|
||||
|
||||
switch (result.first()) {
|
||||
case FAILURE:
|
||||
Toast.makeText(context,
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
||||
attachmentCount),
|
||||
Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case SUCCESS:
|
||||
String message = !TextUtils.isEmpty(result.second()) ? context.getResources().getString(R.string.SaveAttachmentTask_saved_to, result.second())
|
||||
: context.getResources().getString(R.string.SaveAttachmentTask_saved);
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case WRITE_ACCESS_FAILURE:
|
||||
Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation,
|
||||
Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Attachment {
|
||||
public Uri uri;
|
||||
public String fileName;
|
||||
public String contentType;
|
||||
public long date;
|
||||
|
||||
public Attachment(@NonNull Uri uri, @NonNull String contentType,
|
||||
long date, @Nullable String fileName)
|
||||
{
|
||||
if (uri == null || contentType == null || date < 0) {
|
||||
throw new AssertionError("uri, content type, and date must all be specified");
|
||||
}
|
||||
this.uri = uri;
|
||||
this.fileName = fileName;
|
||||
this.contentType = contentType;
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Context context, OnClickListener onAcceptListener) {
|
||||
showWarningDialog(context, onAcceptListener, 1);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Context context, OnClickListener onAcceptListener, int count) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.ConversationFragment_save_to_sd_card);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setCancelable(true);
|
||||
builder.setMessage(context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
|
||||
count, count));
|
||||
builder.setPositiveButton(R.string.yes, onAcceptListener);
|
||||
builder.setNegativeButton(R.string.no, null);
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
196
src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
Normal file
196
src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt
Normal file
@ -0,0 +1,196 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.text.TextUtils
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/**
|
||||
* Saves attachment files to an external storage using [MediaStore] API.
|
||||
*/
|
||||
class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Int, String?>> {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val TAG = SaveAttachmentTask::class.simpleName
|
||||
|
||||
private const val RESULT_SUCCESS = 0
|
||||
private const val RESULT_FAILURE = 1
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun showWarningDialog(context: Context, onAcceptListener: OnClickListener, count: Int = 1) {
|
||||
val builder = AlertDialog.Builder(context)
|
||||
builder.setTitle(R.string.ConversationFragment_save_to_sd_card)
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
builder.setCancelable(true)
|
||||
builder.setMessage(context.resources.getQuantityString(
|
||||
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
|
||||
count,
|
||||
count))
|
||||
builder.setPositiveButton(R.string.yes, onAcceptListener)
|
||||
builder.setNegativeButton(R.string.no, null)
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private val contextReference: WeakReference<Context>
|
||||
private val attachmentCount: Int
|
||||
|
||||
@JvmOverloads
|
||||
constructor(context: Context, count: Int = 1): super(context,
|
||||
context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
|
||||
context.resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)) {
|
||||
this.contextReference = WeakReference(context)
|
||||
this.attachmentCount = count
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg attachments: Attachment?): Pair<Int, String?> {
|
||||
if (attachments.isEmpty()) {
|
||||
throw IllegalArgumentException("Must pass in at least one attachment")
|
||||
}
|
||||
|
||||
try {
|
||||
val context = contextReference.get()
|
||||
var directory: String? = null
|
||||
|
||||
if (context == null) {
|
||||
return Pair(RESULT_FAILURE, null)
|
||||
}
|
||||
|
||||
for (attachment in attachments) {
|
||||
if (attachment != null) {
|
||||
directory = saveAttachment(context, attachment)
|
||||
if (directory == null) return Pair(RESULT_FAILURE, null)
|
||||
}
|
||||
}
|
||||
|
||||
return if (attachments.size > 1)
|
||||
Pair(RESULT_SUCCESS, null)
|
||||
else
|
||||
Pair(RESULT_SUCCESS, directory)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
return Pair(RESULT_FAILURE, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun saveAttachment(context: Context, attachment: Attachment): String? {
|
||||
val resolver = context.contentResolver
|
||||
|
||||
val contentType = MediaUtil.getCorrectedMimeType(attachment.contentType)!!
|
||||
val fileName = attachment.fileName
|
||||
?: sanitizeOutputFileName(generateOutputFileName(contentType, attachment.date))
|
||||
|
||||
val mediaRecord = ContentValues()
|
||||
val mediaVolume = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
MediaStore.VOLUME_EXTERNAL
|
||||
} else {
|
||||
MediaStore.VOLUME_EXTERNAL_PRIMARY
|
||||
}
|
||||
val collectionUri: Uri
|
||||
|
||||
when {
|
||||
contentType.startsWith("video/") -> {
|
||||
collectionUri = MediaStore.Video.Media.getContentUri(mediaVolume)
|
||||
mediaRecord.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
|
||||
mediaRecord.put(MediaStore.Video.Media.MIME_TYPE, contentType)
|
||||
// Add the date meta data to ensure the image is added at the front of the gallery
|
||||
mediaRecord.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
mediaRecord.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
|
||||
}
|
||||
contentType.startsWith("audio/") -> {
|
||||
collectionUri = MediaStore.Audio.Media.getContentUri(mediaVolume)
|
||||
mediaRecord.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName)
|
||||
mediaRecord.put(MediaStore.Audio.Media.MIME_TYPE, contentType)
|
||||
mediaRecord.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
mediaRecord.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
|
||||
}
|
||||
contentType.startsWith("image/") -> {
|
||||
collectionUri = MediaStore.Images.Media.getContentUri(mediaVolume)
|
||||
mediaRecord.put(MediaStore.Images.Media.TITLE, fileName)
|
||||
mediaRecord.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
|
||||
mediaRecord.put(MediaStore.Images.Media.MIME_TYPE, contentType)
|
||||
mediaRecord.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||
mediaRecord.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
|
||||
}
|
||||
else -> {
|
||||
mediaRecord.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName)
|
||||
collectionUri = MediaStore.Files.getContentUri(mediaVolume)
|
||||
}
|
||||
}
|
||||
|
||||
val mediaFileUri = resolver.insert(collectionUri, mediaRecord)
|
||||
if (mediaFileUri == null) return null
|
||||
|
||||
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)
|
||||
if (inputStream == null) return null
|
||||
|
||||
inputStream.use {
|
||||
resolver.openOutputStream(mediaFileUri).use {
|
||||
Util.copy(inputStream, it)
|
||||
}
|
||||
}
|
||||
|
||||
return mediaFileUri.toString()
|
||||
}
|
||||
|
||||
private fun generateOutputFileName(contentType: String, timestamp: Long): String {
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
val extension = mimeTypeMap.getExtensionFromMimeType(contentType) ?: "attach"
|
||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd-HHmmss")
|
||||
val base = "signal-${dateFormatter.format(timestamp)}"
|
||||
|
||||
return "${base}.${extension}";
|
||||
}
|
||||
|
||||
private fun sanitizeOutputFileName(fileName: String): String {
|
||||
return File(fileName).name
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Pair<Int, String?>) {
|
||||
super.onPostExecute(result)
|
||||
val context = contextReference.get()
|
||||
if (context == null) return
|
||||
|
||||
when (result.first) {
|
||||
RESULT_FAILURE -> {
|
||||
val message = context.resources.getQuantityText(
|
||||
R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
||||
attachmentCount)
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
RESULT_SUCCESS -> {
|
||||
val message = if (!TextUtils.isEmpty(result.second)) {
|
||||
context.resources.getString(R.string.SaveAttachmentTask_saved_to, result.second)
|
||||
} else {
|
||||
context.resources.getString(R.string.SaveAttachmentTask_saved)
|
||||
}
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected result value: " + result.first)
|
||||
}
|
||||
}
|
||||
|
||||
data class Attachment(val uri: Uri, val contentType: String, val date: Long, val fileName: String?)
|
||||
}
|
@ -4,6 +4,8 @@ import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
||||
@ -13,14 +15,14 @@ public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends
|
||||
private final String title;
|
||||
private final String message;
|
||||
|
||||
public ProgressDialogAsyncTask(Context context, String title, String message) {
|
||||
public ProgressDialogAsyncTask(@NonNull Context context, @NonNull String title, @NonNull String message) {
|
||||
super();
|
||||
this.contextReference = new WeakReference<>(context);
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ProgressDialogAsyncTask(Context context, int title, int message) {
|
||||
public ProgressDialogAsyncTask(@NonNull Context context, int title, int message) {
|
||||
this(context, context.getString(title), context.getString(message));
|
||||
}
|
||||
|
||||
@ -35,7 +37,7 @@ public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends
|
||||
if (progress != null) progress.dismiss();
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
protected @NonNull Context getContext() {
|
||||
return contextReference.get();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user