1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-15 19:27:31 +00:00

Merge pull request #934 from ammargitham/support-android-11

Migrate to SAF (Storage Access Framework)
This commit is contained in:
Austin Huang 2021-06-14 13:53:30 -04:00 committed by GitHub
commit 95b90ab72f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1498 additions and 608 deletions

View File

@ -20,7 +20,7 @@ android {
applicationId 'me.austinhuang.instagrabber' applicationId 'me.austinhuang.instagrabber'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 63 versionCode 63
versionName '19.2.2' versionName '19.2.2'

View File

@ -4,7 +4,7 @@
package="awais.instagrabber"> package="awais.instagrabber">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -20,7 +20,6 @@
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme.Launcher" android:theme="@style/AppTheme.Launcher"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
@ -98,6 +97,7 @@
<activity <activity
android:name=".utils.ProcessPhoenix" android:name=".utils.ProcessPhoenix"
android:theme="@style/Theme.AppCompat.Translucent" /> android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".activities.DirectorySelectActivity" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@ -4,22 +4,19 @@ import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DisplayListener import android.hardware.display.DisplayManager.DisplayListener
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.webkit.MimeTypeMap
import androidx.camera.core.* import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import awais.instagrabber.databinding.ActivityCameraBinding import awais.instagrabber.databinding.ActivityCameraBinding
import awais.instagrabber.utils.DirectoryUtils import awais.instagrabber.utils.DownloadUtils
import awais.instagrabber.utils.PermissionUtils import awais.instagrabber.utils.PermissionUtils
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
import com.google.common.io.Files import java.io.IOException
import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
@ -28,10 +25,10 @@ import java.util.concurrent.Executors
class CameraActivity : BaseLanguageActivity() { class CameraActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityCameraBinding private lateinit var binding: ActivityCameraBinding
private lateinit var outputDirectory: File
private lateinit var displayManager: DisplayManager private lateinit var displayManager: DisplayManager
private lateinit var cameraExecutor: ExecutorService private lateinit var cameraExecutor: ExecutorService
private var outputDirectory: DocumentFile? = null
private var imageCapture: ImageCapture? = null private var imageCapture: ImageCapture? = null
private var displayId = -1 private var displayId = -1
private var cameraProvider: ProcessCameraProvider? = null private var cameraProvider: ProcessCameraProvider? = null
@ -55,7 +52,7 @@ class CameraActivity : BaseLanguageActivity() {
setContentView(binding.root) setContentView(binding.root)
Utils.transparentStatusBar(this, true, false) Utils.transparentStatusBar(this, true, false)
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
outputDirectory = DirectoryUtils.getOutputMediaDirectory(this, "Camera") outputDirectory = DownloadUtils.getCameraDir()
cameraExecutor = Executors.newSingleThreadExecutor() cameraExecutor = Executors.newSingleThreadExecutor()
displayManager.registerDisplayListener(displayListener, null) displayManager.registerDisplayListener(displayListener, null)
binding.viewFinder.post { binding.viewFinder.post {
@ -176,33 +173,28 @@ class CameraActivity : BaseLanguageActivity() {
private fun takePhoto() { private fun takePhoto() {
if (imageCapture == null) return if (imageCapture == null) return
val photoFile = File(outputDirectory, simpleDateFormat.format(System.currentTimeMillis()) + ".jpg") val fileName = simpleDateFormat.format(System.currentTimeMillis()) + ".jpg"
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() val mimeType = "image/jpg"
val photoFile = outputDirectory?.createFile(mimeType, fileName)?.let { it } ?: return
val outputStream = contentResolver.openOutputStream(photoFile.uri)?.let { it } ?: return
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputStream).build()
imageCapture?.takePicture( imageCapture?.takePicture(
outputFileOptions, outputFileOptions,
cameraExecutor, cameraExecutor,
object : ImageCapture.OnImageSavedCallback { object : ImageCapture.OnImageSavedCallback {
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val uri = Uri.fromFile(photoFile) try { outputStream.close() } catch (ignored: IOException) {}
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(Files.getFileExtension(photoFile.name))
MediaScannerConnection.scanFile(
this@CameraActivity,
arrayOf(photoFile.absolutePath),
arrayOf(mimeType)
) { _: String?, uri1: Uri? ->
Log.d(TAG, "onImageSaved: scan complete")
val intent = Intent() val intent = Intent()
intent.data = uri1 intent.data = photoFile.uri
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)
finish() finish()
} Log.d(TAG, "onImageSaved: " + photoFile.uri)
Log.d(TAG, "onImageSaved: $uri")
} }
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "onError: ", exception) Log.e(TAG, "onError: ", exception)
try { outputStream.close() } catch (ignored: IOException) {}
} }
} }
) )

View File

@ -0,0 +1,115 @@
package awais.instagrabber.activities;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ActivityDirectorySelectBinding;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.viewmodels.DirectorySelectActivityViewModel;
public class DirectorySelectActivity extends BaseLanguageActivity {
private static final String TAG = DirectorySelectActivity.class.getSimpleName();
public static final int SELECT_DIR_REQUEST_CODE = 0x01;
private static final int ERROR_REQUEST_CODE = 0x02;
private Uri initialUri;
private ActivityDirectorySelectBinding binding;
private DirectorySelectActivityViewModel viewModel;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDirectorySelectBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(DirectorySelectActivityViewModel.class);
setupObservers();
binding.selectDir.setOnClickListener(v -> openDirectoryChooser());
AppExecutors.INSTANCE.getMainThread().execute(() -> viewModel.setInitialUri(getIntent()));
}
private void setupObservers() {
viewModel.getMessage().observe(this, message -> binding.message.setText(message));
viewModel.getPrevUri().observe(this, prevUri -> {
if (prevUri == null) {
binding.prevUri.setVisibility(View.GONE);
binding.message2.setVisibility(View.GONE);
return;
}
binding.prevUri.setText(prevUri);
binding.prevUri.setVisibility(View.VISIBLE);
binding.message2.setVisibility(View.VISIBLE);
});
viewModel.getDirSuccess().observe(this, success -> binding.selectDir.setVisibility(success ? View.GONE : View.VISIBLE));
viewModel.isLoading().observe(this, loading -> {
binding.message.setVisibility(loading ? View.GONE : View.VISIBLE);
binding.loadingIndicator.setVisibility(loading ? View.VISIBLE : View.GONE);
});
}
private void openDirectoryChooser() {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
}
startActivityForResult(intent, SELECT_DIR_REQUEST_CODE);
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != SELECT_DIR_REQUEST_CODE) return;
if (resultCode != RESULT_OK) {
showErrorDialog(getString(R.string.select_a_folder));
return;
}
if (data == null || data.getData() == null) {
showErrorDialog(getString(R.string.select_a_folder));
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> {
try {
viewModel.setupSelectedDir(data);
final Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
} catch (Exception e) {
// Should not come to this point.
// If it does, we have to show this error to the user so that they can report it.
try (final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
showErrorDialog("Please report this error to the developers:\n\n" + sw.toString());
} catch (IOException ioException) {
Log.e(TAG, "onActivityResult: ", ioException);
}
}
}, 500);
}
private void showErrorDialog(@NonNull final String message) {
final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
ERROR_REQUEST_CODE,
R.string.error,
message,
R.string.ok,
0,
0
);
dialogFragment.show(getSupportFragmentManager(), ConfirmDialogFragment.class.getSimpleName());
}
}

View File

@ -8,6 +8,7 @@ import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.* import android.os.*
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
import android.text.Editable import android.text.Editable
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
@ -53,6 +54,7 @@ import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.* import awais.instagrabber.utils.*
import awais.instagrabber.utils.AppExecutors.tasksThread import awais.instagrabber.utils.AppExecutors.tasksThread
import awais.instagrabber.utils.DownloadUtils.ReselectDocumentTreeException
import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.TextUtils.shortcodeToId import awais.instagrabber.utils.TextUtils.shortcodeToId
import awais.instagrabber.utils.emoji.EmojiParser import awais.instagrabber.utils.emoji.EmojiParser
@ -73,6 +75,7 @@ import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import java.util.stream.Collectors import java.util.stream.Collectors
class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener { class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@ -107,6 +110,16 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() } private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
try {
DownloadUtils.init(this)
} catch (e: ReselectDocumentTreeException) {
super.onCreate(savedInstanceState)
val intent = Intent(this, DirectorySelectActivity::class.java)
intent.putExtra(EXTRA_INITIAL_URI, e.initialUri)
startActivity(intent)
finish()
return
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = this instance = this
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)

View File

@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -73,6 +74,7 @@ public class ConfirmDialogFragment extends DialogFragment {
ConfirmDialogFragment fragment = new ConfirmDialogFragment(); ConfirmDialogFragment fragment = new ConfirmDialogFragment();
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
public ConfirmDialogFragment() {} public ConfirmDialogFragment() {}
@ -80,11 +82,16 @@ public class ConfirmDialogFragment extends DialogFragment {
@Override @Override
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
this.context = context;
final Fragment parentFragment = getParentFragment(); final Fragment parentFragment = getParentFragment();
if (parentFragment instanceof ConfirmDialogFragmentCallback) { if (parentFragment instanceof ConfirmDialogFragmentCallback) {
callback = (ConfirmDialogFragmentCallback) parentFragment; callback = (ConfirmDialogFragmentCallback) parentFragment;
return;
}
final FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity instanceof ConfirmDialogFragmentCallback) {
callback = (ConfirmDialogFragmentCallback) fragmentActivity;
} }
this.context = context;
} }
@NonNull @NonNull

View File

@ -2,8 +2,10 @@ package awais.instagrabber.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -14,27 +16,26 @@ import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentTransaction;
import java.io.File;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Locale; import java.util.Locale;
import awais.instagrabber.databinding.DialogCreateBackupBinding; import awais.instagrabber.databinding.DialogCreateBackupBinding;
import awais.instagrabber.utils.DirectoryChooser; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ExportImportUtils; import awais.instagrabber.utils.ExportImportUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; import static android.app.Activity.RESULT_OK;
import static awais.instagrabber.utils.DownloadUtils.PERMS;
public class CreateBackupDialogFragment extends DialogFragment { public class CreateBackupDialogFragment extends DialogFragment {
private static final String TAG = CreateBackupDialogFragment.class.getSimpleName();
private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private static final DateTimeFormatter BACKUP_FILE_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss", Locale.US); private static final DateTimeFormatter BACKUP_FILE_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss", Locale.US);
private static final int CREATE_FILE_REQUEST_CODE = 1;
private final OnResultListener onResultListener; private final OnResultListener onResultListener;
private DialogCreateBackupBinding binding; private DialogCreateBackupBinding binding;
@ -113,36 +114,37 @@ public class CreateBackupDialogFragment extends DialogFragment {
imm.hideSoftInputFromWindow(binding.etPassword.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); imm.hideSoftInputFromWindow(binding.etPassword.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
}); });
binding.btnSaveTo.setOnClickListener(v -> { binding.btnSaveTo.setOnClickListener(v -> {
if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { createFile();
showChooser(context); // if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) {
} else { // showChooser(context);
requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); // } else {
} // requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE);
// }
}); });
} }
@Override @Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
final Context context = getContext(); // final Context context = getContext();
if (context == null) return; // if (context == null) return;
showChooser(context); // showChooser(context);
} // }
} }
private void showChooser(@NonNull final Context context) { @Override
final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
if (data == null || data.getData() == null) return;
if (resultCode != RESULT_OK || requestCode != CREATE_FILE_REQUEST_CODE) return;
final Context context = getContext();
if (context == null) return;
final Editable passwordText = binding.etPassword.getText(); final Editable passwordText = binding.etPassword.getText();
final String password = binding.cbPassword.isChecked() final String password = binding.cbPassword.isChecked()
&& passwordText != null && passwordText != null
&& !TextUtils.isEmpty(passwordText.toString()) && !TextUtils.isEmpty(passwordText.toString())
? passwordText.toString().trim() ? passwordText.toString().trim()
: null; : null;
final DirectoryChooser directoryChooser = new DirectoryChooser()
.setInitialDirectory(folderPath)
.setInteractionListener(path -> {
final File file = new File(path, String.format("barinsta_%s.backup", LocalDateTime.now().format(BACKUP_FILE_DATE_TIME_FORMAT)));
int flags = 0; int flags = 0;
if (binding.cbExportFavorites.isChecked()) { if (binding.cbExportFavorites.isChecked()) {
flags |= ExportImportUtils.FLAG_FAVORITES; flags |= ExportImportUtils.FLAG_FAVORITES;
@ -153,19 +155,71 @@ public class CreateBackupDialogFragment extends DialogFragment {
if (binding.cbExportLogins.isChecked()) { if (binding.cbExportLogins.isChecked()) {
flags |= ExportImportUtils.FLAG_COOKIES; flags |= ExportImportUtils.FLAG_COOKIES;
} }
ExportImportUtils.exportData(context, flags, file, password, result -> { ExportImportUtils.exportData(context, flags, data.getData(), password, result -> {
if (onResultListener != null) { if (onResultListener != null) {
onResultListener.onResult(result); onResultListener.onResult(result);
} }
dismiss(); dismiss();
}); });
// try (final OutputStream stream = context.getContentResolver().openOutputStream(data.getData())) {
}); // } catch (Exception e) {
directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); // Log.e(TAG, "onActivityResult: ", e);
directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); // }
directoryChooser.show(getChildFragmentManager(), "directory_chooser");
} }
// private void showChooser(@NonNull final Context context) {
// final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH);
// final Editable passwordText = binding.etPassword.getText();
// final String password = binding.cbPassword.isChecked()
// && passwordText != null
// && !TextUtils.isEmpty(passwordText.toString())
// ? passwordText.toString().trim()
// : null;
// final DirectoryChooser directoryChooser = new DirectoryChooser()
// .setInitialDirectory(folderPath)
// .setInteractionListener(path -> {
// final Date now = new Date();
// final File file = new File(path, String.format("barinsta_%s.backup", BACKUP_FILE_DATE_TIME_FORMAT.format(now)));
// int flags = 0;
// if (binding.cbExportFavorites.isChecked()) {
// flags |= ExportImportUtils.FLAG_FAVORITES;
// }
// if (binding.cbExportSettings.isChecked()) {
// flags |= ExportImportUtils.FLAG_SETTINGS;
// }
// if (binding.cbExportLogins.isChecked()) {
// flags |= ExportImportUtils.FLAG_COOKIES;
// }
// ExportImportUtils.exportData(context, flags, file, password, result -> {
// if (onResultListener != null) {
// onResultListener.onResult(result);
// }
// dismiss();
// });
//
// });
// directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
// directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
// directoryChooser.show(getChildFragmentManager(), "directory_chooser");
// }
private void createFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/octet-stream");
final String fileName = String.format("barinsta_%s.backup", LocalDateTime.now().format(BACKUP_FILE_DATE_TIME_FORMAT));
intent.putExtra(Intent.EXTRA_TITLE, fileName);
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when your app creates the document.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, DownloadUtils.getBackupsDir().getUri());
}
startActivityForResult(intent, CREATE_FILE_REQUEST_CODE);
}
public interface OnResultListener { public interface OnResultListener {
void onResult(boolean result); void onResult(boolean result);
} }

