From 2e318b8b03db66a204f77b4a473cdf08ebfb82de Mon Sep 17 00:00:00 2001 From: CloudyRowly <76801836+CloudyRowly@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:18:55 +1100 Subject: [PATCH] Added "free memory" check before downloading [Android N / API 24+] (#10505) Added "free space" check before downloading eliminating bugs related to out-of-memory on Android N / API 24+ --- .../newpipe/download/DownloadDialog.java | 23 +++++++++- .../streams/io/StoredDirectoryHelper.java | 45 +++++++++++++++++++ .../java/us/shandian/giga/util/Utility.java | 18 +++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 1375d661e..fa312e55e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -16,6 +16,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.provider.Settings; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -147,7 +148,6 @@ public class DownloadDialog extends DialogFragment registerForActivityResult( new StartActivityForResult(), this::requestDownloadPickVideoFolderResult); - /*////////////////////////////////////////////////////////////////////////// // Instance creation //////////////////////////////////////////////////////////////////////////*/ @@ -565,7 +565,6 @@ public class DownloadDialog extends DialogFragment } } - /*////////////////////////////////////////////////////////////////////////// // Listeners //////////////////////////////////////////////////////////////////////////*/ @@ -784,6 +783,7 @@ public class DownloadDialog extends DialogFragment final StoredDirectoryHelper mainStorage; final MediaFormat format; final String selectedMediaType; + final long size; // first, build the filename and get the output folder (if possible) // later, run a very very very large file checking logic @@ -795,6 +795,7 @@ public class DownloadDialog extends DialogFragment selectedMediaType = getString(R.string.last_download_type_audio_key); mainStorage = mainStorageAudio; format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); + size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex); if (format == MediaFormat.WEBMA_OPUS) { mimeTmp = "audio/ogg"; filenameTmp += "opus"; @@ -807,6 +808,7 @@ public class DownloadDialog extends DialogFragment selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); + size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex); if (format != null) { mimeTmp = format.mimeType; filenameTmp += format.getSuffix(); @@ -816,6 +818,7 @@ public class DownloadDialog extends DialogFragment selectedMediaType = getString(R.string.last_download_type_subtitle_key); mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); + size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex); if (format != null) { mimeTmp = format.mimeType; } @@ -871,6 +874,22 @@ public class DownloadDialog extends DialogFragment return; } + // Check for free memory space (for api 24 and up) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + final long freeSpace = mainStorage.getFreeMemory(); + if (freeSpace <= size) { + Toast.makeText(context, getString(R. + string.error_insufficient_storage), Toast.LENGTH_LONG).show(); + // move the user to storage setting tab + final Intent storageSettingsIntent = new Intent(Settings. + ACTION_INTERNAL_STORAGE_SETTINGS); + if (storageSettingsIntent.resolveActivity(context.getPackageManager()) != null) { + startActivity(storageSettingsIntent); + } + return; + } + } + // check for existing file with the same name checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index 74fc74c76..0fe2e0408 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -5,11 +5,15 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; +import android.os.Build; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; import android.provider.DocumentsContract; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.documentfile.provider.DocumentFile; import org.schabi.newpipe.settings.NewPipeSettings; @@ -23,6 +27,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -30,6 +35,8 @@ import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; +import us.shandian.giga.util.Utility; + public class StoredDirectoryHelper { private static final String TAG = StoredDirectoryHelper.class.getSimpleName(); public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION @@ -168,6 +175,44 @@ public class StoredDirectoryHelper { return docTree == null; } + /** + * Get free memory of the storage partition (root of the directory). + * @return amount of free memory in the volume of current directory (bytes) + */ + @RequiresApi(api = Build.VERSION_CODES.N) // Necessary for `getStorageVolume()` + public long getFreeMemory() { + final Uri uri = getUri(); + final StorageManager storageManager = (StorageManager) context. + getSystemService(Context.STORAGE_SERVICE); + final List volumes = storageManager.getStorageVolumes(); + + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + if (split.length > 0) { + final String volumeId = split[0]; + + for (final StorageVolume volume : volumes) { + // if the volume is an internal system volume + if (volume.isPrimary() && volumeId.equalsIgnoreCase("primary")) { + return Utility.getSystemFreeMemory(); + } + + // if the volume is a removable volume (normally an SD card) + if (volume.isRemovable() && !volume.isPrimary()) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + try { + final String sdCardUUID = volume.getUuid(); + return storageManager.getAllocatableBytes(UUID.fromString(sdCardUUID)); + } catch (final Exception e) { + // do nothing + } + } + } + } + } + return Long.MAX_VALUE; + } + /** * Only using Java I/O. Creates the directory named by this abstract pathname, including any * necessary but nonexistent parent directories. diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 3cfa22bd9..c75269757 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -2,6 +2,8 @@ package us.shandian.giga.util; import android.content.Context; import android.os.Build; +import android.os.Environment; +import android.os.StatFs; import android.util.Log; import androidx.annotation.ColorInt; @@ -26,10 +28,8 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.HttpURLConnection; import java.util.Locale; -import java.util.Random; import okio.ByteString; -import us.shandian.giga.get.DownloadMission; public class Utility { @@ -40,6 +40,20 @@ public class Utility { UNKNOWN } + /** + * Get amount of free system's memory. + * @return free memory (bytes) + */ + public static long getSystemFreeMemory() { + try { + final StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath()); + return statFs.getAvailableBlocksLong() * statFs.getBlockSizeLong(); + } catch (final Exception e) { + // do nothing + } + return -1; + } + public static String formatBytes(long bytes) { Locale locale = Locale.getDefault(); if (bytes < 1024) {