diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 58e130489b..5a17a4bec9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -30,6 +30,7 @@ import com.google.android.gms.security.ProviderInstaller; import org.conscrypt.ConscryptSignal; import org.greenrobot.eventbus.EventBus; import org.signal.aesgcmprovider.AesGcmProvider; +import org.signal.core.util.DiskUtil; import org.signal.core.util.MemoryTracker; import org.signal.core.util.concurrent.AnrDetector; import org.signal.core.util.concurrent.SignalExecutors; @@ -299,7 +300,7 @@ public class ApplicationContext extends Application implements AppForegroundObse } public void checkFreeDiskSpace() { - long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes(); + long availableBytes = DiskUtil.getAvailableSpace(getApplicationContext()).getBytes(); SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 93e7c92a50..dfd92e17e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -24,6 +24,7 @@ import org.signal.core.util.Base64 import org.signal.core.util.Base64.decodeBase64OrThrow import org.signal.core.util.ByteSize import org.signal.core.util.CursorUtil +import org.signal.core.util.DiskUtil import org.signal.core.util.EventTimer import org.signal.core.util.PendingIntentFlags.cancelCurrent import org.signal.core.util.Stopwatch @@ -375,7 +376,7 @@ object BackupRepository { * Caller from the attachment / thumbnail download jobs. */ fun checkForOutOfStorageError(tag: String): Boolean { - val availableSpace = getFreeStorageSpace() + val availableSpace = DiskUtil.getAvailableSpace(AppDependencies.application) val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes return if (availableSpace < remainingAttachmentSize) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.kt index 583ab69f54..82260836d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.kt @@ -7,6 +7,8 @@ import android.provider.Settings import android.util.DisplayMetrics import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability +import org.signal.core.util.BidiUtil +import org.signal.core.util.DiskUtil import org.signal.core.util.FontUtil.canRenderEmojiAtFontSize import org.signal.core.util.bytes import org.signal.core.util.roundedString @@ -55,6 +57,7 @@ class LogSectionSystemInfo : LogSection { Memory : ${getMemoryUsage()} Memclass : ${getMemoryClass(context)} MemInfo : ${getMemoryInfo(context)} + Disk Space : ${getDiskSpaceInfo(context)} OS Host : ${Build.HOST} RecipientId : ${if (SignalStore.registration.isRegistrationComplete) self().id else "N/A"} ACI : ${getCensoredAci()} @@ -128,6 +131,14 @@ class LogSectionSystemInfo : LogSection { return "availMem: ${info.availMem.bytes.inMebiBytes.roundedString(2)} MiB, totalMem: ${info.totalMem.bytes.inMebiBytes.roundedString(2)} MiB, threshold: ${info.threshold.bytes.inMebiBytes.roundedString(2)} MiB, lowMemory: ${info.lowMemory}" } + private fun getDiskSpaceInfo(context: Context): String { + val totalSpace = DiskUtil.getTotalDiskSize(context) + val freeSpace = DiskUtil.getAvailableSpace(context) + val usedSpace = totalSpace - freeSpace + + return BidiUtil.stripAllDirectionalCharacters("${usedSpace.toUnitString()} / ${totalSpace.toUnitString()} (${freeSpace.toUnitString()} free)") + } + private fun getScreenResolution(context: Context): String { val displayMetrics = DisplayMetrics() val windowManager = ServiceUtil.getWindowManager(context) diff --git a/core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt b/core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt index 6dd7924cda..e0ad35966e 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt @@ -152,4 +152,8 @@ object BidiUtil { fun forceLtr(text: CharSequence): String { return "\u202a" + text + "\u202c" } + + fun stripAllDirectionalCharacters(text: String): String { + return text.replace("[\\u200f\\u2066\\u2067\\u2068\\u2069\\u202a\\u202b\\u202c\\u202d\\u202e]".toRegex(), "") + } } diff --git a/core-util/src/main/java/org/signal/core/util/DiskUtil.kt b/core-util/src/main/java/org/signal/core/util/DiskUtil.kt new file mode 100644 index 0000000000..b71727c713 --- /dev/null +++ b/core-util/src/main/java/org/signal/core/util/DiskUtil.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import android.app.usage.StorageStatsManager +import android.content.Context +import android.os.Build +import android.os.StatFs +import android.os.storage.StorageManager +import androidx.annotation.RequiresApi + +object DiskUtil { + + /** + * Gets the remaining storage usable by the application. + * + * @param context The application context + */ + @JvmStatic + fun getAvailableSpace(context: Context): ByteSize { + return if (Build.VERSION.SDK_INT >= 26) { + getAvailableStorageBytesApi26(context).bytes + } else { + return getAvailableStorageBytesLegacy(context).bytes + } + } + + /** + * Gets the total disk size of the volume used by the application. + * + * @param context The application context + */ + @JvmStatic + fun getTotalDiskSize(context: Context): ByteSize { + return if (Build.VERSION.SDK_INT >= 26) { + getTotalDiskSizeApi26(context).bytes + } else { + return getTotalDiskSizeLegacy(context).bytes + } + } + + @RequiresApi(26) + private fun getAvailableStorageBytesApi26(context: Context): Long { + val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager + val appStorageUuid = storageManager.getUuidForPath(context.filesDir) + + return storageStatsManager.getFreeBytes(appStorageUuid) + } + + private fun getAvailableStorageBytesLegacy(context: Context): Long { + val stat = StatFs(context.filesDir.absolutePath) + return stat.availableBytes + } + + @RequiresApi(26) + private fun getTotalDiskSizeApi26(context: Context): Long { + val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager + val appStorageUuid = storageManager.getUuidForPath(context.filesDir) + + return storageStatsManager.getTotalBytes(appStorageUuid) + } + + private fun getTotalDiskSizeLegacy(context: Context): Long { + val stat = StatFs(context.filesDir.absolutePath) + return stat.totalBytes + } +}