View File

@ -7,7 +7,6 @@ import android.graphics.Color;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -16,7 +15,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
@ -24,7 +23,7 @@ import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.image.ImageInfo;
import java.io.File; // import java.io.File;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.customviews.drawee.AnimatedZoomableController; import awais.instagrabber.customviews.drawee.AnimatedZoomableController;
@ -36,6 +35,7 @@ import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.UserRepository; import awais.instagrabber.webservices.UserRepository;
import kotlinx.coroutines.Dispatchers; import kotlinx.coroutines.Dispatchers;
@ -114,11 +114,11 @@ public class ProfilePicDialogFragment extends DialogFragment {
binding.download.setOnClickListener(v -> { binding.download.setOnClickListener(v -> {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) {
downloadProfilePicture(); downloadProfilePicture();
return; // return;
} // }
requestPermissions(DownloadUtils.PERMS, 8020); // requestPermissions(DownloadUtils.PERMS, 8020);
}); });
} }
@ -195,14 +195,18 @@ public class ProfilePicDialogFragment extends DialogFragment {
private void downloadProfilePicture() { private void downloadProfilePicture() {
if (url == null) return; if (url == null) return;
final File dir = new File(Environment.getExternalStorageDirectory(), "Download"); // final File dir = new File(Environment.getExternalStorageDirectory(), "Download");
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
if (dir.exists() || dir.mkdirs()) { // if (dir.exists() || dir.mkdirs()) {
final File saveFile = new File(dir, name + '_' + System.currentTimeMillis() + ".jpg"); //
DownloadUtils.download(context, url, saveFile.getAbsolutePath()); // }
return; final String fileName = name + '_' + System.currentTimeMillis() + ".jpg";
} // final File saveFile = new File(dir, fileName);
Toast.makeText(context, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show(); final DocumentFile downloadDir = DownloadUtils.getDownloadDir();
final DocumentFile saveFile = downloadDir.createFile(Utils.mimeTypeMap.getMimeTypeFromExtension("jpg"), fileName);
DownloadUtils.download(context, url, saveFile);
// return;
// Toast.makeText(context, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show();
} }
} }

View File

@ -1,12 +1,18 @@
package awais.instagrabber.dialogs; package awais.instagrabber.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -15,30 +21,28 @@ import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentTransaction;
import java.io.File;
import awais.instagrabber.databinding.DialogRestoreBackupBinding; import awais.instagrabber.databinding.DialogRestoreBackupBinding;
import awais.instagrabber.utils.DirectoryChooser; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.ExportImportUtils; import awais.instagrabber.utils.ExportImportUtils;
import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; import static android.app.Activity.RESULT_OK;
import static awais.instagrabber.utils.DownloadUtils.PERMS;
public class RestoreBackupDialogFragment extends DialogFragment { public class RestoreBackupDialogFragment extends DialogFragment {
private static final String TAG = RestoreBackupDialogFragment.class.getSimpleName();
private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private static final int OPEN_FILE_REQUEST_CODE = 1;
private OnResultListener onResultListener; private OnResultListener onResultListener;
private DialogRestoreBackupBinding binding; private DialogRestoreBackupBinding binding;
private File file; // private File file;
private boolean isEncrypted; private boolean isEncrypted;
private Uri uri;
public RestoreBackupDialogFragment() {} public RestoreBackupDialogFragment() {}
@ -83,18 +87,62 @@ public class RestoreBackupDialogFragment extends DialogFragment {
@Override @Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showChooser(); // showChooser();
// }
} }
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
if (data == null || data.getData() == null) return;
if (resultCode != RESULT_OK || requestCode != OPEN_FILE_REQUEST_CODE) return;
final Context context = getContext();
if (context == null) return;
isEncrypted = ExportImportUtils.isEncrypted(context, data.getData());
if (isEncrypted) {
binding.passwordGroup.setVisibility(View.VISIBLE);
binding.passwordGroup.post(() -> {
binding.etPassword.requestFocus();
binding.etPassword.post(() -> {
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return;
imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT);
});
binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText()));
});
} else {
binding.passwordGroup.setVisibility(View.GONE);
binding.btnRestore.setEnabled(true);
}
uri = data.getData();
AppExecutors.INSTANCE.getMainThread().execute(() -> {
Cursor c = null;
try {
String[] projection = {MediaStore.Files.FileColumns.DISPLAY_NAME};
final ContentResolver contentResolver = context.getContentResolver();
c = contentResolver.query(uri, projection, null, null, null);
if (c != null) {
while (c.moveToNext()) {
final String displayName = c.getString(0);
binding.filePath.setText(displayName);
}
}
} catch (Exception e) {
Log.e(TAG, "onActivityResult: ", e);
} finally {
if (c != null) {
c.close();
}
}
});
} }
private void init() { private void init() {
final Context context = getContext(); final Context context = getContext();
if (context == null) { if (context == null) return;
return;
}
binding.btnRestore.setEnabled(false); binding.btnRestore.setEnabled(false);
binding.btnRestore.setOnClickListener(v -> new Handler().post(() -> { binding.btnRestore.setOnClickListener(v -> new Handler(Looper.getMainLooper()).post(() -> {
if (uri == null) return;
int flags = 0; int flags = 0;
if (binding.cbFavorites.isChecked()) { if (binding.cbFavorites.isChecked()) {
flags |= ExportImportUtils.FLAG_FAVORITES; flags |= ExportImportUtils.FLAG_FAVORITES;
@ -111,7 +159,7 @@ public class RestoreBackupDialogFragment extends DialogFragment {
ExportImportUtils.importData( ExportImportUtils.importData(
context, context,
flags, flags,
file, uri,
!isEncrypted ? null : text.toString(), !isEncrypted ? null : text.toString(),
result -> { result -> {
if (onResultListener != null) { if (onResultListener != null) {
@ -137,45 +185,55 @@ public class RestoreBackupDialogFragment extends DialogFragment {
@Override @Override
public void afterTextChanged(final Editable s) {} public void afterTextChanged(final Editable s) {}
}); });
if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { // if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) {
showChooser(); // showChooser();
return; // return;
} // }
requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); // requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE);
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
// intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{
// "application/pdf", // .pdf
// "application/vnd.oasis.opendocument.text", // .odt
// "text/plain" // .txt
// });
startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
} }
private void showChooser() { // private void showChooser() {
final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); // final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH);
final Context context = getContext(); // final Context context = getContext();
if (context == null) return; // if (context == null) return;
final DirectoryChooser directoryChooser = new DirectoryChooser() // final DirectoryChooser directoryChooser = new DirectoryChooser()
.setInitialDirectory(folderPath) // .setInitialDirectory(folderPath)
.setShowBackupFiles(true) // .setShowBackupFiles(true)
.setInteractionListener(file -> { // .setInteractionListener(file -> {
isEncrypted = ExportImportUtils.isEncrypted(file); // isEncrypted = ExportImportUtils.isEncrypted(file);
if (isEncrypted) { // if (isEncrypted) {
binding.passwordGroup.setVisibility(View.VISIBLE); // binding.passwordGroup.setVisibility(View.VISIBLE);
binding.passwordGroup.post(() -> { // binding.passwordGroup.post(() -> {
binding.etPassword.requestFocus(); // binding.etPassword.requestFocus();
binding.etPassword.post(() -> { // binding.etPassword.post(() -> {
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); // final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return; // if (imm == null) return;
imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT); // imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT);
}); // });
binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText())); // binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText()));
}); // });
} else { // } else {
binding.passwordGroup.setVisibility(View.GONE); // binding.passwordGroup.setVisibility(View.GONE);
binding.btnRestore.setEnabled(true); // binding.btnRestore.setEnabled(true);
} // }
this.file = file; // this.file = file;
binding.filePath.setText(file.getAbsolutePath()); // binding.filePath.setText(file.getAbsolutePath());
}); // });
directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); // directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); // directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
directoryChooser.setOnCancelListener(this::dismiss); // directoryChooser.setOnCancelListener(this::dismiss);
directoryChooser.show(getChildFragmentManager(), "directory_chooser"); // directoryChooser.show(getChildFragmentManager(), "directory_chooser");
} // }
public interface OnResultListener { public interface OnResultListener {
void onResult(boolean result); void onResult(boolean result);

View File

@ -27,7 +27,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.PermissionChecker;
import androidx.core.graphics.ColorUtils; import androidx.core.graphics.ColorUtils;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.NavController; import androidx.navigation.NavController;
@ -65,9 +64,6 @@ import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.CollectionService; import awais.instagrabber.webservices.CollectionService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
public class CollectionPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public class CollectionPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "CollectionPostsFragment"; private static final String TAG = "CollectionPostsFragment";
private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE = 8020;
@ -106,12 +102,12 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
if (CollectionPostsFragment.this.selectedFeedModels == null) return false; if (CollectionPostsFragment.this.selectedFeedModels == null) return false;
final Context context = getContext(); final Context context = getContext();
if (context == null) return false; if (context == null) return false;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.download(context, ImmutableList.copyOf(CollectionPostsFragment.this.selectedFeedModels)); DownloadUtils.download(context, ImmutableList.copyOf(CollectionPostsFragment.this.selectedFeedModels));
binding.posts.endSelection(); binding.posts.endSelection();
return true; // return true;
} // }
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION);
} }
return false; return false;
} }
@ -141,13 +137,13 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
public void onDownloadClick(final Media feedModel, final int childPosition) { public void onDownloadClick(final Media feedModel, final int childPosition) {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.showDownloadDialog(context, feedModel, childPosition); DownloadUtils.showDownloadDialog(context, feedModel, childPosition);
return; // return;
} // }
downloadFeedModel = feedModel; // downloadFeedModel = feedModel;
downloadChildPosition = -1; // downloadChildPosition = -1;
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
} }
@Override @Override

View File

@ -105,7 +105,7 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.PostViewV2ViewModel; import awais.instagrabber.viewmodels.PostViewV2ViewModel;
import static androidx.core.content.PermissionChecker.checkSelfPermission; //import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG; import static awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
@ -119,6 +119,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private DialogPostViewBinding binding; private DialogPostViewBinding binding;
private Context context;
private boolean detailsVisible = true; private boolean detailsVisible = true;
private boolean video; private boolean video;
private VideoPlayerViewHelper videoPlayerViewHelper; private VideoPlayerViewHelper videoPlayerViewHelper;
@ -211,6 +212,12 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
init(); init();
} }
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
this.context = context;
}
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
@ -454,13 +461,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
private void setupDownload() { private void setupDownload() {
bottom.download.setOnClickListener(v -> { bottom.download.setOnClickListener(v -> {
final Context context = getContext();
if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.showDownloadDialog(context, viewModel.getMedia(), sliderPosition); DownloadUtils.showDownloadDialog(context, viewModel.getMedia(), sliderPosition);
return;
}
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
}); });
TooltipCompat.setTooltipText(bottom.download, getString(R.string.action_download)); TooltipCompat.setTooltipText(bottom.download, getString(R.string.action_download));
} }

View File

