mirror of
https://github.com/deltachat/deltachat-android.git
synced 2025-10-03 09:49:21 +02:00
Target SDK/API 30 (#2087)
* Target SDK 30 * Pull changes from Signal * Bugfix, make photo view rail be shown again * Fix capturing images using an external camera * Make backups work. Unfortunately, I did this by copying the backup file to the private storage first. * Fix: Show the correct folder name when exporting attachment Before, after exporting an attachment, on newer Android versions (and maybe also on older ones) the toast always said `File exported to "media"`. * Update src/org/thoughtcrime/securesms/WelcomeActivity.java Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu> Co-authored-by: bjoern <r10s@b44t.com> Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
This commit is contained in:
parent
cc7bfbac51
commit
f34ef4db13
17 changed files with 476 additions and 155 deletions
|
@ -45,7 +45,6 @@
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
tools:ignore="GoogleAppIndexingWarning"
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- android car support, see https://developer.android.com/training/auto/start/,
|
<!-- android car support, see https://developer.android.com/training/auto/start/,
|
||||||
|
@ -123,10 +122,14 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
|
<!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
|
||||||
<data android:scheme="openpgp4fpr" />
|
<data android:scheme="openpgp4fpr" />
|
||||||
<data android:scheme="OPENPGP4FPR" />
|
<data android:scheme="OPENPGP4FPR"
|
||||||
<data android:scheme="OpenPGP4FPR" />
|
tools:ignore="AppLinkUrlError" />
|
||||||
<data android:scheme="OpenPGP4Fpr" />
|
<data android:scheme="OpenPGP4FPR"
|
||||||
<data android:scheme="OpenPGP4fpr" />
|
tools:ignore="AppLinkUrlError" />
|
||||||
|
<data android:scheme="OpenPGP4Fpr"
|
||||||
|
tools:ignore="AppLinkUrlError" />
|
||||||
|
<data android:scheme="OpenPGP4fpr"
|
||||||
|
tools:ignore="AppLinkUrlError" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||||
|
@ -345,6 +348,11 @@
|
||||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
||||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="632.0dip" />
|
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="632.0dip" />
|
||||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="598.0dip" />
|
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="598.0dip" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -10,7 +10,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ dependencies {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
flavorDimensions "none"
|
flavorDimensions "none"
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
dexOptions {
|
dexOptions {
|
||||||
|
@ -106,7 +106,7 @@ android {
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
org.gradle.jvmargs=-Xmx4608m
|
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Tue Jul 09 18:28:04 CEST 2019
|
#Wed Oct 27 11:37:14 CEST 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -26,10 +26,8 @@ import android.os.Build;
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
@ -38,14 +36,15 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.b44t.messenger.DcContext;
|
import com.b44t.messenger.DcContext;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.Prefs;
|
import org.thoughtcrime.securesms.util.Prefs;
|
||||||
import org.thoughtcrime.securesms.util.Scrubber;
|
import org.thoughtcrime.securesms.util.Scrubber;
|
||||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
|
@ -116,7 +115,7 @@ public class LogViewFragment extends Fragment {
|
||||||
String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt";
|
String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
outputDir = StorageUtil.getDownloadDir();
|
outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
String logText = logPreview.getText().toString();
|
String logText = logPreview.getText().toString();
|
||||||
if(!logText.trim().equals("")){
|
if(!logText.trim().equals("")){
|
||||||
File logFile = new File(outputDir + "/" + logFileName);
|
File logFile = new File(outputDir + "/" + logFileName);
|
||||||
|
@ -128,7 +127,7 @@ public class LogViewFragment extends Fragment {
|
||||||
logFileBufferWriter.write(logText);
|
logFileBufferWriter.write(logText);
|
||||||
logFileBufferWriter.close();
|
logFileBufferWriter.close();
|
||||||
}
|
}
|
||||||
} catch (IOException | NoExternalStorageException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,14 +45,6 @@ import androidx.loader.content.Loader;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.b44t.messenger.DcChat;
|
import com.b44t.messenger.DcChat;
|
||||||
import com.b44t.messenger.DcContext;
|
import com.b44t.messenger.DcContext;
|
||||||
import com.b44t.messenger.DcMediaGalleryElement;
|
import com.b44t.messenger.DcMediaGalleryElement;
|
||||||
|
@ -73,6 +65,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -300,20 +293,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||||
|
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||||
|
if (StorageUtil.canWriteToMediaStore(this)) {
|
||||||
|
performSavetoDisk(mediaItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
performSavetoDisk(mediaItem);
|
||||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
|
||||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
private void showInChat() {
|
private void showInChat() {
|
||||||
MediaItem mediaItem = getCurrentMediaItem();
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
if (mediaItem == null || mediaItem.msgId == DcMsg.DC_MSG_NO_ID) {
|
if (mediaItem == null || mediaItem.msgId == DcMsg.DC_MSG_NO_ID) {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.view.ActionMode;
|
import androidx.appcompat.view.ActionMode;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -75,26 +76,33 @@ public abstract class MessageSelectorFragment
|
||||||
|
|
||||||
protected void handleSaveAttachment(final Set<DcMsg> messageRecords) {
|
protected void handleSaveAttachment(final Set<DcMsg> messageRecords) {
|
||||||
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
|
SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> {
|
||||||
Permissions.with(getActivity())
|
if (StorageUtil.canWriteToMediaStore(getContext())) {
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
performSave(messageRecords);
|
||||||
.ifNecessary()
|
return;
|
||||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
}
|
||||||
.onAllGranted(() -> {
|
|
||||||
SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[messageRecords.size()];
|
Permissions.with(getActivity())
|
||||||
int index = 0;
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
for (DcMsg message : messageRecords) {
|
.ifNecessary()
|
||||||
attachments[index] = new SaveAttachmentTask.Attachment(
|
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
Uri.fromFile(message.getFileAsFile()), message.getFilemime(), message.getDateReceived(), message.getFilename());
|
.onAllGranted(() -> performSave(messageRecords))
|
||||||
index++;
|
.execute();
|
||||||
}
|
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getContext());
|
|
||||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments);
|
|
||||||
if (actionMode != null) actionMode.finish();
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void performSave(Set<DcMsg> messageRecords) {
|
||||||
|
SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[messageRecords.size()];
|
||||||
|
int index = 0;
|
||||||
|
for (DcMsg message : messageRecords) {
|
||||||
|
attachments[index] = new SaveAttachmentTask.Attachment(
|
||||||
|
Uri.fromFile(message.getFileAsFile()), message.getFilemime(), message.getDateReceived(), message.getFilename());
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
SaveAttachmentTask saveTask = new SaveAttachmentTask(getContext());
|
||||||
|
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments);
|
||||||
|
if (actionMode != null) actionMode.finish();
|
||||||
|
}
|
||||||
|
|
||||||
protected void handleShowInChat(final DcMsg dcMsg) {
|
protected void handleShowInChat(final DcMsg dcMsg) {
|
||||||
Intent intent = new Intent(getContext(), ConversationActivity.class);
|
Intent intent = new Intent(getContext(), ConversationActivity.class);
|
||||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId());
|
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId());
|
||||||
|
|
|
@ -5,14 +5,18 @@ import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import com.b44t.messenger.DcContext;
|
import com.b44t.messenger.DcContext;
|
||||||
|
@ -24,6 +28,7 @@ import com.google.zxing.integration.android.IntentResult;
|
||||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||||
|
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.qr.RegistrationQrActivity;
|
import org.thoughtcrime.securesms.qr.RegistrationQrActivity;
|
||||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||||
|
@ -31,12 +36,22 @@ import org.thoughtcrime.securesms.service.NotificationController;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.StreamUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class WelcomeActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
|
public class WelcomeActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
|
||||||
public static final String QR_ACCOUNT_EXTRA = "qr_account_extra";
|
public static final String QR_ACCOUNT_EXTRA = "qr_account_extra";
|
||||||
|
public static final int PICK_BACKUP = 20574;
|
||||||
|
private final static String TAG = WelcomeActivity.class.getSimpleName();
|
||||||
|
public static final String TMP_BACKUP_FILE = "tmp-backup-file";
|
||||||
|
|
||||||
private boolean manualConfigure = true; // false: configure by QR account creation
|
private boolean manualConfigure = true; // false: configure by QR account creation
|
||||||
private ProgressDialog progressDialog = null;
|
private ProgressDialog progressDialog = null;
|
||||||
|
@ -106,29 +121,31 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
File imexDir = DcHelper.getImexDir();
|
File imexDir = DcHelper.getImexDir();
|
||||||
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
if (backupFile != null) {
|
AttachmentManager.selectMediaType(this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
|
||||||
new AlertDialog.Builder(this)
|
} else {
|
||||||
.setTitle(R.string.import_backup_title)
|
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
|
||||||
.setMessage(String.format(getResources().getString(R.string.import_backup_ask), backupFile))
|
if (backupFile != null) {
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
new AlertDialog.Builder(this)
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
.setTitle(R.string.import_backup_title)
|
||||||
startImport(backupFile);
|
.setMessage(String.format(getResources().getString(R.string.import_backup_ask), backupFile))
|
||||||
})
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show();
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> startImport(backupFile, null))
|
||||||
}
|
.show();
|
||||||
else {
|
}
|
||||||
new AlertDialog.Builder(this)
|
else {
|
||||||
.setTitle(R.string.import_backup_title)
|
new AlertDialog.Builder(this)
|
||||||
.setMessage(String.format(getResources().getString(R.string.import_backup_no_backup_found), imexDir.getAbsolutePath()))
|
.setTitle(R.string.import_backup_title)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setMessage(String.format(getResources().getString(R.string.import_backup_no_backup_found), imexDir.getAbsolutePath()))
|
||||||
.show();
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startImport(final String backupFile)
|
private void startImport(@Nullable final String backupFile, final @Nullable Uri backupFileUri)
|
||||||
{
|
{
|
||||||
notificationController = GenericForegroundService.startForegroundTask(this, getString(R.string.import_backup_title));
|
notificationController = GenericForegroundService.startForegroundTask(this, getString(R.string.import_backup_title));
|
||||||
if( progressDialog!=null ) {
|
if( progressDialog!=null ) {
|
||||||
|
@ -143,11 +160,36 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||||
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> {
|
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> {
|
||||||
dcContext.stopOngoingProcess();
|
dcContext.stopOngoingProcess();
|
||||||
notificationController.close();
|
notificationController.close();
|
||||||
|
cleanupTempBackupFile();
|
||||||
});
|
});
|
||||||
progressDialog.show();
|
progressDialog.show();
|
||||||
|
|
||||||
DcHelper.getEventCenter(this).captureNextError();
|
Util.runOnBackground(() -> {
|
||||||
dcContext.imex(DcContext.DC_IMEX_IMPORT_BACKUP, backupFile);
|
String file = backupFile;
|
||||||
|
if (backupFile == null) {
|
||||||
|
try {
|
||||||
|
file = copyToCacheDir(backupFileUri).getAbsolutePath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
notificationController.close();
|
||||||
|
cleanupTempBackupFile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DcHelper.getEventCenter(this).captureNextError();
|
||||||
|
dcContext.imex(DcContext.DC_IMEX_IMPORT_BACKUP, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private File copyToCacheDir(Uri uri) throws IOException {
|
||||||
|
try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
|
||||||
|
File file = File.createTempFile(TMP_BACKUP_FILE, ".tmp", getCacheDir());
|
||||||
|
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
StreamUtil.copy(inputStream, outputStream);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startQrAccountCreation(String qrCode)
|
private void startQrAccountCreation(String qrCode)
|
||||||
|
@ -230,6 +272,7 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||||
if (progress==0/*error/aborted*/) {
|
if (progress==0/*error/aborted*/) {
|
||||||
progressError(DcHelper.getEventCenter(this).getCapturedError());
|
progressError(DcHelper.getEventCenter(this).getCapturedError());
|
||||||
notificationController.close();
|
notificationController.close();
|
||||||
|
cleanupTempBackupFile();
|
||||||
}
|
}
|
||||||
else if (progress<1000/*progress in permille*/) {
|
else if (progress<1000/*progress in permille*/) {
|
||||||
progressUpdate((int)progress);
|
progressUpdate((int)progress);
|
||||||
|
@ -239,6 +282,7 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||||
DcHelper.getAccounts(this).startIo();
|
DcHelper.getAccounts(this).startIo();
|
||||||
progressSuccess(false);
|
progressSuccess(false);
|
||||||
notificationController.close();
|
notificationController.close();
|
||||||
|
cleanupTempBackupFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (manualConfigure && eventId==DcContext.DC_EVENT_CONFIGURE_PROGRESS) {
|
else if (manualConfigure && eventId==DcContext.DC_EVENT_CONFIGURE_PROGRESS) {
|
||||||
|
@ -291,6 +335,27 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||||
.show();
|
.show();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else if (requestCode == PICK_BACKUP) {
|
||||||
|
Uri uri = (data != null ? data.getData() : null);
|
||||||
|
if (uri == null) {
|
||||||
|
Log.e(TAG, " Can't import null URI");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startImport(null, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupTempBackupFile() {
|
||||||
|
try {
|
||||||
|
File[] files = getCacheDir().listFiles((dir, name) -> name.startsWith(TMP_BACKUP_FILE));
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.getName().endsWith("tmp")) {
|
||||||
|
Log.i(TAG, "Deleting temp backup file " + file);
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
||||||
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||||
recentRail.setVisibility(View.VISIBLE);
|
recentRail.setVisibility(View.VISIBLE);
|
||||||
loaderManager.restartLoader(1, null, recentRail);
|
loaderManager.restartLoader(1, null, recentRail);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -80,7 +80,7 @@ public class AvatarSelector extends PopupWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
||||||
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||||
recentRail.setVisibility(View.VISIBLE);
|
recentRail.setVisibility(View.VISIBLE);
|
||||||
loaderManager.restartLoader(1, null, recentRail);
|
loaderManager.restartLoader(1, null, recentRail);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
|
@ -13,12 +21,6 @@ import androidx.loader.content.Loader;
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import com.bumptech.glide.load.Key;
|
import com.bumptech.glide.load.Key;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
@ -102,13 +104,13 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||||
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||||
viewHolder.imageView.setImageDrawable(null);
|
viewHolder.imageView.setImageDrawable(null);
|
||||||
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
|
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
|
||||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
|
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
|
||||||
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
||||||
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
|
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
|
||||||
int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION));
|
int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION));
|
||||||
|
|
||||||
final Uri uri = Uri.withAppendedPath(baseUri, Long.toString(id));
|
final Uri uri = ContentUris.withAppendedId(RecentPhotosLoader.BASE_URL, rowId);
|
||||||
|
|
||||||
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,9 @@ import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
import androidx.loader.content.CursorLoader;
|
import androidx.loader.content.CursorLoader;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
@ -22,6 +24,9 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||||
MediaStore.Images.ImageColumns.MIME_TYPE
|
MediaStore.Images.ImageColumns.MIME_TYPE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static final String SELECTION = Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1"
|
||||||
|
: MediaStore.Images.Media.DATA + " IS NULL";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
public RecentPhotosLoader(Context context) {
|
public RecentPhotosLoader(Context context) {
|
||||||
|
@ -33,7 +38,7 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||||
public Cursor loadInBackground() {
|
public Cursor loadInBackground() {
|
||||||
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
PROJECTION, null, null,
|
PROJECTION, SELECTION, null,
|
||||||
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
|
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.graphics.PorterDuff;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
|
@ -383,17 +385,12 @@ public class AttachmentManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectDocument(Activity activity, int requestCode) {
|
public static void selectDocument(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
selectMediaType(activity, "*/*", null, requestCode);
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
.ifNecessary()
|
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
|
||||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode) {
|
public static void selectGallery(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||||
|
@ -402,7 +399,7 @@ public class AttachmentManager {
|
||||||
|
|
||||||
public static void selectImage(Activity activity, int requestCode) {
|
public static void selectImage(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode))
|
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode))
|
||||||
|
@ -411,7 +408,7 @@ public class AttachmentManager {
|
||||||
|
|
||||||
public static void selectAudio(Activity activity, int requestCode) {
|
public static void selectAudio(Activity activity, int requestCode) {
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied))
|
||||||
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
|
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
|
||||||
|
@ -511,7 +508,11 @@ public class AttachmentManager {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
public static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||||
|
selectMediaType(activity, type, extraMimeType, requestCode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode, @Nullable Uri initialUri) {
|
||||||
final Intent intent = new Intent();
|
final Intent intent = new Intent();
|
||||||
intent.setType(type);
|
intent.setType(type);
|
||||||
|
|
||||||
|
@ -519,6 +520,10 @@ public class AttachmentManager {
|
||||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeType);
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialUri != null && Build.VERSION.SDK_INT >= 26) {
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -4,14 +4,15 @@ import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.media.MediaMetadataRetriever;
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import com.b44t.messenger.DcMsg;
|
import com.b44t.messenger.DcMsg;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||||
|
@ -45,6 +46,7 @@ public class MediaUtil {
|
||||||
public static final String AUDIO_AAC = "audio/aac";
|
public static final String AUDIO_AAC = "audio/aac";
|
||||||
public static final String AUDIO_UNSPECIFIED = "audio/*";
|
public static final String AUDIO_UNSPECIFIED = "audio/*";
|
||||||
public static final String VIDEO_UNSPECIFIED = "video/*";
|
public static final String VIDEO_UNSPECIFIED = "video/*";
|
||||||
|
public static final String OCTET = "application/octet-stream";
|
||||||
|
|
||||||
|
|
||||||
public static Slide getSlideForMsg(Context context, DcMsg dcMsg) {
|
public static Slide getSlideForMsg(Context context, DcMsg dcMsg) {
|
||||||
|
@ -194,6 +196,18 @@ public class MediaUtil {
|
||||||
return (null != contentType) && contentType.startsWith("video/");
|
return (null != contentType) && contentType.startsWith("video/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isOctetStream(@Nullable String contentType) {
|
||||||
|
return OCTET.equals(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isImageOrVideoType(String contentType) {
|
||||||
|
return isImageType(contentType) || isVideoType(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isImageVideoOrAudioType(String contentType) {
|
||||||
|
return isImageOrVideoType(contentType) || isAudioType(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
public static class ThumbnailSize {
|
public static class ThumbnailSize {
|
||||||
public ThumbnailSize(int width, int height) {
|
public ThumbnailSize(int width, int height) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.util.Log;
|
import androidx.loader.content.CursorLoader;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
|
|
||||||
|
@ -22,8 +30,12 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, String>> {
|
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, Uri>> {
|
||||||
private static final String TAG = SaveAttachmentTask.class.getSimpleName();
|
private static final String TAG = SaveAttachmentTask.class.getSimpleName();
|
||||||
|
|
||||||
static final int SUCCESS = 0;
|
static final int SUCCESS = 0;
|
||||||
|
@ -40,16 +52,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Pair<Integer, String> doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
protected Pair<Integer, Uri> doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||||
if (attachments == null || attachments.length == 0) {
|
if (attachments == null || attachments.length == 0) {
|
||||||
throw new AssertionError("must pass in at least one attachment");
|
throw new AssertionError("must pass in at least one attachment");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Context context = contextReference.get();
|
Context context = contextReference.get();
|
||||||
String directory = null;
|
Uri uri = null;
|
||||||
|
|
||||||
if (!StorageUtil.canWriteInSignalStorageDir()) {
|
if (!StorageUtil.canWriteToMediaStore(context)) {
|
||||||
return new Pair<>(WRITE_ACCESS_FAILURE, null);
|
return new Pair<>(WRITE_ACCESS_FAILURE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,61 +71,142 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||||
|
|
||||||
for (Attachment attachment : attachments) {
|
for (Attachment attachment : attachments) {
|
||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
directory = saveAttachment(context, attachment);
|
uri = saveAttachment(context, attachment);
|
||||||
if (directory == null) return new Pair<>(FAILURE, null);
|
if (uri == null) return new Pair<>(FAILURE, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
|
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
|
||||||
else return new Pair<>(SUCCESS, directory);
|
else return new Pair<>(SUCCESS, uri);
|
||||||
} catch (NoExternalStorageException|IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
Log.w(TAG, ioe);
|
Log.w(TAG, ioe);
|
||||||
return new Pair<>(FAILURE, null);
|
return new Pair<>(FAILURE, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String saveAttachment(Context context, Attachment attachment)
|
private @Nullable Uri saveAttachment(Context context, Attachment attachment) throws IOException
|
||||||
throws NoExternalStorageException, IOException
|
|
||||||
{
|
{
|
||||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
String contentType = Objects.requireNonNull(MediaUtil.getCorrectedMimeType(attachment.contentType));
|
||||||
String fileName = attachment.fileName;
|
String fileName = attachment.fileName;
|
||||||
|
|
||||||
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
|
if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date);
|
||||||
fileName = sanitizeOutputFileName(fileName);
|
fileName = sanitizeOutputFileName(fileName);
|
||||||
|
|
||||||
File outputDirectory = createOutputDirectoryFromContentType(contentType);
|
Uri outputUri = getMediaStoreContentUriForType(contentType);
|
||||||
File mediaFile = createOutputFile(outputDirectory, fileName);
|
Uri mediaUri = createOutputUri(outputUri, contentType, fileName);
|
||||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri);
|
ContentValues updateValues = new ContentValues();
|
||||||
|
|
||||||
if (inputStream == null) {
|
if (mediaUri == null) {
|
||||||
|
Log.w(TAG, "Failed to create mediaUri for " + contentType);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputStream outputStream = new FileOutputStream(mediaFile);
|
try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)) {
|
||||||
Util.copy(inputStream, outputStream);
|
|
||||||
|
|
||||||
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
|
if (inputStream == null) {
|
||||||
new String[]{contentType}, null);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return outputDirectory.getName();
|
if (Util.equals(outputUri.getScheme(), ContentResolver.SCHEME_FILE)) {
|
||||||
}
|
try (OutputStream outputStream = new FileOutputStream(mediaUri.getPath())) {
|
||||||
|
StreamUtil.copy(inputStream, outputStream);
|
||||||
private File createOutputDirectoryFromContentType(@NonNull String contentType)
|
MediaScannerConnection.scanFile(context, new String[]{mediaUri.getPath()}, new String[]{contentType}, null);
|
||||||
throws NoExternalStorageException
|
}
|
||||||
{
|
} else {
|
||||||
File outputDirectory;
|
try (OutputStream outputStream = context.getContentResolver().openOutputStream(mediaUri, "w")) {
|
||||||
|
long total = StreamUtil.copy(inputStream, outputStream);
|
||||||
if (contentType.startsWith("video/")) {
|
if (total > 0) {
|
||||||
outputDirectory = StorageUtil.getVideoDir();
|
updateValues.put(MediaStore.MediaColumns.SIZE, total);
|
||||||
} else if (contentType.startsWith("audio/")) {
|
}
|
||||||
outputDirectory = StorageUtil.getAudioDir();
|
}
|
||||||
} else if (contentType.startsWith("image/")) {
|
}
|
||||||
outputDirectory = StorageUtil.getImageDir();
|
|
||||||
} else {
|
|
||||||
outputDirectory = StorageUtil.getDownloadDir();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
return outputDirectory;
|
updateValues.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateValues.size() > 0) {
|
||||||
|
getContext().getContentResolver().update(mediaUri, updateValues, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String getRealPathFromURI(Uri contentUri) {
|
||||||
|
String[] proj = {MediaStore.MediaColumns.DATA};
|
||||||
|
CursorLoader loader = new CursorLoader(getContext(), contentUri, proj, null, null, null);
|
||||||
|
Cursor cursor = loader.loadInBackground();
|
||||||
|
int column_index = 0;
|
||||||
|
String result = null;
|
||||||
|
if (cursor != null) {
|
||||||
|
column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||||
|
cursor.moveToFirst();
|
||||||
|
result = cursor.getString(column_index);
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull Uri getMediaStoreContentUriForType(@NonNull String contentType) {
|
||||||
|
if (contentType.startsWith("video/")) {
|
||||||
|
return StorageUtil.getVideoUri();
|
||||||
|
} else if (contentType.startsWith("audio/")) {
|
||||||
|
return StorageUtil.getAudioUri();
|
||||||
|
} else if (contentType.startsWith("image/")) {
|
||||||
|
return StorageUtil.getImageUri();
|
||||||
|
} else {
|
||||||
|
return StorageUtil.getDownloadUri();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable File ensureExternalPath(@Nullable File path) {
|
||||||
|
if (path != null && path.exists()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path == null) {
|
||||||
|
File documents = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
if (documents.exists() || documents.mkdirs()) {
|
||||||
|
return documents;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.mkdirs()) {
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a path to a shared media (or documents) directory for the type of the file.
|
||||||
|
*
|
||||||
|
* Note that this method attempts to create a directory if the path returned from
|
||||||
|
* Environment object does not exist yet. The attempt may fail in which case it attempts
|
||||||
|
* to return the default "Document" path. It finally returns null if it also fails.
|
||||||
|
* Otherwise it returns the absolute path to the directory.
|
||||||
|
*
|
||||||
|
* @param contentType a MIME type of a file
|
||||||
|
* @return an absolute path to a directory or null
|
||||||
|
*/
|
||||||
|
private @Nullable String getExternalPathForType(String contentType) {
|
||||||
|
File storage = null;
|
||||||
|
if (contentType.startsWith("video/")) {
|
||||||
|
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||||
|
} else if (contentType.startsWith("audio/")) {
|
||||||
|
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||||
|
} else if (contentType.startsWith("image/")) {
|
||||||
|
storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||||
|
}
|
||||||
|
|
||||||
|
storage = ensureExternalPath(storage);
|
||||||
|
if (storage == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage.getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
|
private String generateOutputFileName(@NonNull String contentType, long timestamp) {
|
||||||
|
@ -130,25 +223,73 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||||
return new File(fileName).getName();
|
return new File(fileName).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
private File createOutputFile(@NonNull File outputDirectory, @NonNull String fileName)
|
private @Nullable Uri createOutputUri(@NonNull Uri outputUri, @NonNull String contentType, @NonNull String fileName)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
String[] fileParts = getFileNameParts(fileName);
|
String[] fileParts = getFileNameParts(fileName);
|
||||||
String base = fileParts[0];
|
String base = fileParts[0];
|
||||||
String extension = fileParts[1];
|
String extension = fileParts[1];
|
||||||
|
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
|
||||||
File outputFile = new File(outputDirectory, base + "." + extension);
|
if (MediaUtil.isOctetStream(mimeType) && MediaUtil.isImageVideoOrAudioType(contentType)) {
|
||||||
|
Log.d(TAG, "MimeTypeMap returned octet stream for media, changing to provided content type [" + contentType + "] instead.");
|
||||||
int i = 0;
|
mimeType = contentType;
|
||||||
while (outputFile.exists()) {
|
|
||||||
outputFile = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFile.isHidden()) {
|
ContentValues contentValues = new ContentValues();
|
||||||
throw new IOException("Specified name would not be visible");
|
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||||
|
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
|
||||||
|
contentValues.put(MediaStore.MediaColumns.DATE_ADDED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
|
||||||
|
contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT > 28) {
|
||||||
|
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||||
|
} else if (Util.equals(outputUri.getScheme(), ContentResolver.SCHEME_FILE)) {
|
||||||
|
File outputDirectory = new File(outputUri.getPath());
|
||||||
|
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 Uri.fromFile(outputFile);
|
||||||
|
} else {
|
||||||
|
String dir = getExternalPathForType(contentType);
|
||||||
|
if (dir == null) {
|
||||||
|
throw new IOException(String.format(Locale.US, "Path for type: %s was not available", contentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
String outputFileName = fileName;
|
||||||
|
String dataPath = String.format("%s/%s", dir, outputFileName);
|
||||||
|
int i = 0;
|
||||||
|
while (pathTaken(outputUri, dataPath)) {
|
||||||
|
Log.d(TAG, "The content exists. Rename and check again.");
|
||||||
|
outputFileName = base + "-" + (++i) + "." + extension;
|
||||||
|
dataPath = String.format("%s/%s", dir, outputFileName);
|
||||||
|
}
|
||||||
|
contentValues.put(MediaStore.MediaColumns.DATA, dataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFile;
|
return getContext().getContentResolver().insert(outputUri, contentValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pathTaken(@NonNull Uri outputUri, @NonNull String dataPath) throws IOException {
|
||||||
|
try (Cursor cursor = getContext().getContentResolver().query(outputUri,
|
||||||
|
new String[] { MediaStore.MediaColumns.DATA },
|
||||||
|
MediaStore.MediaColumns.DATA + " = ?",
|
||||||
|
new String[] { dataPath },
|
||||||
|
null))
|
||||||
|
{
|
||||||
|
if (cursor == null) {
|
||||||
|
throw new IOException("Something is wrong with the filename to save");
|
||||||
|
}
|
||||||
|
return cursor.moveToFirst();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getFileNameParts(String fileName) {
|
private String[] getFileNameParts(String fileName) {
|
||||||
|
@ -164,7 +305,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(final Pair<Integer, String> result) {
|
protected void onPostExecute(final Pair<Integer, Uri> result) {
|
||||||
super.onPostExecute(result);
|
super.onPostExecute(result);
|
||||||
final Context context = contextReference.get();
|
final Context context = contextReference.get();
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
|
@ -176,7 +317,19 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
break;
|
break;
|
||||||
case SUCCESS:
|
case SUCCESS:
|
||||||
String dir = result.second();
|
Uri uri = result.second();
|
||||||
|
String dir;
|
||||||
|
|
||||||
|
String path = getRealPathFromURI(uri);
|
||||||
|
if (path != null) uri = Uri.parse(path);
|
||||||
|
|
||||||
|
List<String> segments = uri.getPathSegments();
|
||||||
|
if (segments.size() >= 2) {
|
||||||
|
dir = segments.get(segments.size() - 2);
|
||||||
|
} else {
|
||||||
|
dir = uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(context,
|
Toast.makeText(context,
|
||||||
dir==null? context.getString(R.string.done) : context.getString(R.string.file_saved_to, dir),
|
dir==null? context.getString(R.string.done) : context.getString(R.string.file_saved_to, dir),
|
||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
@ -31,20 +39,46 @@ public class StorageUtil {
|
||||||
return storage.canWrite();
|
return storage.canWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getVideoDir() throws NoExternalStorageException {
|
public static boolean canWriteToMediaStore(Context context) {
|
||||||
return new File(getStorageDir(), Environment.DIRECTORY_MOVIES);
|
return Build.VERSION.SDK_INT > 28 ||
|
||||||
|
Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getAudioDir() throws NoExternalStorageException {
|
public static @NonNull Uri getVideoUri() {
|
||||||
return new File(getStorageDir(), Environment.DIRECTORY_MUSIC);
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
return getLegacyUri(Environment.DIRECTORY_MOVIES);
|
||||||
|
} else {
|
||||||
|
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getImageDir() throws NoExternalStorageException {
|
public static @NonNull
|
||||||
return new File(getStorageDir(), Environment.DIRECTORY_PICTURES);
|
Uri getAudioUri() {
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
return getLegacyUri(Environment.DIRECTORY_MUSIC);
|
||||||
|
} else {
|
||||||
|
return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getDownloadDir() throws NoExternalStorageException {
|
public static @NonNull Uri getImageUri() {
|
||||||
return new File(getStorageDir(), Environment.DIRECTORY_DOWNLOADS);
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
return getLegacyUri(Environment.DIRECTORY_PICTURES);
|
||||||
|
} else {
|
||||||
|
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull Uri getDownloadUri() {
|
||||||
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
|
return getLegacyUri(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
} else {
|
||||||
|
return MediaStore.Downloads.EXTERNAL_CONTENT_URI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull Uri getLegacyUri(@NonNull String directory) {
|
||||||
|
return Uri.fromFile(Environment.getExternalStoragePublicDirectory(directory));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable String getCleanFileName(@Nullable String fileName) {
|
public static @Nullable String getCleanFileName(@Nullable String fileName) {
|
||||||
|
|
25
src/org/thoughtcrime/securesms/util/StreamUtil.java
Normal file
25
src/org/thoughtcrime/securesms/util/StreamUtil.java
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class StreamUtil {
|
||||||
|
|
||||||
|
public static long copy(InputStream in, OutputStream out) throws IOException {
|
||||||
|
byte[] buffer = new byte[64 * 1024];
|
||||||
|
int read;
|
||||||
|
long total = 0;
|
||||||
|
|
||||||
|
while ((read = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
total += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue