Merge pull request #324 from metaphore/external-file-access-fix

External Files Access Fix
This commit is contained in:
Niels Andriesse 2020-09-09 16:26:06 +10:00 committed by GitHub
commit 4dd89ba03f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 174 additions and 169 deletions

View File

@ -8,4 +8,6 @@
<external-path name="external_video" path="Movies"/> <external-path name="external_video" path="Movies"/>
<external-path name="external_audio" path="Music"/> <external-path name="external_audio" path="Music"/>
<external-path name="external_download" path="Download"/> <external-path name="external_download" path="Download"/>
<cache-path name="internal_cache" path="." />
</paths> </paths>

View File

@ -76,6 +76,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -278,7 +279,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@Override @Override
protected @Nullable BackupUtil.BackupInfo doInBackground(Void... voids) { protected @Nullable BackupUtil.BackupInfo doInBackground(Void... voids) {
try { try {
return BackupUtil.getLatestBackup(); return BackupUtil.getLatestBackup(RegistrationActivity.this);
} catch (NoExternalStorageException e) { } catch (NoExternalStorageException e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;

View File

@ -4,9 +4,11 @@ import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore; import android.provider.MediaStore;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -14,9 +16,9 @@ import androidx.core.content.ContextCompat;
import com.theartofdev.edmodo.cropper.CropImage; import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView; import com.theartofdev.edmodo.cropper.CropImageView;
import network.loki.messenger.R; import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.ExternalStorageUtil;
import org.thoughtcrime.securesms.util.FileProviderUtil; import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.IntentUtils;
@ -25,18 +27,20 @@ import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import network.loki.messenger.R;
import static android.provider.MediaStore.EXTRA_OUTPUT; import static android.provider.MediaStore.EXTRA_OUTPUT;
public final class AvatarSelection { public final class AvatarSelection {
private static final String TAG = AvatarSelection.class.getSimpleName(); private static final String TAG = AvatarSelection.class.getSimpleName();
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
private AvatarSelection() { private AvatarSelection() {
} }
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
/** /**
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE} * Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
*/ */
@ -63,16 +67,14 @@ public final class AvatarSelection {
* @return Temporary capture file if created. * @return Temporary capture file if created.
*/ */
public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) { public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) {
File captureFile = null; File captureFile = null;
boolean hasCameraPermission = ContextCompat
if (attemptToIncludeCamera) { .checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
if (Permissions.hasAll(activity, Manifest.permission.CAMERA)) { if (attemptToIncludeCamera && hasCameraPermission) {
try { try {
captureFile = File.createTempFile("capture", "jpg", activity.getExternalCacheDir()); captureFile = File.createTempFile("avatar-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
} catch (IOException e) { } catch (IOException | NoExternalStorageException e) {
Log.w(TAG, e); Log.e("Cannot reserve a temporary avatar capture file.", e);
captureFile = null;
}
} }
} }
@ -83,8 +85,7 @@ public final class AvatarSelection {
private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) { private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) {
List<Intent> extraIntents = new LinkedList<>(); List<Intent> extraIntents = new LinkedList<>();
Intent galleryIntent = new Intent(Intent.ACTION_PICK); Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
if (!IntentUtils.isResolvable(context, galleryIntent)) { if (!IntentUtils.isResolvable(context, galleryIntent)) {
@ -93,12 +94,11 @@ public final class AvatarSelection {
} }
if (tempCaptureFile != null) { if (tempCaptureFile != null) {
Uri uri = FileProviderUtil.getUriFor(context, tempCaptureFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(EXTRA_OUTPUT, uri);
if (cameraIntent.resolveActivity(context.getPackageManager()) != null) { cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
cameraIntent.putExtra(EXTRA_OUTPUT, FileProviderUtil.getUriFor(context, tempCaptureFile)); extraIntents.add(cameraIntent);
extraIntents.add(cameraIntent);
}
} }
if (includeClear) { if (includeClear) {

View File

@ -77,7 +77,7 @@ public class BackupDialog {
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> { .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
BackupPassphrase.set(context, null); BackupPassphrase.set(context, null);
TextSecurePreferences.setBackupEnabled(context, false); TextSecurePreferences.setBackupEnabled(context, false);
BackupUtil.deleteAllBackups(); BackupUtil.deleteAllBackups(context);
preference.setChecked(false); preference.setChecked(false);
}) })
.create() .create()

View File

@ -54,7 +54,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.ExternalStorageUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
@ -473,7 +473,7 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(fileName)); contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(fileName));
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings()); database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings());
} }
@ -724,7 +724,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(DIGEST, attachment.getDigest()); contentValues.put(DIGEST, attachment.getDigest());
contentValues.put(CONTENT_DISPOSITION, attachment.getKey()); contentValues.put(CONTENT_DISPOSITION, attachment.getKey());
contentValues.put(NAME, attachment.getRelay()); contentValues.put(NAME, attachment.getRelay());
contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(attachment.getFileName())); contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(attachment.getFileName()));
contentValues.put(SIZE, attachment.getSize()); contentValues.put(SIZE, attachment.getSize());
contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0); contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0);

View File

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.ExternalStorageUtil;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -71,7 +71,7 @@ public class LocalBackupJob extends BaseJob {
try { try {
String backupPassword = BackupPassphrase.get(context); String backupPassword = BackupPassphrase.get(context);
File backupDirectory = StorageUtil.getBackupDirectory(); File backupDirectory = ExternalStorageUtil.getBackupDir(context);
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date()); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
String fileName = String.format("session-%s.backup", timestamp); String fileName = String.format("session-%s.backup", timestamp);
File backupFile = new File(backupDirectory, fileName); File backupFile = new File(backupDirectory, fileName);
@ -84,7 +84,7 @@ public class LocalBackupJob extends BaseJob {
throw new IOException("Backup password is null"); throw new IOException("Backup password is null");
} }
File tempFile = File.createTempFile("backup", "tmp", StorageUtil.getBackupCacheDirectory(context)); File tempFile = File.createTempFile("backup", "tmp", ExternalStorageUtil.getCacheDir(context));
FullBackupExporter.export(context, FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
@ -97,7 +97,7 @@ public class LocalBackupJob extends BaseJob {
throw new IOException("Renaming temporary backup file failed!"); throw new IOException("Renaming temporary backup file failed!");
} }
BackupUtil.deleteOldBackups(); BackupUtil.deleteOldBackups(context);
} finally { } finally {
GenericForegroundService.stopForegroundTask(context); GenericForegroundService.stopForegroundTask(context);
} }