@ -19,7 +19,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.PermissionChecker;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
@ -46,8 +45,6 @@ import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
public final class SavedViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public final class SavedViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
@ -88,12 +85,12 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
if (SavedViewerFragment.this.selectedFeedModels == null) return false; if (SavedViewerFragment.this.selectedFeedModels == null) return false;
final Context context = getContext(); final Context context = getContext();
if (context == null) return false; if (context == null) return false;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.download(context, ImmutableList.copyOf(SavedViewerFragment.this.selectedFeedModels)); DownloadUtils.download(context, ImmutableList.copyOf(SavedViewerFragment.this.selectedFeedModels));
binding.posts.endSelection(); binding.posts.endSelection();
return true; // return true;
} // }
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION);
} }
return false; return false;
} }
@ -123,13 +120,13 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
public void onDownloadClick(final Media feedModel, final int childPosition) { public void onDownloadClick(final Media feedModel, final int childPosition) {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.showDownloadDialog(context, feedModel, childPosition); DownloadUtils.showDownloadDialog(context, feedModel, childPosition);
return; // return;
} // }
downloadFeedModel = feedModel; // downloadFeedModel = feedModel;
downloadChildPosition = childPosition; // downloadChildPosition = childPosition;
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
} }
@Override @Override

View File

@ -24,7 +24,6 @@ import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.PermissionChecker;
import androidx.core.graphics.ColorUtils; import androidx.core.graphics.ColorUtils;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.NavController; import androidx.navigation.NavController;
@ -60,9 +59,6 @@ import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.DiscoverService; import awais.instagrabber.webservices.DiscoverService;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = TopicPostsFragment.class.getSimpleName(); private static final String TAG = TopicPostsFragment.class.getSimpleName();
private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE = 8020;
@ -99,12 +95,12 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
if (TopicPostsFragment.this.selectedFeedModels == null) return false; if (TopicPostsFragment.this.selectedFeedModels == null) return false;
final Context context = getContext(); final Context context = getContext();
if (context == null) return false; if (context == null) return false;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.download(context, ImmutableList.copyOf(TopicPostsFragment.this.selectedFeedModels)); DownloadUtils.download(context, ImmutableList.copyOf(TopicPostsFragment.this.selectedFeedModels));
binding.posts.endSelection(); binding.posts.endSelection();
return true; return true;
} // }
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION);
} }
return false; return false;
} }
@ -134,13 +130,13 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
public void onDownloadClick(final Media feedModel, final int childPosition) { public void onDownloadClick(final Media feedModel, final int childPosition) {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.showDownloadDialog(context, feedModel, childPosition); DownloadUtils.showDownloadDialog(context, feedModel, childPosition);
return; // return;
} // }
downloadFeedModel = feedModel; // downloadFeedModel = feedModel;
downloadChildPosition = -1; // downloadChildPosition = -1;
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
} }
@Override @Override

View File

@ -1408,13 +1408,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return; return;
} }
if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) {
DownloadUtils.download(context, media); DownloadUtils.download(context, media);
Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show();
return; // return;
} // }
tempMedia = media; // tempMedia = media;
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
} }
@Nullable @Nullable

View File

@ -183,11 +183,12 @@ public class ImageEditFragment extends Fragment {
if (context == null) return; if (context == null) return;
final Uri resultUri = viewModel.getResultUri().getValue(); final Uri resultUri = viewModel.getResultUri().getValue();
if (resultUri == null) return; if (resultUri == null) return;
Utils.mediaScanFile(context, new File(resultUri.toString()), (path, uri) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { AppExecutors.INSTANCE.getMainThread().execute(() -> {
final NavController navController = NavHostFragment.findNavController(this); final NavController navController = NavHostFragment.findNavController(this);
setNavControllerResult(navController, resultUri); setNavControllerResult(navController, resultUri);
navController.navigateUp(); navController.navigateUp();
})); });
// Utils.mediaScanFile(context, new File(resultUri.toString()), (path, uri) -> );
}); });
} }

View File

@ -1,27 +1,41 @@
package awais.instagrabber.fragments.settings; package awais.instagrabber.fragments.settings;
import android.content.Context; import android.content.Context;
import android.view.View; import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatButton; import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreferenceCompat; import androidx.preference.SwitchPreferenceCompat;
import com.google.android.material.switchmaterial.SwitchMaterial; import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.utils.DirectoryChooser; import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; import static android.app.Activity.RESULT_OK;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_SAVE_TO; import static awais.instagrabber.activities.DirectorySelectActivity.SELECT_DIR_REQUEST_CODE;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
public class DownloadsPreferencesFragment extends BasePreferencesFragment { public class DownloadsPreferencesFragment extends BasePreferencesFragment {
private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName();
// private SaveToCustomFolderPreference.ResultCallback resultCallback;
@Override @Override
void setupPreferenceScreen(final PreferenceScreen screen) { void setupPreferenceScreen(final PreferenceScreen screen) {
final Context context = getContext(); final Context context = getContext();
@ -40,13 +54,88 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
} }
private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { private Preference getSaveToCustomFolderPreference(@NonNull final Context context) {
return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() final Preference preference = new Preference(context);
.setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) preference.setKey(PreferenceKeys.PREF_BARINSTA_DIR_URI);
.setInteractionListener(file -> { preference.setIconSpaceReserved(false);
settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); preference.setTitle(R.string.barinsta_folder);
resultCallback.onResult(file.getAbsolutePath()); preference.setSummaryProvider(p -> {
}) final String currentValue = settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI);
.show(getParentFragmentManager(), null)); if (TextUtils.isEmpty(currentValue)) return "";
String path;
try {
path = URLDecoder.decode(currentValue, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
path = currentValue;
}
return path;
});
preference.setOnPreferenceClickListener(p -> {
openDirectoryChooser(DownloadUtils.getRootDirUri());
return true;
});
return preference;
// return new SaveToCustomFolderPreference(context, checked -> {
// try {
// DownloadUtils.init(context);
// } catch (DownloadUtils.ReselectDocumentTreeException e) {
// if (!checked) return;
// startDocumentSelector(e.getInitialUri());
// } catch (Exception e) {
// Log.e(TAG, "getSaveToCustomFolderPreference: ", e);
// }
// }, (resultCallback) -> {
// // Choose a directory using the system's file picker.
// startDocumentSelector(null);
// this.resultCallback = resultCallback;
//
// // new DirectoryChooser()
// // .setInitialDirectory(settingsHelper.getString(FOLDER_PATH))
// // .setInteractionListener(file -> {
// // settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath());
// // resultCallback.onResult(file.getAbsolutePath());
// // })
// // .show(getParentFragmentManager(), null);
// });
}
private void openDirectoryChooser(final Uri initialUri) {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
}
startActivityForResult(intent, SELECT_DIR_REQUEST_CODE);
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
if (requestCode != SELECT_DIR_REQUEST_CODE) return;
if (resultCode != RESULT_OK) return;
if (data == null || data.getData() == null) return;
final Context context = getContext();
if (context == null) return;
AppExecutors.INSTANCE.getMainThread().execute(() -> {
try {
Utils.setupSelectedDir(context, data);
} catch (Exception e) {
// Should not come to this point.
// If it does, we have to show this error to the user so that they can report it.
try (final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
123,
R.string.error,
"Please report this error to the developers:\n\n" + sw.toString(),
R.string.ok,
0,
0
);
dialogFragment.show(getChildFragmentManager(), ConfirmDialogFragment.class.getSimpleName());
} catch (IOException ioException) {
Log.e(TAG, "onActivityResult: ", ioException);
}
}
}, 500);
} }
private Preference getPrependUsernameToFilenamePreference(@NonNull final Context context) { private Preference getPrependUsernameToFilenamePreference(@NonNull final Context context) {
@ -57,53 +146,74 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
return preference; return preference;
} }
public static class SaveToCustomFolderPreference extends Preference { // public static class SaveToCustomFolderPreference extends Preference {
private AppCompatTextView customPathTextView; // private AppCompatTextView customPathTextView;
private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; // private final OnSaveToChangeListener onSaveToChangeListener;
private final String key; // private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener;
// private final String key;
public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { //
super(context); // public SaveToCustomFolderPreference(final Context context,
this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; // final OnSaveToChangeListener onSaveToChangeListener,
key = PreferenceKeys.FOLDER_SAVE_TO; // final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) {
setLayoutResource(R.layout.pref_custom_folder); // super(context);
setKey(key); // this.onSaveToChangeListener = onSaveToChangeListener;
setTitle(R.string.save_to_folder); // this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener;
setIconSpaceReserved(false); // key = FOLDER_SAVE_TO;
} // setLayoutResource(R.layout.pref_custom_folder);
// setKey(key);
@Override // setTitle(R.string.save_to_folder);
public void onBindViewHolder(final PreferenceViewHolder holder) { // setIconSpaceReserved(false);
super.onBindViewHolder(holder); // }
final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); //
final View buttonContainer = holder.findViewById(R.id.button_container); // @Override
customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); // public void onBindViewHolder(final PreferenceViewHolder holder) {
cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { // super.onBindViewHolder(holder);
settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); // final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo);
buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); // final View buttonContainer = holder.findViewById(R.id.button_container);
final String customPath = settingsHelper.getString(FOLDER_PATH); // customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path);
customPathTextView.setText(customPath); // cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> {
}); // settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked);
final boolean savedToEnabled = settingsHelper.getBoolean(key); // buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE);
holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); // final Context context = getContext();
cbSaveTo.setChecked(savedToEnabled); // String customPath = settingsHelper.getString(FOLDER_PATH);
buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); // if (!TextUtils.isEmpty(customPath) && customPath.startsWith("content") && context != null) {
final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); // final Uri uri = Uri.parse(customPath);
btnSaveTo.setOnClickListener(v -> { // final DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri);
if (onSelectFolderButtonClickListener == null) return; // try {
onSelectFolderButtonClickListener.onClick(result -> { // customPath = Utils.getDocumentFileRealPath(context, documentFile).getAbsolutePath();
if (TextUtils.isEmpty(result)) return; // } catch (Exception e) {
customPathTextView.setText(result); // Log.e(TAG, "onBindViewHolder: ", e);
}); // }
}); // }
} // customPathTextView.setText(customPath);
// if (onSaveToChangeListener != null) {
public interface ResultCallback { // onSaveToChangeListener.onChange(isChecked);
void onResult(String result); // }
} // });
// final boolean savedToEnabled = settingsHelper.getBoolean(key);
public interface OnSelectFolderButtonClickListener { // holder.itemView.setOnClickListener(v -> cbSaveTo.toggle());
void onClick(ResultCallback resultCallback); // cbSaveTo.setChecked(savedToEnabled);
} // buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE);
} // final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo);
// btnSaveTo.setOnClickListener(v -> {
// if (onSelectFolderButtonClickListener == null) return;
// onSelectFolderButtonClickListener.onClick(result -> {
// if (TextUtils.isEmpty(result)) return;
// customPathTextView.setText(result);
// });
// });
// }
//
// public interface ResultCallback {
// void onResult(String result);
// }
//
// public interface OnSelectFolderButtonClickListener {
// void onClick(ResultCallback resultCallback);
// }
//
// public interface OnSaveToChangeListener {
// void onChange(boolean checked);
// }
// }
} }

View File

