diff --git a/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt b/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt index cfdf4516..1eca69d8 100644 --- a/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/CameraActivity.kt @@ -4,22 +4,19 @@ import android.content.Intent import android.content.res.Configuration import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager.DisplayListener -import android.media.MediaScannerConnection -import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater -import android.webkit.MimeTypeMap import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.documentfile.provider.DocumentFile import awais.instagrabber.databinding.ActivityCameraBinding -import awais.instagrabber.utils.DirectoryUtils +import awais.instagrabber.utils.DownloadUtils import awais.instagrabber.utils.PermissionUtils import awais.instagrabber.utils.Utils import awais.instagrabber.utils.extensions.TAG -import com.google.common.io.Files -import java.io.File +import java.io.IOException import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.ExecutionException @@ -28,10 +25,10 @@ import java.util.concurrent.Executors class CameraActivity : BaseLanguageActivity() { private lateinit var binding: ActivityCameraBinding - private lateinit var outputDirectory: File private lateinit var displayManager: DisplayManager private lateinit var cameraExecutor: ExecutorService + private var outputDirectory: DocumentFile? = null private var imageCapture: ImageCapture? = null private var displayId = -1 private var cameraProvider: ProcessCameraProvider? = null @@ -55,7 +52,7 @@ class CameraActivity : BaseLanguageActivity() { setContentView(binding.root) Utils.transparentStatusBar(this, true, false) displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager - outputDirectory = DirectoryUtils.getOutputMediaDirectory(this, "Camera") + outputDirectory = DownloadUtils.getCameraDir() cameraExecutor = Executors.newSingleThreadExecutor() displayManager.registerDisplayListener(displayListener, null) binding.viewFinder.post { @@ -176,33 +173,28 @@ class CameraActivity : BaseLanguageActivity() { private fun takePhoto() { if (imageCapture == null) return - val photoFile = File(outputDirectory, simpleDateFormat.format(System.currentTimeMillis()) + ".jpg") - val outputFileOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + val fileName = simpleDateFormat.format(System.currentTimeMillis()) + ".jpg" + 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( outputFileOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback { @Suppress("UnstableApiUsage") override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { - val uri = Uri.fromFile(photoFile) - 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() - intent.data = uri1 - setResult(RESULT_OK, intent) - finish() - } - Log.d(TAG, "onImageSaved: $uri") + try { outputStream.close() } catch (ignored: IOException) {} + val intent = Intent() + intent.data = photoFile.uri + setResult(RESULT_OK, intent) + finish() + Log.d(TAG, "onImageSaved: " + photoFile.uri) } override fun onError(exception: ImageCaptureException) { Log.e(TAG, "onError: ", exception) + try { outputStream.close() } catch (ignored: IOException) {} } } ) diff --git a/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java index e76426c0..7fffbfec 100644 --- a/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java @@ -39,7 +39,7 @@ public class DirectorySelectActivity extends BaseLanguageActivity { viewModel = new ViewModelProvider(this).get(DirectorySelectActivityViewModel.class); setupObservers(); binding.selectDir.setOnClickListener(v -> openDirectoryChooser()); - AppExecutors.getInstance().mainThread().execute(() -> viewModel.setInitialUri(getIntent())); + AppExecutors.INSTANCE.getMainThread().execute(() -> viewModel.setInitialUri(getIntent())); } private void setupObservers() { @@ -81,7 +81,7 @@ public class DirectorySelectActivity extends BaseLanguageActivity { showErrorDialog(getString(R.string.select_a_folder)); return; } - AppExecutors.getInstance().mainThread().execute(() -> { + AppExecutors.INSTANCE.getMainThread().execute(() -> { try { viewModel.setupSelectedDir(data); final Intent intent = new Intent(this, MainActivity.class); diff --git a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java index ba3f5202..bbf2e35e 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java @@ -207,8 +207,7 @@ public class CreateBackupDialogFragment extends DialogFragment { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/octet-stream"); - final Date now = new Date(); - final String fileName = String.format("barinsta_%s.backup", BACKUP_FILE_DATE_TIME_FORMAT.format(now)); + 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 diff --git a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java index 78431b6f..c4d8ea87 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java @@ -115,7 +115,7 @@ public class RestoreBackupDialogFragment extends DialogFragment { binding.btnRestore.setEnabled(true); } uri = data.getData(); - AppExecutors.getInstance().mainThread().execute(() -> { + AppExecutors.INSTANCE.getMainThread().execute(() -> { Cursor c = null; try { String[] projection = {MediaStore.Files.FileColumns.DISPLAY_NAME}; diff --git a/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java b/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java index 7586bdce..034b6706 100644 --- a/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java @@ -183,11 +183,12 @@ public class ImageEditFragment extends Fragment { if (context == null) return; final Uri resultUri = viewModel.getResultUri().getValue(); 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); setNavControllerResult(navController, resultUri); navController.navigateUp(); - })); + }); + // Utils.mediaScanFile(context, new File(resultUri.toString()), (path, uri) -> ); }); } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt index bdc8a77c..bf1b7edf 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt @@ -19,6 +19,7 @@ object PreferenceKeys { const val APP_THEME = "app_theme_v19" const val APP_LANGUAGE = "app_language_v19" const val STORY_SORT = "story_sort" + const val PREF_BARINSTA_DIR_URI = "barinsta_dir_uri" // set string prefs const val KEYWORD_FILTERS = "keyword_filters" diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt index 379ea8db..4d6abced 100644 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.util.Log import android.util.LruCache import androidx.core.util.Pair +import androidx.documentfile.provider.DocumentFile import awais.instagrabber.utils.extensions.TAG import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -192,9 +193,9 @@ object BitmapUtils { } @Throws(IOException::class) - fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File { - val tempFile = file ?: DownloadUtils.getTempFile() - FileOutputStream(tempFile).use { output -> + fun convertToJpegAndSaveToFile(contentResolver: ContentResolver, bitmap: Bitmap, file: DocumentFile?): DocumentFile { + val tempFile = file ?: DownloadUtils.getTempFile(null, "jpg") + contentResolver.openOutputStream(tempFile.uri).use { output -> val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) if (!compressResult) { throw RuntimeException("Compression failed!") diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.kt b/app/src/main/java/awais/instagrabber/utils/Constants.kt index 8cf96e03..82b1f87e 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.kt +++ b/app/src/main/java/awais/instagrabber/utils/Constants.kt @@ -89,4 +89,5 @@ object Constants { const val DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id" const val DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title" const val X_IG_APP_ID = "936619743392459" + const val EXTRA_INITIAL_URI = "initial_uri" } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index 819c2b2c..755b6198 100644 --- a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java @@ -4,6 +4,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.UriPermission; +import android.Manifest; import android.net.Uri; import android.util.Log; import android.webkit.MimeTypeMap; @@ -59,6 +60,9 @@ public final class DownloadUtils { private static DocumentFile root; + 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 void init(@NonNull final Context context) throws ReselectDocumentTreeException { final String barinstaDirUri = Utils.settingsHelper.getString(PREF_BARINSTA_DIR_URI); if (TextUtils.isEmpty(barinstaDirUri)) { @@ -177,7 +181,7 @@ public final class DownloadUtils { private static List getSubPathForUserFolder(final String username) { final List list = new ArrayList<>(); - if (!Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) || TextUtils.isEmpty(username)) { + if (!Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_USER_FOLDER) || TextUtils.isEmpty(username)) { list.add(DIR_DOWNLOADS); return list; } @@ -425,7 +429,7 @@ public final class DownloadUtils { final String extension = DownloadUtils.getFileExtensionFromUrl(url); final String baseFileName = storyModel.getStoryMediaId() + "_" + storyModel.getTimestamp() + extension; - final String usernamePrepend = Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_PREPEND_USER_NAME) + final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && storyModel.getUsername() != null ? storyModel.getUsername() + "_" : ""; final String fileName = usernamePrepend + baseFileName; DocumentFile saveFile = downloadDir.findFile(fileName); @@ -501,7 +505,7 @@ public final class DownloadUtils { if (childPositionIfSingle >= 0 && feedModels.size() == 1 && i != childPositionIfSingle) continue; final Media child = sliderItems.get(i); final String url = getUrlOfType(child); - final String usernamePrepend = Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null + final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null ? mediaUser.getUsername() : ""; final Pair, String> pair = getDownloadChildSavePaths( diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt index 911b30c8..65fff446 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -3,6 +3,7 @@ package awais.instagrabber.utils import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri +import androidx.documentfile.provider.DocumentFile import awais.instagrabber.models.UploadVideoOptions import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor import kotlinx.coroutines.Dispatchers @@ -12,8 +13,6 @@ import okio.BufferedSink import okio.Okio import org.json.JSONObject import ru.gildor.coroutines.okhttp.await -import java.io.File -import java.io.FileInputStream import java.io.IOException import java.io.InputStream @@ -29,20 +28,23 @@ object MediaUploader { ): MediaUploadResponse = withContext(Dispatchers.IO) { val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") - uploadPhoto(bitmap) + uploadPhoto(contentResolver, bitmap) } @Suppress("BlockingMethodInNonBlockingContext") private suspend fun uploadPhoto( + contentResolver: ContentResolver, bitmap: Bitmap, ): 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 options = createUploadPhotoOptions(byteLength) val headers = getUploadPhotoHeaders(options) val url = HOST + "/rupload_igphoto/" + options.name + "/" try { - FileInputStream(file).use { input -> upload(input, url, headers) } + contentResolver.openInputStream(file.uri).use { input -> + upload(input!!, url, headers) + } } finally { file.delete() } diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUtils.java b/app/src/main/java/awais/instagrabber/utils/MediaUtils.java index f4084946..0d58a76f 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/MediaUtils.java @@ -57,7 +57,7 @@ public final class MediaUtils { public static void getVoiceInfo(@NonNull final ContentResolver contentResolver, @NonNull final Uri uri, @NonNull final OnInfoLoadListener listener) { - AppExecutors.getInstance().tasksThread().submit(() -> { + AppExecutors.INSTANCE.getTasksThread().submit(() -> { try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) { if (parcelFileDescriptor == null) { listener.onLoad(null); diff --git a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java index 75ec561f..266d814c 100644 --- a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java +++ b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java @@ -1,6 +1,7 @@ package awais.instagrabber.utils; import android.app.Application; +import android.content.ContentResolver; import android.media.MediaRecorder; import android.os.Handler; import android.os.Message; @@ -44,7 +45,7 @@ public class VoiceRecorder { this.callback = callback; } - public void startRecording(final Application application) { + public void startRecording(final ContentResolver contentResolver) { stopped = false; ParcelFileDescriptor parcelFileDescriptor = null; try { @@ -53,7 +54,7 @@ public class VoiceRecorder { recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); deleteTempAudioFile(); audioTempFile = getAudioRecordFile(); - parcelFileDescriptor = application.getContentResolver().openFileDescriptor(audioTempFile.getUri(), "rwt"); + parcelFileDescriptor = contentResolver.openFileDescriptor(audioTempFile.getUri(), "rwt"); recorder.setOutputFile(parcelFileDescriptor.getFileDescriptor()); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); recorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt index f7626af3..70407c06 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt @@ -1,10 +1,10 @@ package awais.instagrabber.viewmodels +import android.R.attr import android.app.Application import android.content.ContentResolver -import android.media.MediaScannerConnection import android.net.Uri -import android.util.Log +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.* import awais.instagrabber.customviews.emoji.Emoji 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.VoiceRecorder.VoiceRecorderCallback import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult -import awais.instagrabber.utils.extensions.TAG -import java.io.File import java.util.* + class DirectThreadViewModel( application: Application, val threadId: String, @@ -37,7 +36,7 @@ class DirectThreadViewModel( // private static final String ERROR_INVALID_THREAD = "Invalid thread"; 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 lateinit var threadManager: ThreadManager @@ -87,33 +86,24 @@ class DirectThreadViewModel( fun startRecording(): LiveData> { val data = MutableLiveData>() - voiceRecorder = VoiceRecorder(recordingsDir, object : VoiceRecorderCallback { + voiceRecorder = VoiceRecorder(recordingsDir!!, object : VoiceRecorderCallback { override fun onStart() {} override fun onComplete(result: VoiceRecordingResult) { - Log.d(TAG, "onComplete: recording complete. Scanning file...") - MediaScannerConnection.scanFile( - getApplication(), - arrayOf(result.file.absolutePath), - arrayOf(result.mimeType) - ) { _: 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 { + // Log.d(TAG, "onComplete: recording complete. Scanning file..."); + MediaUtils.getVoiceInfo( + contentResolver, + result.file.uri, + object : OnInfoLoadListener { override fun onLoad(videoInfo: VideoInfo?) { if (videoInfo == null) return threadManager.sendVoice( data, - uri, + result.file.uri, result.waveform, result.samplingFreq, videoInfo.duration, - videoInfo.size, - viewModelScope, + result.file.length(), + viewModelScope ) } @@ -121,12 +111,11 @@ class DirectThreadViewModel( data.postValue(error(t.message, null)) } }) - } } override fun onCancel() {} }) - voiceRecorder?.startRecording() + voiceRecorder?.startRecording(contentResolver) return data } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java index a1bfdccf..03e0420f 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java @@ -24,6 +24,8 @@ 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(); @@ -67,7 +69,7 @@ public class DirectorySelectActivityViewModel extends AndroidViewModel { private void setMessage(@Nullable final Uri initialUri) { if (initialUri == null) { - final String prevVersionFolderPath = Utils.settingsHelper.getString(Constants.FOLDER_PATH); + final String prevVersionFolderPath = Utils.settingsHelper.getString(FOLDER_PATH); if (TextUtils.isEmpty(prevVersionFolderPath)) { // default message message.postValue(getApplication().getString(R.string.dir_select_default_message)); diff --git a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt index a749cc07..b182c514 100644 --- a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt +++ b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt @@ -6,8 +6,8 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever -import android.media.MediaScannerConnection import android.net.Uri import android.os.Build import android.os.Handler @@ -15,7 +15,7 @@ import android.os.Looper import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.ForegroundInfo @@ -37,13 +37,12 @@ import kotlinx.coroutines.withContext import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter import java.io.BufferedInputStream import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream import java.net.URL import java.util.* import java.util.concurrent.ExecutionException import kotlin.math.abs + class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { private val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(context) @@ -89,15 +88,15 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti return Result.success() } - private suspend fun download(urlToFilePathMap: Map) { + private suspend fun download(urlToFilePathMap: Map) { val notificationId = notificationId val entries = urlToFilePathMap.entries var count = 1 val total = urlToFilePathMap.size - for ((url, value) in entries) { + for ((url, file) in entries) { updateDownloadProgress(notificationId, count, total, 0f) withContext(Dispatchers.IO) { - download(notificationId, count, total, url, value) + download(notificationId, count, total, url, file) } count++ } @@ -111,47 +110,49 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti position: Int, total: Int, 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 - val outFile = if (isJpg) DownloadUtils.getTempFile() else File(filePath) + val outFile = if (isJpg) DownloadUtils.getTempFile(null, "jpg") else filePath try { val urlConnection = URL(url).openConnection() val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong() var totalRead = 0f try { BufferedInputStream(urlConnection.getInputStream()).use { bis -> - FileOutputStream(outFile).use { fos -> + contentResolver.openOutputStream(outFile.uri).use { fos -> val buffer = ByteArray(0x2000) var count: Int while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) { totalRead += count - fos.write(buffer, 0, count) + fos!!.write(buffer, 0, count) setProgressAsync(Data.Builder().putString(URL, url) .putFloat(PROGRESS, totalRead * 100f / fileSize) .build()) updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize) } - fos.flush() + fos!!.flush() } } } 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) { - val finalFile = File(filePath) try { - FileInputStream(outFile).use { fis -> - FileOutputStream(finalFile).use { fos -> + contentResolver.openInputStream(outFile.uri).use { fis -> + contentResolver.openOutputStream(filePath.uri).use { fos -> val jpegIptcRewriter = JpegIptcRewriter() jpegIptcRewriter.removeIPTC(fis, fos) } } } catch (e: Exception) { Log.e(TAG, "Error while removing iptc: url: " + url - + ", tempFile: " + outFile.absolutePath - + ", finalFile: " + finalFile.absolutePath, e) + + ", tempFile: " + outFile.name + + ", finalFile: " + filePath.name, e) } val deleted = outFile.delete() if (!deleted) { @@ -218,53 +219,90 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti return builder.build() } - private fun showSummary(urlToFilePathMap: Map?) { + private fun showSummary(urlToFilePathMap: Map?) { val context = applicationContext val filePaths = urlToFilePathMap!!.values val notifications: MutableList = LinkedList() val notificationIds: MutableList = LinkedList() var count = 1 - for (filePath in filePaths) { - val file = File(filePath) - context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))) - MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null, null) - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + for (filePath: DocumentFile in filePaths) { + // final File file = new File(filePath); + // context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, filePath.getUri())); + // Utils.scanDocumentFile(context, filePath, (path, uri) -> {}); + // final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file); 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 intent = Intent(Intent.ACTION_VIEW, uri) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_FROM_BACKGROUND - or Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, uri) + val intent = Intent(Intent.ACTION_VIEW, filePath.uri) + .addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_FROM_BACKGROUND + or Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + .putExtra(Intent.EXTRA_STREAM, filePath.uri) val pendingIntent = PendingIntent.getActivity( context, DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT ) - val notificationId = notificationId + count + val notificationId: Int = notificationId + count notificationIds.add(notificationId) count++ - val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_download) - .setContentText(null) - .setContentTitle(downloadComplete) - .setWhen(System.currentTimeMillis()) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setGroup(NOTIF_GROUP_NAME + "_" + id) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setContentIntent(pendingIntent) - .addAction(R.drawable.ic_delete, - context.getString(R.string.delete), - DeleteImageIntentService.pendingIntent(context, filePath, notificationId)) + val builder: NotificationCompat.Builder = + NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_download) + .setContentText(null) + .setContentTitle(downloadComplete) + .setWhen(System.currentTimeMillis()) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setGroup(NOTIF_GROUP_NAME + "_" + id) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent(pendingIntent) + .addAction( + R.drawable.ic_delete, + context.getString(R.string.delete), + DeleteImageIntentService.pendingIntent(context, filePath, notificationId) + ) if (bitmap != null) { builder.setLargeIcon(bitmap) - .setStyle(NotificationCompat.BigPictureStyle() - .bigPicture(bitmap) - .bigLargeIcon(null)) + .setStyle( + NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null) + ) .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) } notifications.add(builder) @@ -344,16 +382,16 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti return bitmap } - class DownloadRequest private constructor(val urlToFilePathMap: Map) { + class DownloadRequest private constructor(val urlToFilePathMap: Map) { class Builder { - private var urlToFilePathMap: MutableMap = mutableMapOf() - fun setUrlToFilePathMap(urlToFilePathMap: MutableMap): Builder { + private var urlToFilePathMap: MutableMap = mutableMapOf() + fun setUrlToFilePathMap(urlToFilePathMap: MutableMap): Builder { this.urlToFilePathMap = urlToFilePathMap return this } - fun addUrl(url: String, filePath: String): Builder { + fun addUrl(url: String, filePath: DocumentFile): Builder { urlToFilePathMap[url] = filePath return this }