View File

@ -96,11 +96,11 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
return; return;
} }
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
try { try {
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination)); Util.copy(avatarStream, new FileOutputStream(decryptDestination));
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress())); decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.loki.activities package org.thoughtcrime.securesms.loki.activities
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapDecodingException
@ -148,6 +150,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
// endregion // endregion
// region Updating // region Updating
@ -241,7 +248,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
private fun showEditProfilePictureUI() { private fun showEditProfilePictureUI() {
tempFile = AvatarSelection.startAvatarSelection(this, false, true) // Ask for an optional camera permission.
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAnyResult {
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
}
.execute()
} }
private fun copyPublicKey() { private fun copyPublicKey() {

View File

@ -33,6 +33,11 @@ import java.util.Map;
/** /**
* Handles the retrieval of media present on the user's device. * 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 { class MediaRepository {

View File

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
@ -54,6 +55,8 @@ import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ExternalStorageUtil;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -65,6 +68,7 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
@ -73,6 +77,8 @@ import java.util.concurrent.ExecutionException;
import network.loki.messenger.R; import network.loki.messenger.R;
import static android.provider.MediaStore.EXTRA_OUTPUT;
public class AttachmentManager { public class AttachmentManager {
@ -112,7 +118,6 @@ public class AttachmentManager {
thumbnail.setOnClickListener(new ThumbnailClickListener()); thumbnail.setOnClickListener(new ThumbnailClickListener());
documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY); documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
} }
} }
public void clear(@NonNull GlideRequests glideRequests, boolean animate) { public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
@ -438,25 +443,29 @@ public class AttachmentManager {
public void capturePhoto(Activity activity, int requestCode) { public void capturePhoto(Activity activity, int requestCode) {
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.ifNecessary() .ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
.onAllGranted(() -> { .onAllGranted(() -> {
try { try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File captureFile = File.createTempFile(
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { "conversation-capture",
if (captureUri == null) { ".jpg",
captureUri = DeprecatedPersistentBlobProvider.getInstance(context).createForExternal(context, MediaUtil.IMAGE_JPEG); ExternalStorageUtil.getImageDir(activity));
} Uri captureUri = FileProviderUtil.getUriFor(context, captureFile);
Log.d(TAG, "captureUri path is " + captureUri.getPath()); Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri); captureIntent.putExtra(EXTRA_OUTPUT, captureUri);
activity.startActivityForResult(captureIntent, requestCode); captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
} catch (IOException ioe) { Log.d(TAG, "captureUri path is " + captureUri.getPath());
Log.w(TAG, ioe); this.captureUri = captureUri;
} activity.startActivityForResult(captureIntent, requestCode);
}) }
.execute(); } catch (IOException | NoExternalStorageException e) {
throw new RuntimeException("Error creating image capture intent.", e);
}
})
.execute();
} }
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {

View File

@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.ExternalStorageUtil;
public class DocumentSlide extends Slide { public class DocumentSlide extends Slide {
@ -19,7 +19,7 @@ public class DocumentSlide extends Slide {
@NonNull String contentType, long size, @NonNull String contentType, long size,
@Nullable String fileName) @Nullable String fileName)
{ {
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, false, false)); super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, ExternalStorageUtil.getCleanFileName(fileName), null, null, false, false));
} }
@Override @Override

View File

@ -171,7 +171,7 @@ public class Permissions {
} }
for (String permission : requestedPermissions) { for (String permission : requestedPermissions) {
request.addMapping(permission, permissionObject.shouldShouldPermissionRationale(permission)); request.addMapping(permission, permissionObject.shouldShowPermissionRationale(permission));
} }
permissionObject.requestPermissions(requestCode, requestedPermissions); permissionObject.requestPermissions(requestCode, requestedPermissions);
@ -240,7 +240,7 @@ public class Permissions {
for (int i=0;i<permissions.length;i++) { for (int i=0;i<permissions.length;i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
shouldShowRationaleDialog[i] = context.shouldShouldPermissionRationale(permissions[i]); shouldShowRationaleDialog[i] = context.shouldShowPermissionRationale(permissions[i]);
} }
} }
@ -259,7 +259,7 @@ public class Permissions {
private abstract static class PermissionObject { private abstract static class PermissionObject {
abstract Context getContext(); abstract Context getContext();
abstract boolean shouldShouldPermissionRationale(String permission); abstract boolean shouldShowPermissionRationale(String permission);
abstract boolean hasAll(String... permissions); abstract boolean hasAll(String... permissions);
abstract void requestPermissions(int requestCode, String... permissions); abstract void requestPermissions(int requestCode, String... permissions);
@ -287,7 +287,7 @@ public class Permissions {
} }
@Override @Override
public boolean shouldShouldPermissionRationale(String permission) { public boolean shouldShowPermissionRationale(String permission) {
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission); return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
} }
@ -316,7 +316,7 @@ public class Permissions {
} }
@Override @Override
public boolean shouldShouldPermissionRationale(String permission) { public boolean shouldShowPermissionRationale(String permission) {
return fragment.shouldShowRequestPermissionRationale(permission); return fragment.shouldShowRequestPermissionRationale(permission);
} }

View File

@ -22,7 +22,7 @@ public class BackupUtil {
public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) { public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
try { try {
BackupInfo backup = getLatestBackup(); BackupInfo backup = getLatestBackup(context);
if (backup == null) return context.getString(R.string.BackupUtil_never); if (backup == null) return context.getString(R.string.BackupUtil_never);
else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp()); else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp());
@ -32,8 +32,8 @@ public class BackupUtil {
} }
} }
public static @Nullable BackupInfo getLatestBackup() throws NoExternalStorageException { public static @Nullable BackupInfo getLatestBackup(Context context) throws NoExternalStorageException {
File backupDirectory = StorageUtil.getBackupDirectory(); File backupDirectory = ExternalStorageUtil.getBackupDir(context);
File[] backups = backupDirectory.listFiles(); File[] backups = backupDirectory.listFiles();
BackupInfo latestBackup = null; BackupInfo latestBackup = null;
@ -49,9 +49,9 @@ public class BackupUtil {
} }
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static void deleteAllBackups() { public static void deleteAllBackups(Context context) {
try { try {
File backupDirectory = StorageUtil.getBackupDirectory(); File backupDirectory = ExternalStorageUtil.getBackupDir(context);
File[] backups = backupDirectory.listFiles(); File[] backups = backupDirectory.listFiles();
for (File backup : backups) { for (File backup : backups) {
@ -62,9 +62,9 @@ public class BackupUtil {
} }
} }
public static void deleteOldBackups() { public static void deleteOldBackups(Context context) {
try { try {
File backupDirectory = StorageUtil.getBackupDirectory(); File backupDirectory = ExternalStorageUtil.getBackupDir(context);
File[] backups = backupDirectory.listFiles(); File[] backups = backupDirectory.listFiles();
if (backups != null && backups.length > 2) { if (backups != null && backups.length > 2) {

View File

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import java.io.File;
public class ExternalStorageUtil {
public static final String DIRECTORY_BACKUPS = "Backups";
/** @see Context#getExternalFilesDir(String) */
@NonNull
public static File getDir(Context context, @Nullable String type) throws NoExternalStorageException {
final File dir = context.getExternalFilesDir(type);
if (dir == null) {
throw new NoExternalStorageException("External storage dir is currently unavailable: " + type);
}
return dir;
}
@NonNull
public static File getBackupDir(Context context) throws NoExternalStorageException {
return getDir(context, DIRECTORY_BACKUPS);
}
@NonNull
public static File getVideoDir(Context context) throws NoExternalStorageException {
return getDir(context, Environment.DIRECTORY_MOVIES);
}
@NonNull
public static File getAudioDir(Context context) throws NoExternalStorageException {
return getDir(context, Environment.DIRECTORY_MUSIC);
}
@NonNull
public static File getImageDir(Context context) throws NoExternalStorageException {
return getDir(context, Environment.DIRECTORY_PICTURES);
}
@NonNull
public static File getDownloadDir(Context context) throws NoExternalStorageException {
return getDir(context, Environment.DIRECTORY_DOWNLOADS);
}
@Nullable
public static File getCacheDir(Context context) {
return context.getExternalCacheDir();
}
@Nullable
public static String getCleanFileName(@Nullable String fileName) {
if (fileName == null) return null;
fileName = fileName.replace('\u202D', '\uFFFD');
fileName = fileName.replace('\u202E', '\uFFFD');
return fileName;
}
}

View File

@ -1,18 +1,16 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ResolveInfo; import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import java.util.List; import androidx.annotation.NonNull;
public class IntentUtils { public class IntentUtils {
public static boolean isResolvable(@NonNull Context context, @NonNull Intent intent) { public static boolean isResolvable(@NonNull Context context, @NonNull Intent intent) {
List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(intent, 0); return context.getPackageManager()
return resolveInfoList != null && resolveInfoList.size() > 1; .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
} }
} }

View File

@ -59,10 +59,6 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
Context context = contextReference.get(); Context context = contextReference.get();
String directory = null; String directory = null;
if (!StorageUtil.canWriteInSessionStorageDir()) {
return new Pair<>(WRITE_ACCESS_FAILURE, null);
}
if (context == null) { if (context == null) {
return new Pair<>(FAILURE, null); return new Pair<>(FAILURE, null);
} }
@ -114,13 +110,13 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
File outputDirectory; File outputDirectory;
if (contentType.startsWith("video/")) { if (contentType.startsWith("video/")) {
outputDirectory = StorageUtil.getVideoDir(); outputDirectory = ExternalStorageUtil.getVideoDir(getContext());
} else if (contentType.startsWith("audio/")) { } else if (contentType.startsWith("audio/")) {
outputDirectory = StorageUtil.getAudioDir(); outputDirectory = ExternalStorageUtil.getAudioDir(getContext());
} else if (contentType.startsWith("image/")) { } else if (contentType.startsWith("image/")) {
outputDirectory = StorageUtil.getImageDir(); outputDirectory = ExternalStorageUtil.getImageDir(getContext());
} else { } else {
outputDirectory = StorageUtil.getDownloadDir(); outputDirectory = ExternalStorageUtil.getDownloadDir(getContext());
} }
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue"); if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");