@ -19,6 +19,7 @@ object PreferenceKeys {
const val APP_THEME = "app_theme_v19" const val APP_THEME = "app_theme_v19"
const val APP_LANGUAGE = "app_language_v19" const val APP_LANGUAGE = "app_language_v19"
const val STORY_SORT = "story_sort" const val STORY_SORT = "story_sort"
const val PREF_BARINSTA_DIR_URI = "barinsta_dir_uri"
// set string prefs // set string prefs
const val KEYWORD_FILTERS = "keyword_filters" const val KEYWORD_FILTERS = "keyword_filters"

View File

@ -4,13 +4,14 @@ import android.app.IntentService;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.util.Random; import java.util.Random;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
@ -39,7 +40,10 @@ public class DeleteImageIntentService extends IntentService {
if (intent != null && Intent.ACTION_DELETE.equals(intent.getAction()) && intent.hasExtra(EXTRA_IMAGE_PATH)) { if (intent != null && Intent.ACTION_DELETE.equals(intent.getAction()) && intent.hasExtra(EXTRA_IMAGE_PATH)) {
final String path = intent.getStringExtra(EXTRA_IMAGE_PATH); final String path = intent.getStringExtra(EXTRA_IMAGE_PATH);
if (TextUtils.isEmpty(path)) return; if (TextUtils.isEmpty(path)) return;
final File file = new File(path); // final File file = new File(path);
final Uri parse = Uri.parse(path);
if (parse == null) return;
final DocumentFile file = DocumentFile.fromSingleUri(getApplicationContext(), parse);
boolean deleted; boolean deleted;
if (file.exists()) { if (file.exists()) {
deleted = file.delete(); deleted = file.delete();
@ -58,11 +62,11 @@ public class DeleteImageIntentService extends IntentService {
@NonNull @NonNull
public static PendingIntent pendingIntent(@NonNull final Context context, public static PendingIntent pendingIntent(@NonNull final Context context,
@NonNull final String imagePath, @NonNull final DocumentFile imagePath,
final int notificationId) { final int notificationId) {
final Intent intent = new Intent(context, DeleteImageIntentService.class); final Intent intent = new Intent(context, DeleteImageIntentService.class);
intent.setAction(Intent.ACTION_DELETE); intent.setAction(Intent.ACTION_DELETE);
intent.putExtra(EXTRA_IMAGE_PATH, imagePath); intent.putExtra(EXTRA_IMAGE_PATH, imagePath.getUri().toString());
intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId); intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
return PendingIntent.getService(context, random.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT); return PendingIntent.getService(context, random.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
} }

View File

@ -8,6 +8,7 @@ import android.net.Uri
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.documentfile.provider.DocumentFile
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -192,9 +193,9 @@ object BitmapUtils {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File { fun convertToJpegAndSaveToFile(contentResolver: ContentResolver, bitmap: Bitmap, file: DocumentFile?): DocumentFile {
val tempFile = file ?: DownloadUtils.getTempFile() val tempFile = file ?: DownloadUtils.getTempFile(null, "jpg")
FileOutputStream(tempFile).use { output -> contentResolver.openOutputStream(tempFile.uri).use { output ->
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
if (!compressResult) { if (!compressResult) {
throw RuntimeException("Compression failed!") throw RuntimeException("Compression failed!")

View File

@ -89,4 +89,5 @@ object Constants {
const val DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id" const val DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id"
const val DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title" const val DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title"
const val X_IG_APP_ID = "936619743392459" const val X_IG_APP_ID = "936619743392459"
const val EXTRA_INITIAL_URI = "initial_uri"
} }

View File

@ -3,7 +3,6 @@ package awais.instagrabber.utils;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
@ -11,19 +10,14 @@ import android.os.FileObserver;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -95,15 +89,15 @@ public final class DirectoryChooser extends DialogFragment {
if (context == null) context = getContext(); if (context == null) context = getContext();
if (context == null) context = getActivity(); if (context == null) context = getActivity();
if (context == null) return; if (context == null) return;
if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) != PackageManager.PERMISSION_GRANTED) { // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) != PackageManager.PERMISSION_GRANTED) {
final String text = "Storage permissions denied!"; // final String text = "Storage permissions denied!";
if (container == null) { // if (container == null) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show(); // Toast.makeText(context, text, Toast.LENGTH_LONG).show();
} else { // } else {
Snackbar.make(container, text, BaseTransientBottomBar.LENGTH_LONG).show(); // Snackbar.make(container, text, BaseTransientBottomBar.LENGTH_LONG).show();
} // }
dismiss(); // dismiss();
} // }
final View.OnClickListener clickListener = v -> { final View.OnClickListener clickListener = v -> {
if (v == binding.btnConfirm) { if (v == binding.btnConfirm) {
if (interactionListener != null && isValidFile(selectedDir)) if (interactionListener != null && isValidFile(selectedDir))

View File

@ -1,7 +1,5 @@
package awais.instagrabber.utils; package awais.instagrabber.utils;
import android.content.Context;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import java.io.File; import java.io.File;
@ -10,8 +8,6 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import awais.instagrabber.R;
public class DirectoryUtils { public class DirectoryUtils {
private static final Pattern DIR_SEPORATOR = Pattern.compile("/"); private static final Pattern DIR_SEPORATOR = Pattern.compile("/");
@ -73,22 +69,22 @@ public class DirectoryUtils {
return rv; return rv;
} }
public static File getOutputMediaDirectory(final Context context, final String... dirs) { // public static File getOutputMediaDirectory(final Context context, final String... dirs) {
if (context == null) return null; // if (context == null) return null;
final File[] externalMediaDirs = context.getExternalMediaDirs(); // final File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs == null || externalMediaDirs.length == 0) return context.getFilesDir(); // if (externalMediaDirs == null || externalMediaDirs.length == 0) return context.getFilesDir();
final File externalMediaDir = externalMediaDirs[0]; // final File externalMediaDir = externalMediaDirs[0];
File subDir = new File(externalMediaDir, context.getString(R.string.app_name)); // File subDir = new File(externalMediaDir, context.getString(R.string.app_name));
if (dirs != null) { // if (dirs != null) {
for (final String dir : dirs) { // for (final String dir : dirs) {
subDir = new File(subDir, dir); // subDir = new File(subDir, dir);
//noinspection ResultOfMethodCallIgnored // //noinspection ResultOfMethodCallIgnored
subDir.mkdirs(); // subDir.mkdirs();
} // }
} // }
if (!subDir.exists()) { // if (!subDir.exists()) {
return context.getFilesDir(); // return context.getFilesDir();
} // }
return subDir; // return subDir;
} // }
} }

View File

@ -1,9 +1,11 @@
package awais.instagrabber.utils; package awais.instagrabber.utils;
import android.Manifest; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Environment; import android.content.UriPermission;
import android.Manifest;
import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
@ -11,6 +13,8 @@ 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 androidx.core.util.Pair;
import androidx.documentfile.provider.DocumentFile;
import androidx.work.Constraints; import androidx.work.Constraints;
import androidx.work.Data; import androidx.work.Data;
import androidx.work.NetworkType; import androidx.work.NetworkType;
@ -21,9 +25,9 @@ import androidx.work.WorkRequest;
import com.google.gson.Gson; import com.google.gson.Gson;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
@ -42,51 +46,159 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.repositories.responses.VideoVersion;
import awais.instagrabber.workers.DownloadWorker; import awais.instagrabber.workers.DownloadWorker;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_SAVE_TO;
public final class DownloadUtils { public final class DownloadUtils {
private static final String TAG = "DownloadUtils"; private static final String TAG = DownloadUtils.class.getSimpleName();
// private static final String DIR_BARINSTA = "Barinsta";
private static final String DIR_DOWNLOADS = "Downloads";
private static final String DIR_CAMERA = "Camera";
private static final String DIR_EDIT = "Edit";
private static final String DIR_RECORDINGS = "Sent Recordings";
private static final String DIR_TEMP = "Temp";
private static final String DIR_BACKUPS = "Backups";
private static DocumentFile root;
public static final String WRITE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE; public static final String WRITE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE;
public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
@NonNull public static void init(@NonNull final Context context) throws ReselectDocumentTreeException {
private static File getDownloadDir() { final String barinstaDirUri = Utils.settingsHelper.getString(PREF_BARINSTA_DIR_URI);
File dir = new File(Environment.getExternalStorageDirectory(), "Download"); if (TextUtils.isEmpty(barinstaDirUri)) {
throw new ReselectDocumentTreeException("folder path is null or empty");
}
if (!barinstaDirUri.startsWith("content")) {
// reselect the folder in selector view
throw new ReselectDocumentTreeException(Uri.parse(barinstaDirUri));
}
final Uri uri = Uri.parse(barinstaDirUri);
final List<UriPermission> existingPermissions = context.getContentResolver().getPersistedUriPermissions();
if (existingPermissions.isEmpty()) {
// reselect the folder in selector view
throw new ReselectDocumentTreeException(uri);
}
final boolean anyMatch = existingPermissions.stream().anyMatch(uriPermission -> uriPermission.getUri().equals(uri));
if (!anyMatch) {
// reselect the folder in selector view
throw new ReselectDocumentTreeException(uri);
}
root = DocumentFile.fromTreeUri(context, uri);
if (root == null || !root.exists() || root.lastModified() == 0) {
root = null;
throw new ReselectDocumentTreeException(uri);
}
}
if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { public static void destroy() {
final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); root = null;
if (!TextUtils.isEmpty(customPath)) {
dir = new File(customPath);
}
}
return dir;
} }
@Nullable @Nullable
private static File getDownloadDir(@NonNull final Context context, @Nullable final String username) { public static DocumentFile getDownloadDir(final String... dirs) {
return getDownloadDir(context, username, false); if (root == null) {
return null;
}
DocumentFile subDir = root;
if (dirs != null) {
for (final String dir : dirs) {
if (subDir == null || TextUtils.isEmpty(dir)) continue;
final DocumentFile subDirFile = subDir.findFile(dir);
final boolean exists = subDirFile != null && subDirFile.exists();
subDir = exists ? subDirFile : subDir.createDirectory(dir);
}
}
return subDir;
} }
@Nullable @Nullable
private static File getDownloadDir(final Context context, public static DocumentFile getDownloadDir() {
@Nullable final String username, // final File parent = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
final boolean skipCreateDir) { // final File dir = new File(new File(parent, "barinsta"), "downloads");
File dir = getDownloadDir(); // if (!dir.exists()) {
// final boolean mkdirs = dir.mkdirs();
if (Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_USER_FOLDER) && !TextUtils.isEmpty(username)) { // if (!mkdirs) {
final String finaleUsername = username.startsWith("@") ? username.substring(1) : username; // Log.e(TAG, "getDownloadDir: failed to create dir");
dir = new File(dir, finaleUsername); // }
// }
// if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) {
// final String customPath = Utils.settingsHelper.getString(FOLDER_PATH);
// if (!TextUtils.isEmpty(customPath)) {
// dir = new File(customPath);
// }
// }
return getDownloadDir(DIR_DOWNLOADS);
} }
if (context != null && !skipCreateDir && !dir.exists() && !dir.mkdirs()) { @Nullable
public static DocumentFile getCameraDir() {
return getDownloadDir(DIR_CAMERA);
}
@Nullable
public static DocumentFile getImageEditDir(final String sessionId) {
return getDownloadDir(DIR_EDIT, sessionId);
}
@Nullable
public static DocumentFile getRecordingsDir() {
return getDownloadDir(DIR_RECORDINGS);
}
@Nullable
public static DocumentFile getBackupsDir() {
return getDownloadDir(DIR_BACKUPS);
}
// @Nullable
// private static DocumentFile getDownloadDir(@NonNull final Context context, @Nullable final String username) {
// return getDownloadDir(context, username, false);
// }
@Nullable
private static DocumentFile getDownloadDir(final Context context,
@Nullable final String username) {
final List<String> userFolderPaths = getSubPathForUserFolder(username);
DocumentFile dir = root;
for (final String dirName : userFolderPaths) {
final DocumentFile file = dir.findFile(dirName);
if (file != null) {
dir = file;
continue;
}
dir = dir.createDirectory(dirName);
if (dir == null) break;
}
// final String joined = android.text.TextUtils.join("/", userFolderPaths);
// final Uri userFolderUri = DocumentsContract.buildDocumentUriUsingTree(root.getUri(), joined);
// final DocumentFile userFolder = DocumentFile.fromSingleUri(context, userFolderUri);
if (context != null && (dir == null || !dir.exists())) {
Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show();
return null; return null;
} }
return dir; return dir;
} }
private static List<String> getSubPathForUserFolder(final String username) {
final List<String> list = new ArrayList<>();
if (!Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_USER_FOLDER) || TextUtils.isEmpty(username)) {
list.add(DIR_DOWNLOADS);
return list;
}
final String finalUsername = username.startsWith("@") ? username.substring(1) : username;
list.add(DIR_DOWNLOADS);
list.add(finalUsername);
return list;
}
private static DocumentFile getTempDir() {
DocumentFile file = root.findFile(DIR_TEMP);
if (file == null) {
file = root.createDirectory(DIR_TEMP);
}
return file;
}
// public static void dmDownload(@NonNull final Context context, // public static void dmDownload(@NonNull final Context context,
// @Nullable final String username, // @Nullable final String username,
// final String modelId, // final String modelId,
@ -103,66 +215,78 @@ public final class DownloadUtils {
// @Nullable final String username, // @Nullable final String username,
// final String modelId, // final String modelId,
// final String url) { // final String url) {
// final File dir = getDownloadDir(context, username); // final DocumentFile dir = getDownloadDir(context, username);
// if (dir.exists() || dir.mkdirs()) { // if (dir != null && dir.exists()) {
// download(context, // download(context, url, getDownloadSavePaths(dir, modelId, url));
// url,
// getDownloadSaveFile(dir, modelId, url).getAbsolutePath());
// return; // return;
// } // }
// Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); // Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show();
// } // }
@NonNull private static Pair<List<String>, String> getDownloadSavePaths(final List<String> paths,
private static File getDownloadSaveFile(final File finalDir,
final String postId, final String postId,
final String displayUrl) { final String displayUrl) {
return getDownloadSaveFile(finalDir, postId, "", displayUrl, ""); return getDownloadSavePaths(paths, postId, "", displayUrl, "");
} }
@NonNull private static Pair<List<String>, String> getDownloadSavePaths(final List<String> paths,
private static File getDownloadSaveFile(final File finalDir,
final String postId, final String postId,
final String displayUrl, final String displayUrl,
final String username) { final String username) {
return getDownloadSaveFile(finalDir, postId, "", displayUrl, username); return getDownloadSavePaths(paths, postId, "", displayUrl, username);
} }
private static File getDownloadChildSaveFile(final File downloadDir, private static Pair<List<String>, String> getDownloadChildSavePaths(final List<String> paths,
final String postId, final String postId,
final int childPosition, final int childPosition,
final String url, final String url,
final String username) { final String username) {
final String sliderPostfix = "_slide_" + childPosition; final String sliderPostfix = "_slide_" + childPosition;
return getDownloadSaveFile(downloadDir, postId, sliderPostfix, url, username); return getDownloadSavePaths(paths, postId, sliderPostfix, url, username);
} }
@NonNull private static Pair<List<String>, String> getDownloadSavePaths(final List<String> paths,
private static File getDownloadSaveFile(final File finalDir,
final String postId, final String postId,
final String sliderPostfix, final String sliderPostfix,
final String displayUrl, final String displayUrl,
final String username) { final String username) {
if (paths == null) return null;
final String extension = getFileExtensionFromUrl(displayUrl);
final String usernamePrepend = TextUtils.isEmpty(username) ? "" : (username + "_"); final String usernamePrepend = TextUtils.isEmpty(username) ? "" : (username + "_");
final String fileName = usernamePrepend + postId + sliderPostfix + getFileExtensionFromUrl(displayUrl); final String fileName = usernamePrepend + postId + sliderPostfix + extension;
return new File(finalDir, fileName); // return new File(finalDir, fileName);
// DocumentFile file = finalDir.findFile(fileName);
// if (file == null) {
final String mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension.startsWith(".") ? extension.substring(1) : extension);
// file = finalDir.createFile(mimeType, fileName);
// }
paths.add(fileName);
return new Pair<>(paths, mimeType);
} }
@NonNull // public static DocumentFile getTempFile() {
public static File getTempFile() { // return getTempFile(null, null);
return getTempFile(null, null); // }
}
public static File getTempFile(final String fileName, final String extension) { public static DocumentFile getTempFile(final String fileName, final String extension) {
final File dir = getDownloadDir(); final DocumentFile dir = getTempDir();
String name = fileName; String name = fileName;
if (TextUtils.isEmpty(name)) { if (TextUtils.isEmpty(name)) {
name = UUID.randomUUID().toString(); name = UUID.randomUUID().toString();
} }
String mimeType = "application/octet-stream";
if (!TextUtils.isEmpty(extension)) { if (!TextUtils.isEmpty(extension)) {
name += "." + extension; name += "." + extension;
final String mimeType1 = Utils.mimeTypeMap.getMimeTypeFromExtension(extension);
if (mimeType1 != null) {
mimeType = mimeType1;
} }
return new File(dir, name); }
DocumentFile file = dir.findFile(name);
if (file == null) {
file = dir.createFile(mimeType, name);
}
return file;
} }
/** /**
@ -212,14 +336,20 @@ public final class DownloadUtils {
if (user != null) { if (user != null) {
username = user.getUsername(); username = user.getUsername();
} }
final File downloadDir = getDownloadDir(null, "@" + username, true); final List<String> userFolderPaths = getSubPathForUserFolder(username);
switch (media.getMediaType()) { switch (media.getMediaType()) {
case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO: { case MEDIA_TYPE_VIDEO: {
final String url = ResponseBodyUtils.getImageUrl(media); final String url = ResponseBodyUtils.getImageUrl(media);
final File file = getDownloadSaveFile(downloadDir, media.getCode(), url, ""); final Pair<List<String>, String> file = getDownloadSavePaths(new ArrayList<>(userFolderPaths), media.getCode(), url, "");
final File usernamePrependedFile = getDownloadSaveFile(downloadDir, media.getCode(), url, username); final boolean fileExists = file.first != null && checkPathExists(file.first);
checkList.add(file.exists() || usernamePrependedFile.exists()); boolean usernameFileExists = false;
if (!fileExists) {
final Pair<List<String>, String> usernameFile = getDownloadSavePaths(
new ArrayList<>(userFolderPaths), media.getCode(), url, username);
usernameFileExists = usernameFile.first != null && checkPathExists(usernameFile.first);
}
checkList.add(fileExists || usernameFileExists);
break; break;
} }
case MEDIA_TYPE_SLIDER: case MEDIA_TYPE_SLIDER:
@ -228,9 +358,16 @@ public final class DownloadUtils {
final Media child = sliderItems.get(i); final Media child = sliderItems.get(i);
if (child == null) continue; if (child == null) continue;
final String url = ResponseBodyUtils.getImageUrl(child); final String url = ResponseBodyUtils.getImageUrl(child);
final File file = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url, ""); final Pair<List<String>, String> file = getDownloadChildSavePaths(
final File usernamePrependedFile = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url, username); new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, "");
checkList.add(file.exists() || usernamePrependedFile.exists()); final boolean fileExists = file.first != null && checkPathExists(file.first);
boolean usernameFileExists = false;
if (!fileExists) {
final Pair<List<String>, String> usernameFile = getDownloadChildSavePaths(
new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, username);
usernameFileExists = usernameFile.first != null && checkPathExists(usernameFile.first);
}
checkList.add(fileExists || usernameFileExists);
} }
break; break;
default: default:
@ -238,6 +375,18 @@ public final class DownloadUtils {
return checkList; return checkList;
} }
private static boolean checkPathExists(@NonNull final List<String> paths) {
if (root == null) return false;
DocumentFile dir = root;
for (final String path : paths) {
dir = dir.findFile(path);
if (dir == null || !dir.exists()) {
return false;
}
}
return true;
}
public static void showDownloadDialog(@NonNull Context context, public static void showDownloadDialog(@NonNull Context context,
@NonNull final Media feedModel, @NonNull final Media feedModel,
final int childPosition) { final int childPosition) {
@ -272,17 +421,25 @@ public final class DownloadUtils {
public static void download(@NonNull final Context context, public static void download(@NonNull final Context context,
@NonNull final StoryModel storyModel) { @NonNull final StoryModel storyModel) {
final File downloadDir = getDownloadDir(context, "@" + storyModel.getUsername()); final DocumentFile downloadDir = getDownloadDir(context, storyModel.getUsername());
if (downloadDir == null) return;
final String url = storyModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO final String url = storyModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO
? storyModel.getVideoUrl() ? storyModel.getVideoUrl()
: storyModel.getStoryUrl(); : storyModel.getStoryUrl();
final String extension = DownloadUtils.getFileExtensionFromUrl(url);
final String baseFileName = storyModel.getStoryMediaId() + "_" final String baseFileName = storyModel.getStoryMediaId() + "_"
+ storyModel.getTimestamp() + DownloadUtils.getFileExtensionFromUrl(url); + storyModel.getTimestamp() + extension;
final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME)
&& storyModel.getUsername() != null ? storyModel.getUsername() + "_" : ""; && storyModel.getUsername() != null ? storyModel.getUsername() + "_" : "";
final File saveFile = new File(downloadDir, final String fileName = usernamePrepend + baseFileName;
usernamePrepend + baseFileName); DocumentFile saveFile = downloadDir.findFile(fileName);
download(context, url, saveFile.getAbsolutePath()); if (saveFile == null) {
final String mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension.startsWith(".") ? extension.substring(1) : extension);
if (mimeType == null) return;
saveFile = downloadDir.createFile(mimeType, fileName);
}
// final File saveFile = new File(downloadDir, fileName);
download(context, url, saveFile);
} }
public static void download(@NonNull final Context context, public static void download(@NonNull final Context context,
@ -304,11 +461,12 @@ public final class DownloadUtils {
private static void download(@NonNull final Context context, private static void download(@NonNull final Context context,
@NonNull final List<Media> feedModels, @NonNull final List<Media> feedModels,
final int childPositionIfSingle) { final int childPositionIfSingle) {
final Map<String, String> map = new HashMap<>(); final Map<String, DocumentFile> map = new HashMap<>();
for (final Media media : feedModels) { for (final Media media : feedModels) {
final User mediaUser = media.getUser(); final User mediaUser = media.getUser();
final File downloadDir = getDownloadDir(context, mediaUser == null ? "" : mediaUser.getUsername()); final String username = mediaUser == null ? "" : mediaUser.getUsername();
if (downloadDir == null) return; final List<String> userFolderPaths = getSubPathForUserFolder(username);
// final DocumentFile downloadDir = getDownloadDir(context, mediaUser == null ? "" : mediaUser.getUsername());
switch (media.getMediaType()) { switch (media.getMediaType()) {
case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO: { case MEDIA_TYPE_VIDEO: {
@ -323,8 +481,10 @@ public final class DownloadUtils {
fileName = mediaUser.getUsername() + "_" + fileName; fileName = mediaUser.getUsername() + "_" + fileName;
} }
} }
final File file = getDownloadSaveFile(downloadDir, fileName, url); final Pair<List<String>, String> pair = getDownloadSavePaths(userFolderPaths, fileName, url);
map.put(url, file.getAbsolutePath()); final DocumentFile file = createFile(pair);
if (file == null) continue;
map.put(url, file);
break; break;
} }
case MEDIA_TYPE_VOICE: { case MEDIA_TYPE_VOICE: {
@ -333,29 +493,54 @@ public final class DownloadUtils {
if (mediaUser != null) { if (mediaUser != null) {
fileName = mediaUser.getUsername() + "_" + fileName; fileName = mediaUser.getUsername() + "_" + fileName;
} }
final File file = getDownloadSaveFile(downloadDir, fileName, url); final Pair<List<String>, String> pair = getDownloadSavePaths(userFolderPaths, fileName, url);
map.put(url, file.getAbsolutePath()); final DocumentFile file = createFile(pair);
if (file == null) continue;
map.put(url, file);
break; break;
} }
case MEDIA_TYPE_SLIDER: case MEDIA_TYPE_SLIDER:
final List<Media> sliderItems = media.getCarouselMedia(); final List<Media> sliderItems = media.getCarouselMedia();
for (int i = 0; i < sliderItems.size(); i++) { for (int i = 0; i < sliderItems.size(); i++) {
if (childPositionIfSingle >= 0 && feedModels.size() == 1 && i != childPositionIfSingle) { if (childPositionIfSingle >= 0 && feedModels.size() == 1 && i != childPositionIfSingle) continue;
continue;
}
final Media child = sliderItems.get(i); final Media child = sliderItems.get(i);
final String url = getUrlOfType(child); final String url = getUrlOfType(child);
final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null ? mediaUser.getUsername() : ""; final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null
final File file = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url, usernamePrepend); ? mediaUser.getUsername()
map.put(url, file.getAbsolutePath()); : "";
final Pair<List<String>, String> pair = getDownloadChildSavePaths(
new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, usernamePrepend);
final DocumentFile file = createFile(pair);
if (file == null) continue;
map.put(url, file);
} }
break; break;
default: default:
} }
} }
if (map.isEmpty()) return;
download(context, map); download(context, map);
} }
@Nullable
private static DocumentFile createFile(@NonNull final Pair<List<String>, String> pair) {
if (root == null) return null;
if (pair.first == null || pair.second == null) return null;
DocumentFile dir = root;
final List<String> first = pair.first;
for (int i = 0; i < first.size(); i++) {
final String name = first.get(i);
final DocumentFile file = dir.findFile(name);
if (file != null) {
dir = file;
continue;
}
dir = i == first.size() - 1 ? dir.createFile(pair.second, name) : dir.createDirectory(name);
if (dir == null) break;
}
return dir;
}
@Nullable @Nullable
private static String getUrlOfType(@NonNull final Media media) { private static String getUrlOfType(@NonNull final Media media) {
switch (media.getMediaType()) { switch (media.getMediaType()) {
@ -387,12 +572,13 @@ public final class DownloadUtils {
public static void download(final Context context, public static void download(final Context context,
final String url, final String url,
final String filePath) { final DocumentFile filePath) {
if (context == null || url == null || filePath == null) return; if (context == null || url == null || filePath == null) return;
download(context, Collections.singletonMap(url, filePath)); download(context, Collections.singletonMap(url, filePath));
} }
private static void download(final Context context, final Map<String, String> urlFilePathMap) { private static void download(final Context context, final Map<String, DocumentFile> urlFilePathMap) {
if (context == null) return;
final Constraints constraints = new Constraints.Builder() final Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build(); .build();
@ -400,19 +586,25 @@ public final class DownloadUtils {
.setUrlToFilePathMap(urlFilePathMap) .setUrlToFilePathMap(urlFilePathMap)
.build(); .build();
final String requestJson = new Gson().toJson(request); final String requestJson = new Gson().toJson(request);
final File tempFile = getTempFile(); final DocumentFile tempFile = getTempFile(null, "json");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { if (tempFile == null) {
Log.e(TAG, "download: temp file is null");
return;
}
final Uri uri = tempFile.getUri();
final ContentResolver contentResolver = context.getContentResolver();
if (contentResolver == null) return;
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(contentResolver.openOutputStream(uri)))) {
writer.write(requestJson); writer.write(requestJson);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "download: Error writing request to file", e); Log.e(TAG, "download: Error writing request to file", e);
//noinspection ResultOfMethodCallIgnored
tempFile.delete(); tempFile.delete();
return; return;
} }
final WorkRequest downloadWorkRequest = new OneTimeWorkRequest.Builder(DownloadWorker.class) final WorkRequest downloadWorkRequest = new OneTimeWorkRequest.Builder(DownloadWorker.class)
.setInputData( .setInputData(
new Data.Builder() new Data.Builder()
.putString(DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON, tempFile.getAbsolutePath()) .putString(DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON, tempFile.getUri().toString())
.build() .build()
) )
.setConstraints(constraints) .setConstraints(constraints)
@ -421,4 +613,30 @@ public final class DownloadUtils {
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueue(downloadWorkRequest); .enqueue(downloadWorkRequest);
} }
@Nullable
public static Uri getRootDirUri() {
return root != null ? root.getUri() : null;
}
public static class ReselectDocumentTreeException extends Exception {
private final Uri initialUri;
public ReselectDocumentTreeException() {
initialUri = null;
}
public ReselectDocumentTreeException(final String message) {
super(message);
initialUri = null;
}
public ReselectDocumentTreeException(final Uri initialUri) {
this.initialUri = initialUri;
}
public Uri getInitialUri() {
return initialUri;
}
}
} }

View File

@ -2,6 +2,7 @@ package awais.instagrabber.utils;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -20,9 +21,8 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.File; import java.io.InputStream;
import java.io.FileInputStream; import java.io.OutputStream;
import java.io.FileOutputStream;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
@ -55,14 +55,15 @@ public final class ExportImportUtils {
public static void importData(@NonNull final Context context, public static void importData(@NonNull final Context context,
@ExportImportFlags final int flags, @ExportImportFlags final int flags,
@NonNull final File file, @NonNull final Uri uri,
final String password, final String password,
final FetchListener<Boolean> fetchListener) throws IncorrectPasswordException { final FetchListener<Boolean> fetchListener) throws IncorrectPasswordException {
try (final FileInputStream fis = new FileInputStream(file)) { try (final InputStream stream = context.getContentResolver().openInputStream(uri)) {
final int configType = fis.read(); if (stream == null) return;
final int configType = stream.read();
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
int c; int c;
while ((c = fis.read()) != -1) { while ((c = stream.read()) != -1) {
builder.append((char) c); builder.append((char) c);
} }
if (configType == 'A') { if (configType == 'A') {
@ -223,9 +224,11 @@ public final class ExportImportUtils {
} }
} }
public static boolean isEncrypted(final File file) { public static boolean isEncrypted(@NonNull final Context context,
try (final FileInputStream fis = new FileInputStream(file)) { @NonNull final Uri uri) {
final int configType = fis.read(); try (final InputStream stream = context.getContentResolver().openInputStream(uri)) {
if (stream == null) return false;
final int configType = stream.read();
if (configType == 'A') { if (configType == 'A') {
return true; return true;
} }
@ -237,7 +240,7 @@ public final class ExportImportUtils {
public static void exportData(@NonNull final Context context, public static void exportData(@NonNull final Context context,
@ExportImportFlags final int flags, @ExportImportFlags final int flags,
@NonNull final File filePath, @NonNull final Uri uri,
final String password, final String password,
final FetchListener<Boolean> fetchListener) { final FetchListener<Boolean> fetchListener) {
getExportString(flags, context, exportString -> { getExportString(flags, context, exportString -> {
@ -258,15 +261,20 @@ public final class ExportImportUtils {
exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING);
} }
if (exportBytes != null && exportBytes.length > 1) { if (exportBytes != null && exportBytes.length > 1) {
try (final FileOutputStream fos = new FileOutputStream(filePath)) { try (final OutputStream stream = context.getContentResolver().openOutputStream(uri)) {
fos.write(isPass ? 'A' : 'Z'); if (stream == null) return;
fos.write(exportBytes); stream.write(isPass ? 'A' : 'Z');
stream.write(exportBytes);
if (fetchListener != null) fetchListener.onResult(true); if (fetchListener != null) fetchListener.onResult(true);
} catch (final Exception e) { } catch (Exception e) {
if (fetchListener != null) fetchListener.onResult(false); if (fetchListener != null) fetchListener.onResult(false);
if (BuildConfig.DEBUG) Log.e(TAG, "", e); if (BuildConfig.DEBUG) Log.e(TAG, "", e);
} }
} else if (fetchListener != null) fetchListener.onResult(false); return;
}
if (fetchListener != null) {
fetchListener.onResult(false);
}
}); });
} }

View File

@ -3,6 +3,7 @@ package awais.instagrabber.utils
import android.content.ContentResolver import android.content.ContentResolver
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import awais.instagrabber.models.UploadVideoOptions import awais.instagrabber.models.UploadVideoOptions
import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -12,8 +13,6 @@ import okio.BufferedSink
import okio.Okio import okio.Okio
import org.json.JSONObject import org.json.JSONObject
import ru.gildor.coroutines.okhttp.await import ru.gildor.coroutines.okhttp.await
import java.io.File
import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -29,20 +28,23 @@ object MediaUploader {
): MediaUploadResponse = withContext(Dispatchers.IO) { ): MediaUploadResponse = withContext(Dispatchers.IO) {
val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false)
val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null")
uploadPhoto(bitmap) uploadPhoto(contentResolver, bitmap)
} }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
private suspend fun uploadPhoto( private suspend fun uploadPhoto(
contentResolver: ContentResolver,
bitmap: Bitmap, bitmap: Bitmap,
): MediaUploadResponse = withContext(Dispatchers.IO) { ): MediaUploadResponse = withContext(Dispatchers.IO) {
val file: File = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) val file: DocumentFile = BitmapUtils.convertToJpegAndSaveToFile(contentResolver, bitmap, null)
val byteLength: Long = file.length() val byteLength: Long = file.length()
val options = createUploadPhotoOptions(byteLength) val options = createUploadPhotoOptions(byteLength)
val headers = getUploadPhotoHeaders(options) val headers = getUploadPhotoHeaders(options)
val url = HOST + "/rupload_igphoto/" + options.name + "/" val url = HOST + "/rupload_igphoto/" + options.name + "/"
try { try {
FileInputStream(file).use { input -> upload(input, url, headers) } contentResolver.openInputStream(file.uri).use { input ->
upload(input!!, url, headers)
}
} finally { } finally {
file.delete() file.delete()
} }

View File

@ -2,13 +2,17 @@ package awais.instagrabber.utils;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.database.Cursor; import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.FileDescriptor;
public final class MediaUtils { public final class MediaUtils {
private static final String TAG = MediaUtils.class.getSimpleName(); private static final String TAG = MediaUtils.class.getSimpleName();
private static final String[] PROJECTION_VIDEO = { private static final String[] PROJECTION_VIDEO = {
@ -28,9 +32,7 @@ public final class MediaUtils {
AppExecutors.INSTANCE.getTasksThread().submit(() -> { AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_VIDEO)) { try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_VIDEO)) {
if (cursor == null) { if (cursor == null) {
if (listener != null) {
listener.onLoad(null); listener.onLoad(null);
}
return; return;
} }
int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION); int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
@ -38,7 +40,6 @@ public final class MediaUtils {
int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT); int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT);
int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE); int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
if (cursor.moveToNext()) { if (cursor.moveToNext()) {
if (listener != null) {
listener.onLoad(new VideoInfo( listener.onLoad(new VideoInfo(
cursor.getLong(durationColumn), cursor.getLong(durationColumn),
cursor.getInt(widthColumn), cursor.getInt(widthColumn),
@ -46,17 +47,10 @@ public final class MediaUtils {
cursor.getLong(sizeColumn) cursor.getLong(sizeColumn)
)); ));
} }
}
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "getVideoInfo: ", e); Log.e(TAG, "getVideoInfo: ", e);
if (listener != null) {
listener.onFailure(e); listener.onFailure(e);
} }
return;
}
if (listener != null) {
listener.onLoad(null);
}
}); });
} }
@ -64,35 +58,26 @@ public final class MediaUtils {
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) { @NonNull final OnInfoLoadListener<VideoInfo> listener) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> { AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_AUDIO)) { try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) {
if (cursor == null) { if (parcelFileDescriptor == null) {
if (listener != null) {
listener.onLoad(null); listener.onLoad(null);
}
return; return;
} }
int durationColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION); final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
int sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE); final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
if (cursor.moveToNext()) { mediaMetadataRetriever.setDataSource(fileDescriptor);
if (listener != null) { String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (TextUtils.isEmpty(duration)) duration = "0";
listener.onLoad(new VideoInfo( listener.onLoad(new VideoInfo(
cursor.getLong(durationColumn), Long.parseLong(duration),
0, 0,
0, 0,
cursor.getLong(sizeColumn) 0
)); ));
}
}
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "getVoiceInfo: ", e); Log.e(TAG, "getVoiceInfo: ", e);
if (listener != null) {
listener.onFailure(e); listener.onFailure(e);
} }
return;
}
if (listener != null) {
listener.onLoad(null);
}
}); });
} }

View File

@ -13,17 +13,15 @@ import androidx.fragment.app.Fragment;
import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.RECORD_AUDIO; import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static androidx.core.content.PermissionChecker.checkSelfPermission; import static androidx.core.content.PermissionChecker.checkSelfPermission;
public class PermissionUtils { public class PermissionUtils {
public static final String[] AUDIO_RECORD_PERMS = new String[]{WRITE_EXTERNAL_STORAGE, RECORD_AUDIO}; public static final String[] AUDIO_RECORD_PERMS = new String[]{RECORD_AUDIO};
public static final String[] ATTACH_MEDIA_PERMS = new String[]{READ_EXTERNAL_STORAGE}; public static final String[] ATTACH_MEDIA_PERMS = new String[]{READ_EXTERNAL_STORAGE};
public static final String[] CAMERA_PERMS = new String[]{CAMERA}; public static final String[] CAMERA_PERMS = new String[]{CAMERA};
public static boolean hasAudioRecordPerms(@NonNull final Context context) { public static boolean hasAudioRecordPerms(@NonNull final Context context) {
return checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PermissionChecker.PERMISSION_GRANTED return checkSelfPermission(context, RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED;
&& checkSelfPermission(context, RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED;
} }
public static void requestAudioRecordPerms(final Fragment fragment, final int requestCode) { public static void requestAudioRecordPerms(final Fragment fragment, final int requestCode) {

View File

@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT;
@ -159,7 +160,7 @@ public final class SettingsHelper {
CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME,
PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT,
PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT,
STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, PREF_TAB_ORDER}) STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, PREF_TAB_ORDER, PREF_BARINSTA_DIR_URI})
public @interface StringSettings {} public @interface StringSettings {}
@StringDef({DOWNLOAD_USER_FOLDER, DOWNLOAD_PREPEND_USER_NAME, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, MUTED_VIDEOS, @StringDef({DOWNLOAD_USER_FOLDER, DOWNLOAD_PREPEND_USER_NAME, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, MUTED_VIDEOS,

View File

@ -13,11 +13,12 @@ import android.graphics.Color;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.OnScanCompletedListener;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.Browser; import android.provider.Browser;
import android.provider.DocumentsContract;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -37,6 +38,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MediatorLiveData;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
@ -46,12 +48,13 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.google.common.io.Files;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.File; import java.io.File;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -66,6 +69,8 @@ import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.Tab; import awais.instagrabber.models.Tab;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI;
public final class Utils { public final class Utils {
private static final String TAG = "Utils"; private static final String TAG = "Utils";
private static final int VIDEO_CACHE_MAX_BYTES = 10 * 1024 * 1024; private static final int VIDEO_CACHE_MAX_BYTES = 10 * 1024 * 1024;
@ -82,6 +87,7 @@ public final class Utils {
public static String cacheDir; public static String cacheDir;
public static String tabOrderString; public static String tabOrderString;
private static int defaultStatusBarColor; private static int defaultStatusBarColor;
private static Object[] volumes;
public static int convertDpToPx(final float dp) { public static int convertDpToPx(final float dp) {
return Math.round((dp * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); return Math.round((dp * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
@ -346,18 +352,18 @@ public final class Utils {
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
} }
public static void mediaScanFile(@NonNull final Context context, // public static void mediaScanFile(@NonNull final Context context,
@NonNull File file, // @NonNull File file,
@NonNull final OnScanCompletedListener callback) { // @NonNull final OnScanCompletedListener callback) {
//noinspection UnstableApiUsage // //noinspection UnstableApiUsage
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(Files.getFileExtension(file.getName())); // final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(Files.getFileExtension(file.getName()));
MediaScannerConnection.scanFile( // MediaScannerConnection.scanFile(
context, // context,
new String[]{file.getAbsolutePath()}, // new String[]{file.getAbsolutePath()},
new String[]{mimeType}, // new String[]{mimeType},
callback // callback
); // );
} // }
public static void showKeyboard(@NonNull final View view) { public static void showKeyboard(@NonNull final View view) {
final Context context = view.getContext(); final Context context = view.getContext();
@ -524,6 +530,73 @@ public final class Utils {
return tabOrderString.contains(navRootString); return tabOrderString.contains(navRootString);
} }
// public static void scanDocumentFile(@NonNull final Context context,
// @NonNull final DocumentFile documentFile,
// @NonNull final OnScanCompletedListener callback) {
// if (!documentFile.isFile() || !documentFile.exists()) {
// Log.d(TAG, "scanDocumentFile: " + documentFile);
// callback.onScanCompleted(null, null);
// return;
// }
// File file = null;
// try {
// file = getDocumentFileRealPath(context, documentFile);
// } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
// Log.e(TAG, "scanDocumentFile: ", e);
// }
// if (file == null) return;
// MediaScannerConnection.scanFile(context,
// new String[]{file.getAbsolutePath()},
// new String[]{documentFile.getType()},
// callback);
// }
public static File getDocumentFileRealPath(@NonNull final Context context,
@NonNull final DocumentFile documentFile)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final String docId = DocumentsContract.getDocumentId(documentFile.getUri());
final String[] split = docId.split(":");
final String type = split[0];
if (type.equalsIgnoreCase("primary")) {
return new File(Environment.getExternalStorageDirectory(), split[1]);
} else if (type.equalsIgnoreCase("raw")) {
return new File(split[1]);
} else {
if (volumes == null) {
final StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (sm == null) return null;
final Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList");
volumes = (Object[]) getVolumeListMethod.invoke(sm);
}
if (volumes == null) return null;
for (Object volume : volumes) {
final Method getUuidMethod = volume.getClass().getMethod("getUuid");
final String uuid = (String) getUuidMethod.invoke(volume);
if (uuid != null && uuid.equalsIgnoreCase(type)) {
final Method getPathMethod = volume.getClass().getMethod("getPath");
final String path = (String) getPathMethod.invoke(volume);
return new File(path, split[1]);
}
}
}
return null;
}
public static void setupSelectedDir(@NonNull final Context context,
@NonNull final Intent intent) throws DownloadUtils.ReselectDocumentTreeException {
final Uri dirUri = intent.getData();
Log.d(TAG, "onActivityResult: " + dirUri);
if (dirUri == null) return;
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(dirUri, takeFlags);
settingsHelper.putString(PREF_BARINSTA_DIR_URI, dirUri.toString());
// re-init DownloadUtils
DownloadUtils.init(context);
}
@NonNull @NonNull
public static Point getNavigationBarSize(@NonNull Context context) { public static Point getNavigationBarSize(@NonNull Context context) {
Point appUsableSize = getAppUsableScreenSize(context); Point appUsableSize = getAppUsableScreenSize(context);

View File

@ -1,13 +1,18 @@
package awais.instagrabber.utils; package awais.instagrabber.utils;
import android.app.Application;
import android.content.ContentResolver;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.File; import java.io.File;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -27,28 +32,30 @@ public class VoiceRecorder {
private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(FILE_FORMAT, Locale.US); private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(FILE_FORMAT, Locale.US);
private final List<Float> waveform = new ArrayList<>(); private final List<Float> waveform = new ArrayList<>();
private final File recordingsDir; private final DocumentFile recordingsDir;
private final VoiceRecorderCallback callback; private final VoiceRecorderCallback callback;
private MediaRecorder recorder; private MediaRecorder recorder;
private File audioTempFile; private DocumentFile audioTempFile;
private MaxAmpHandler maxAmpHandler; private MaxAmpHandler maxAmpHandler;
private boolean stopped; private boolean stopped;
public VoiceRecorder(@NonNull final File recordingsDir, final VoiceRecorderCallback callback) { public VoiceRecorder(@NonNull final DocumentFile recordingsDir, final VoiceRecorderCallback callback) {
this.recordingsDir = recordingsDir; this.recordingsDir = recordingsDir;
this.callback = callback; this.callback = callback;
} }
public void startRecording() { public void startRecording(final ContentResolver contentResolver) {
stopped = false; stopped = false;
ParcelFileDescriptor parcelFileDescriptor = null;
try { try {
recorder = new MediaRecorder(); recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
deleteTempAudioFile(); deleteTempAudioFile();
audioTempFile = getAudioRecordFile(); audioTempFile = getAudioRecordFile();
recorder.setOutputFile(audioTempFile.getAbsolutePath()); parcelFileDescriptor = contentResolver.openFileDescriptor(audioTempFile.getUri(), "rwt");
recorder.setOutputFile(parcelFileDescriptor.getFileDescriptor());
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
recorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); recorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
recorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); recorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
@ -63,6 +70,12 @@ public class VoiceRecorder {
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Audio recording failed", e); Log.e(TAG, "Audio recording failed", e);
deleteTempAudioFile(); deleteTempAudioFile();
} finally {
if (parcelFileDescriptor != null) {
try {
parcelFileDescriptor.close();
} catch (IOException ignored) {}
}
} }
} }
@ -140,9 +153,13 @@ public class VoiceRecorder {
// } // }
@NonNull @NonNull
private File getAudioRecordFile() { private DocumentFile getAudioRecordFile() {
final String name = String.format("%s-%s.%s", FILE_PREFIX, LocalDateTime.now().format(SIMPLE_DATE_FORMAT), EXTENSION); final String name = String.format("%s-%s.%s", FILE_PREFIX, LocalDateTime.now().format(SIMPLE_DATE_FORMAT), EXTENSION);
return new File(recordingsDir, name); DocumentFile file = recordingsDir.findFile(name);
if (file == null || !file.exists()) {
file = recordingsDir.createFile(MIME_TYPE, name);
}
return file;
} }
private void deleteTempAudioFile() { private void deleteTempAudioFile() {
@ -160,11 +177,11 @@ public class VoiceRecorder {
public static class VoiceRecordingResult { public static class VoiceRecordingResult {
private final String mimeType; private final String mimeType;
private final File file; private final DocumentFile file;
private final List<Float> waveform; private final List<Float> waveform;
private final int samplingFreq = 10; private final int samplingFreq = 10;
public VoiceRecordingResult(final String mimeType, final File file, final List<Float> waveform) { public VoiceRecordingResult(final String mimeType, final DocumentFile file, final List<Float> waveform) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.file = file; this.file = file;
this.waveform = waveform; this.waveform = waveform;
@ -174,7 +191,7 @@ public class VoiceRecorder {
return mimeType; return mimeType;
} }
public File getFile() { public DocumentFile getFile() {
return file; return file;
} }

View File

@ -1,10 +1,10 @@
package awais.instagrabber.viewmodels package awais.instagrabber.viewmodels
import android.R.attr
import android.app.Application import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.util.Log import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.* import androidx.lifecycle.*
import awais.instagrabber.customviews.emoji.Emoji import awais.instagrabber.customviews.emoji.Emoji
import awais.instagrabber.managers.DirectMessagesManager import awais.instagrabber.managers.DirectMessagesManager
@ -23,10 +23,9 @@ import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.VoiceRecorder.VoiceRecorderCallback import awais.instagrabber.utils.VoiceRecorder.VoiceRecorderCallback
import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult
import awais.instagrabber.utils.extensions.TAG
import java.io.File
import java.util.* import java.util.*
class DirectThreadViewModel( class DirectThreadViewModel(
application: Application, application: Application,
val threadId: String, val threadId: String,
@ -37,7 +36,7 @@ class DirectThreadViewModel(
// private static final String ERROR_INVALID_THREAD = "Invalid thread"; // private static final String ERROR_INVALID_THREAD = "Invalid thread";
private val contentResolver: ContentResolver = application.contentResolver private val contentResolver: ContentResolver = application.contentResolver
private val recordingsDir: File = DirectoryUtils.getOutputMediaDirectory(application, "Recordings") private val recordingsDir: DocumentFile? = DownloadUtils.getRecordingsDir()
private var voiceRecorder: VoiceRecorder? = null private var voiceRecorder: VoiceRecorder? = null
private lateinit var threadManager: ThreadManager private lateinit var threadManager: ThreadManager
@ -87,33 +86,24 @@ class DirectThreadViewModel(
fun startRecording(): LiveData<Resource<Any?>> { fun startRecording(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
voiceRecorder = VoiceRecorder(recordingsDir, object : VoiceRecorderCallback { voiceRecorder = VoiceRecorder(recordingsDir!!, object : VoiceRecorderCallback {
override fun onStart() {} override fun onStart() {}
override fun onComplete(result: VoiceRecordingResult) { override fun onComplete(result: VoiceRecordingResult) {
Log.d(TAG, "onComplete: recording complete. Scanning file...") // Log.d(TAG, "onComplete: recording complete. Scanning file...");
MediaScannerConnection.scanFile( MediaUtils.getVoiceInfo(
getApplication(), contentResolver,
arrayOf(result.file.absolutePath), result.file.uri,
arrayOf(result.mimeType) object : OnInfoLoadListener<VideoInfo?> {
) { _: String?, uri: Uri? ->
if (uri == null) {
val msg = "Scan failed!"
Log.e(TAG, msg)
data.postValue(error(msg, null))
return@scanFile
}
Log.d(TAG, "onComplete: scan complete")
MediaUtils.getVoiceInfo(contentResolver, uri, object : OnInfoLoadListener<VideoInfo?> {
override fun onLoad(videoInfo: VideoInfo?) { override fun onLoad(videoInfo: VideoInfo?) {
if (videoInfo == null) return if (videoInfo == null) return
threadManager.sendVoice( threadManager.sendVoice(
data, data,
uri, result.file.uri,
result.waveform, result.waveform,
result.samplingFreq, result.samplingFreq,
videoInfo.duration, videoInfo.duration,
videoInfo.size, result.file.length(),
viewModelScope, viewModelScope
) )
} }
@ -122,11 +112,10 @@ class DirectThreadViewModel(
} }
}) })
} }
}
override fun onCancel() {} override fun onCancel() {}
}) })
voiceRecorder?.startRecording() voiceRecorder?.startRecording(contentResolver)
return data return data
} }

View File

@ -0,0 +1,113 @@
package awais.instagrabber.viewmodels;
import android.app.Application;
import android.content.Intent;
import android.content.UriPermission;
import android.net.Uri;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH;
public class DirectorySelectActivityViewModel extends AndroidViewModel {
private static final String TAG = DirectorySelectActivityViewModel.class.getSimpleName();
private final MutableLiveData<String> message = new MutableLiveData<>();
private final MutableLiveData<String> prevUri = new MutableLiveData<>();
private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> dirSuccess = new MutableLiveData<>(false);
public DirectorySelectActivityViewModel(final Application application) {
super(application);
}
public LiveData<String> getMessage() {
return message;
}
public LiveData<String> getPrevUri() {
return prevUri;
}
public LiveData<Boolean> isLoading() {
return loading;
}
public LiveData<Boolean> getDirSuccess() {
return dirSuccess;
}
public void setInitialUri(final Intent intent) {
if (intent == null) {
setMessage(null);
return;
}
final Parcelable initialUriParcelable = intent.getParcelableExtra(Constants.EXTRA_INITIAL_URI);
if (!(initialUriParcelable instanceof Uri)) {
setMessage(null);
return;
}
setMessage((Uri) initialUriParcelable);
}
private void setMessage(@Nullable final Uri initialUri) {
if (initialUri == null) {
final String prevVersionFolderPath = Utils.settingsHelper.getString(FOLDER_PATH);
if (TextUtils.isEmpty(prevVersionFolderPath)) {
// default message
message.postValue(getApplication().getString(R.string.dir_select_default_message));
prevUri.postValue(null);
return;
}
message.postValue(getApplication().getString(R.string.dir_select_reselect_message));
prevUri.postValue(prevVersionFolderPath);
return;
}
final List<UriPermission> existingPermissions = getApplication().getContentResolver().getPersistedUriPermissions();
final boolean anyMatch = existingPermissions.stream().anyMatch(uriPermission -> uriPermission.getUri().equals(initialUri));
final DocumentFile documentFile = DocumentFile.fromSingleUri(getApplication(), initialUri);
String path;
try {
path = URLDecoder.decode(initialUri.toString(), StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
path = initialUri.toString();
}
if (!anyMatch) {
message.postValue(getApplication().getString(R.string.dir_select_permission_revoked_message));
prevUri.postValue(path);
return;
}
if (documentFile == null || !documentFile.exists() || documentFile.lastModified() == 0) {
message.postValue(getApplication().getString(R.string.dir_select_folder_not_exist));
prevUri.postValue(path);
}
}
public void setupSelectedDir(@NonNull final Intent data) throws DownloadUtils.ReselectDocumentTreeException {
loading.postValue(true);
try {
Utils.setupSelectedDir(getApplication(), data);
message.postValue(getApplication().getString(R.string.dir_select_success_message));
dirSuccess.postValue(true);
} finally {
loading.postValue(false);
}
}
}

View File

@ -5,6 +5,7 @@ import android.graphics.RectF;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
@ -24,8 +25,9 @@ import awais.instagrabber.fragments.imageedit.filters.filters.Filter;
import awais.instagrabber.fragments.imageedit.filters.properties.Property; import awais.instagrabber.fragments.imageedit.filters.properties.Property;
import awais.instagrabber.models.SavedImageEditState; import awais.instagrabber.models.SavedImageEditState;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.DirectoryUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.SerializablePair; import awais.instagrabber.utils.SerializablePair;
import awais.instagrabber.utils.Utils;
import jp.co.cyberagent.android.gpuimage.GPUImage; import jp.co.cyberagent.android.gpuimage.GPUImage;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup;
@ -34,6 +36,7 @@ public class ImageEditViewModel extends AndroidViewModel {
private static final String CROP = "crop"; private static final String CROP = "crop";
private static final String RESULT = "result"; private static final String RESULT = "result";
private static final String FILE_FORMAT = "yyyyMMddHHmmssSSS"; private static final String FILE_FORMAT = "yyyyMMddHHmmssSSS";
private static final String MIME_TYPE = Utils.mimeTypeMap.getMimeTypeFromExtension("jpg");
private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(FILE_FORMAT, Locale.US); private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(FILE_FORMAT, Locale.US);
private Uri originalUri; private Uri originalUri;
@ -48,18 +51,18 @@ public class ImageEditViewModel extends AndroidViewModel {
private final MutableLiveData<Boolean> isCropped = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isCropped = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isTuned = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isTuned = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isFiltered = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isFiltered = new MutableLiveData<>(false);
private final File outputDir; private final DocumentFile outputDir;
private List<Filter<? extends GPUImageFilter>> tuningFilters; private List<Filter<? extends GPUImageFilter>> tuningFilters;
private Filter<? extends GPUImageFilter> appliedFilter; private Filter<? extends GPUImageFilter> appliedFilter;
private final File destinationFile; private final DocumentFile destinationFile;
public ImageEditViewModel(final Application application) { public ImageEditViewModel(final Application application) {
super(application); super(application);
sessionId = LocalDateTime.now().format(SIMPLE_DATE_FORMAT); sessionId = LocalDateTime.now().format(SIMPLE_DATE_FORMAT);
outputDir = DirectoryUtils.getOutputMediaDirectory(application, "Edit", sessionId); outputDir = DownloadUtils.getImageEditDir(sessionId);
destinationFile = new File(outputDir, RESULT + ".jpg"); destinationFile = outputDir.createFile(MIME_TYPE, RESULT + ".jpg");
destinationUri = Uri.fromFile(destinationFile); destinationUri = destinationFile.getUri();
cropDestinationUri = Uri.fromFile(new File(outputDir, CROP + ".jpg")); cropDestinationUri = outputDir.createFile(MIME_TYPE, CROP + ".jpg").getUri();
} }
public String getSessionId() { public String getSessionId() {
@ -159,16 +162,15 @@ public class ImageEditViewModel extends AndroidViewModel {
delete(outputDir); delete(outputDir);
} }
private void delete(@NonNull final File file) { private void delete(@NonNull final DocumentFile file) {
if (file.isDirectory()) { if (file.isDirectory()) {
final File[] files = file.listFiles(); final DocumentFile[] files = file.listFiles();
if (files != null) { if (files != null) {
for (File f : files) { for (DocumentFile f : files) {
delete(f); delete(f);
} }
} }
} }
//noinspection ResultOfMethodCallIgnored
file.delete(); file.delete();
} }
@ -206,9 +208,9 @@ public class ImageEditViewModel extends AndroidViewModel {
return new SerializablePair<>(type, propertyValueMap); return new SerializablePair<>(type, propertyValueMap);
} }
public File getDestinationFile() { // public File getDestinationFile() {
return destinationFile; // return destinationFile;
} // }
public enum Tab { public enum Tab {
RESULT, RESULT,

View File

@ -6,8 +6,8 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
@ -15,7 +15,7 @@ import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
@ -37,11 +37,11 @@ import kotlinx.coroutines.withContext
import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.stream.Collectors
import kotlin.collections.Map
import kotlin.math.abs import kotlin.math.abs
class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
@ -55,9 +55,14 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
.build()) .build())
} }
val downloadRequestString: String val downloadRequestString: String
val requestFile = File(downloadRequestFilePath) val requestFile = Uri.parse(downloadRequestFilePath)
val context = applicationContext
val contentResolver = context.contentResolver ?: return Result.failure(Data.Builder()
.putString("error", "contentResolver is null")
.build())
try { try {
downloadRequestString = requestFile.bufferedReader().use { it.readText() } val scanner = Scanner(contentResolver.openInputStream(requestFile))
downloadRequestString = scanner.useDelimiter("\\A").next()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "doWork: ", e) Log.e(TAG, "doWork: ", e)
return Result.failure(Data.Builder() return Result.failure(Data.Builder()
@ -82,7 +87,7 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
val urlToFilePathMap = downloadRequest.urlToFilePathMap val urlToFilePathMap = downloadRequest.urlToFilePathMap
download(urlToFilePathMap) download(urlToFilePathMap)
Handler(Looper.getMainLooper()).postDelayed({ showSummary(urlToFilePathMap) }, 500) Handler(Looper.getMainLooper()).postDelayed({ showSummary(urlToFilePathMap) }, 500)
val deleted = requestFile.delete() val deleted = DocumentFile.fromSingleUri(context, requestFile)!!.delete()
if (!deleted) { if (!deleted) {
Log.w(TAG, "doWork: requestFile not deleted!") Log.w(TAG, "doWork: requestFile not deleted!")
} }
@ -94,10 +99,11 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
val entries = urlToFilePathMap.entries val entries = urlToFilePathMap.entries
var count = 1 var count = 1
val total = urlToFilePathMap.size val total = urlToFilePathMap.size
for ((url, value) in entries) { for ((url, uriString) in entries) {
updateDownloadProgress(notificationId, count, total, 0f) updateDownloadProgress(notificationId, count, total, 0f)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
download(notificationId, count, total, url, value) val file = DocumentFile.fromSingleUri(applicationContext, Uri.parse(uriString))
download(notificationId, count, total, url, file!!)
} }
count++ count++
} }
@ -111,47 +117,49 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
position: Int, position: Int,
total: Int, total: Int,
url: String, url: String,
filePath: String, filePath: DocumentFile,
) { ) {
val isJpg = filePath.endsWith("jpg") val context = applicationContext.let { it }
val contentResolver = context.contentResolver?.let { it } ?: return
val filePathType = filePath.type?.let { it } ?: return
val isJpg = filePathType.startsWith("image")
// using temp file approach to remove IPTC so that download progress can be reported // using temp file approach to remove IPTC so that download progress can be reported
val outFile = if (isJpg) DownloadUtils.getTempFile() else File(filePath) val outFile = if (isJpg) DownloadUtils.getTempFile(null, "jpg") else filePath
try { try {
val urlConnection = URL(url).openConnection() val urlConnection = URL(url).openConnection()
val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong() val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong()
var totalRead = 0f var totalRead = 0f
try { try {
BufferedInputStream(urlConnection.getInputStream()).use { bis -> BufferedInputStream(urlConnection.getInputStream()).use { bis ->
FileOutputStream(outFile).use { fos -> contentResolver.openOutputStream(outFile.uri).use { fos ->
val buffer = ByteArray(0x2000) val buffer = ByteArray(0x2000)
var count: Int var count: Int
while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) { while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) {
totalRead += count totalRead += count
fos.write(buffer, 0, count) fos!!.write(buffer, 0, count)
setProgressAsync(Data.Builder().putString(URL, url) setProgressAsync(Data.Builder().putString(URL, url)
.putFloat(PROGRESS, totalRead * 100f / fileSize) .putFloat(PROGRESS, totalRead * 100f / fileSize)
.build()) .build())
updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize) updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize)
} }
fos.flush() fos!!.flush()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.absolutePath, e) Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.name, e)
} }
if (isJpg) { if (isJpg) {
val finalFile = File(filePath)
try { try {
FileInputStream(outFile).use { fis -> contentResolver.openInputStream(outFile.uri).use { fis ->
FileOutputStream(finalFile).use { fos -> contentResolver.openOutputStream(filePath.uri).use { fos ->
val jpegIptcRewriter = JpegIptcRewriter() val jpegIptcRewriter = JpegIptcRewriter()
jpegIptcRewriter.removeIPTC(fis, fos) jpegIptcRewriter.removeIPTC(fis, fos)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error while removing iptc: url: " + url Log.e(TAG, "Error while removing iptc: url: " + url
+ ", tempFile: " + outFile.absolutePath + ", tempFile: " + outFile.name
+ ", finalFile: " + finalFile.absolutePath, e) + ", finalFile: " + filePath.name, e)
} }
val deleted = outFile.delete() val deleted = outFile.delete()
if (!deleted) { if (!deleted) {
@ -218,36 +226,69 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
return builder.build() return builder.build()
} }
private fun showSummary(urlToFilePathMap: Map<String, String>?) { private fun showSummary(urlToFilePathMap: Map<String, String>) {
val context = applicationContext val context = applicationContext
val filePaths = urlToFilePathMap!!.values val filePaths = urlToFilePathMap.mapNotNull { DocumentFile.fromSingleUri(context, Uri.parse(it.value)) }
val notifications: MutableList<NotificationCompat.Builder> = LinkedList() val notifications: MutableList<NotificationCompat.Builder> = LinkedList()
val notificationIds: MutableList<Int> = LinkedList() val notificationIds: MutableList<Int> = LinkedList()
var count = 1 var count = 1
for (filePath in filePaths) { for (filePath: DocumentFile in filePaths) {
val file = File(filePath) // final File file = new File(filePath);
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))) // context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, filePath.getUri()));
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null, null) // Utils.scanDocumentFile(context, filePath, (path, uri) -> {});
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) // final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val bitmap = getThumbnail(context, file, uri, contentResolver) var bitmap: Bitmap? = null
val mimeType = filePath.type // Utils.getMimeType(uri, contentResolver);
if (!isEmpty(mimeType)) {
if (mimeType!!.startsWith("image")) {
try {
contentResolver.openInputStream(filePath.uri).use { inputStream ->
bitmap = BitmapFactory.decodeStream(inputStream)
}
} catch (e: java.lang.Exception) {
if (BuildConfig.DEBUG) Log.e(TAG, "", e)
}
} else if (mimeType.startsWith("video")) {
val retriever = MediaMetadataRetriever()
try {
try {
retriever.setDataSource(context, filePath.uri)
} catch (e: java.lang.Exception) {
// retriever.setDataSource(file.getAbsolutePath());
Log.e(TAG, "showSummary: ", e)
}
bitmap = retriever.frameAtTime
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) try {
retriever.close()
} catch (e: java.lang.Exception) {
Log.e(TAG, "showSummary: ", e)
}
} catch (e: java.lang.Exception) {
Log.e(TAG, "", e)
}
}
}
val downloadComplete = context.getString(R.string.downloader_complete) val downloadComplete = context.getString(R.string.downloader_complete)
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, filePath.uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK .addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_FROM_BACKGROUND or Intent.FLAG_FROM_BACKGROUND
or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.putExtra(Intent.EXTRA_STREAM, uri) )
.putExtra(Intent.EXTRA_STREAM, filePath.uri)
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
context, context,
DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT
) )
val notificationId = notificationId + count val notificationId: Int = notificationId + count
notificationIds.add(notificationId) notificationIds.add(notificationId)
count++ count++
val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) val builder: NotificationCompat.Builder =
NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download) .setSmallIcon(R.drawable.ic_download)
.setContentText(null) .setContentText(null)
.setContentTitle(downloadComplete) .setContentTitle(downloadComplete)
@ -257,14 +298,18 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
.setGroup(NOTIF_GROUP_NAME + "_" + id) .setGroup(NOTIF_GROUP_NAME + "_" + id)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.addAction(R.drawable.ic_delete, .addAction(
R.drawable.ic_delete,
context.getString(R.string.delete), context.getString(R.string.delete),
DeleteImageIntentService.pendingIntent(context, filePath, notificationId)) DeleteImageIntentService.pendingIntent(context, filePath, notificationId)
)
if (bitmap != null) { if (bitmap != null) {
builder.setLargeIcon(bitmap) builder.setLargeIcon(bitmap)
.setStyle(NotificationCompat.BigPictureStyle() .setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(bitmap) .bigPicture(bitmap)
.bigLargeIcon(null)) .bigLargeIcon(null)
)
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
} }
notifications.add(builder) notifications.add(builder)
@ -348,13 +393,15 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti
class Builder { class Builder {
private var urlToFilePathMap: MutableMap<String, String> = mutableMapOf() private var urlToFilePathMap: MutableMap<String, String> = mutableMapOf()
fun setUrlToFilePathMap(urlToFilePathMap: MutableMap<String, String>): Builder { fun setUrlToFilePathMap(urlToFilePathMap: MutableMap<String, DocumentFile>): Builder {
this.urlToFilePathMap = urlToFilePathMap this.urlToFilePathMap = urlToFilePathMap
.mapValues { it.value.uri.toString() }
.toMutableMap()
return this return this
} }
fun addUrl(url: String, filePath: String): Builder { fun addUrl(url: String, filePath: DocumentFile): Builder {
urlToFilePathMap[url] = filePath urlToFilePathMap[url] = filePath.uri.toString()
return this return this
} }

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintBottom_toTopOf="@id/prev_uri"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginBottom="0dp"
tools:text="@string/dir_select_permission_revoked_message"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/prev_uri"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="monospace"
android:padding="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/blue_500"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/message2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_goneMarginBottom="0dp"
tools:text="content://something/something/content/content"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/message2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/dir_select_message2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/prev_uri"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/select_dir"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<com.google.android.material.button.MaterialButton
android:id="@+id/select_dir"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/select_folder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -69,5 +69,5 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/create_backup" /> android:text="@string/backup" />
</LinearLayout> </LinearLayout>

View File

@ -127,7 +127,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/restore_backup" android:text="@string/restore"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_password_divider" /> app:layout_constraintTop_toBottomOf="@id/bottom_password_divider" />

View File

@ -501,6 +501,16 @@
<string name="dm_remove_warning">If saved, all DM related features will be disabled on next launch</string> <string name="dm_remove_warning">If saved, all DM related features will be disabled on next launch</string>
<string name="copy_caption">Copy caption</string> <string name="copy_caption">Copy caption</string>
<string name="copy_reply">Copy reply</string> <string name="copy_reply">Copy reply</string>
<string name="restore">Restore</string>
<string name="backup">Backup</string>
<string name="dir_select_default_message">Select a folder where Barinsta can store downloads and temporary files.\n\nYou can change this later in More > Settings > Downloads.</string>
<string name="dir_select_reselect_message">Android has changed the way apps can access files and directories on storage. Currently Barinsta does not have permission to access the following folder:</string>
<string name="dir_select_permission_revoked_message">Permissions for the previously selected folder were revoked by the system:</string>
<string name="dir_select_folder_not_exist">The previously selected folder does not exist now:</string>
<string name="dir_select_message2">Re-select the directory or select a new directory by clicking the button below.</string>
<string name="select_a_folder">No folder selected!</string>
<string name="dir_select_success_message">Success! Please wait. Starting app…</string>
<string name="barinsta_folder">Barinsta folder</string>
<string name="top">Top</string> <string name="top">Top</string>
<string name="recent">Recent</string> <string name="recent">Recent</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>