View File

@ -1,86 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import java.io.File;
public class StorageUtil {
public static File getBackupDirectory() throws NoExternalStorageException {
File storage = Environment.getExternalStorageDirectory();
if (!storage.canWrite()) {
throw new NoExternalStorageException();
}
File session = new File(storage, "Session");
File backups = new File(session, "Backups");
if (!backups.exists()) {
if (!backups.mkdirs()) {
throw new NoExternalStorageException("Unable to create backup directory...");
}
}
return backups;
}
public static File getBackupCacheDirectory(Context context) {
return context.getExternalCacheDir();
}
private static File getSessionStorageDir() throws NoExternalStorageException {
final File storage = Environment.getExternalStorageDirectory();
if (!storage.canWrite()) {
throw new NoExternalStorageException();
}
return storage;
}
public static boolean canWriteInSessionStorageDir() {
File storage;
try {
storage = getSessionStorageDir();
} catch (NoExternalStorageException e) {
return false;
}
return storage.canWrite();
}
public static File getLegacyBackupDirectory() throws NoExternalStorageException {
return getSessionStorageDir();
}
public static File getVideoDir() throws NoExternalStorageException {
return new File(getSessionStorageDir(), Environment.DIRECTORY_MOVIES);
}
public static File getAudioDir() throws NoExternalStorageException {
return new File(getSessionStorageDir(), Environment.DIRECTORY_MUSIC);
}
public static File getImageDir() throws NoExternalStorageException {
return new File(getSessionStorageDir(), Environment.DIRECTORY_PICTURES);
}
public static File getDownloadDir() throws NoExternalStorageException {
return new File(getSessionStorageDir(), Environment.DIRECTORY_DOWNLOADS);
}
public static @Nullable String getCleanFileName(@Nullable String fileName) {
if (fileName == null) return null;
fileName = fileName.replace('\u202D', '\uFFFD');
fileName = fileName.replace('\u202E', '\uFFFD');
return fileName;
}
}