Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ android {

defaultConfig {
applicationId = "com.anggrayudi.storage.sample"
minSdk = 26
minSdk = 23
targetSdk = 37
versionCode = 1
versionName = rootProject.extra["VERSION_NAME"] as String
Expand Down
2 changes: 1 addition & 1 deletion storage-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ android {
resourcePrefix = "ss_"

defaultConfig {
minSdk = 26
minSdk = 23
consumerProguardFiles("consumer-rules.pro")
}

Expand Down
2 changes: 1 addition & 1 deletion storage/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
resourcePrefix = "ss_"

defaultConfig {
minSdk = 26
minSdk = 23
consumerProguardFiles("consumer-rules.pro")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ public class StorageAccessManager(activity: ComponentActivity) {
* and returns [BookmarkResult.Granted] with an **updated** bookmark to persist.
* 3. Nothing matches → [BookmarkResult.VolumeNotMounted].
*/
@RequiresApi(Build.VERSION_CODES.N)
public suspend fun resolveBookmark(
bookmark: VolumeBookmark,
requiresWriteAccess: Boolean = true,
Expand Down Expand Up @@ -325,6 +326,7 @@ public class StorageAccessManager(activity: ComponentActivity) {
* Builds a [VolumeBookmark] for [folder] so it can be re-resolved later with [resolveBookmark].
* Returns `null` when the folder has no resolvable [StorageFile.path].
*/
@RequiresApi(Build.VERSION_CODES.N)
public fun createBookmark(folder: StorageFile): VolumeBookmark? {
val path = folder.path ?: return null
val label =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion.ACTION_REQUEST_PERMISSIONS
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion.EXTRA_PERMISSIONS
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion.EXTRA_PERMISSION_GRANT_RESULTS
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.EmptyActivity
import com.anggrayudi.storage.file.DocumentFileCompat.cleanupRedundantUriPermissions
import com.anggrayudi.storage.file.DocumentFileCompat.externalStoragePath
import com.anggrayudi.storage.file.DocumentFileCompat.getDefaultExternalStorageIntent
import com.anggrayudi.storage.file.DocumentFileCompat.hasStoragePermission
import com.anggrayudi.storage.file.DocumentFileCompat.isSdCardPresent
import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.extension.fromSingleUri
import com.anggrayudi.storage.extension.fromTreeUri
Expand All @@ -30,6 +26,11 @@ import com.anggrayudi.storage.extension.isDocumentsDocument
import com.anggrayudi.storage.extension.isDownloadsDocument
import com.anggrayudi.storage.extension.isExternalStorageDocument
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileCompat.cleanupRedundantUriPermissions
import com.anggrayudi.storage.file.DocumentFileCompat.externalStoragePath
import com.anggrayudi.storage.file.DocumentFileCompat.getDefaultExternalStorageIntent
import com.anggrayudi.storage.file.DocumentFileCompat.hasStoragePermission
import com.anggrayudi.storage.file.DocumentFileCompat.isSdCardPresent
import com.anggrayudi.storage.file.FileFullPath
import com.anggrayudi.storage.file.MimeType
import com.anggrayudi.storage.file.PublicDirectory
Expand Down Expand Up @@ -68,6 +69,7 @@ internal fun getExternalStorageRootAccessIntent(context: Context): Intent =
* != External Storage.
*/
@Suppress("DEPRECATION")
@RequiresApi(api = Build.VERSION_CODES.N)
internal fun getSdCardRootAccessIntent(context: Context): Intent {
val sm = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
return sm.storageVolumes
Expand All @@ -87,8 +89,10 @@ internal fun getSdCardRootAccessIntent(context: Context): Intent {
}

internal fun addInitialPathToIntent(context: Context, intent: Intent, initialPath: FileFullPath?) {
initialPath?.toDocumentUri(context)?.let {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it)
if (Build.VERSION.SDK_INT >= 26) {
initialPath?.toDocumentUri(context)?.let {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it)
}
}
}

Expand Down Expand Up @@ -156,7 +160,8 @@ public class OpenFolderPickerContract(context: Context) :
it == DocumentFileCompat.DOWNLOADS_TREE_URI || it == DocumentFileCompat.DOCUMENTS_TREE_URI
} ||
DocumentFileCompat.isRootUri(uri) &&
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
(Build.VERSION.SDK_INT < Build.VERSION_CODES.N && storageType == StorageType.SD_CARD ||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) &&
!DocumentFileCompat.isStorageUriPermissionGranted(appContext, storageId)
) {
saveUriPermission(appContext, uri)
Expand All @@ -174,11 +179,7 @@ public class OpenFolderPickerContract(context: Context) :
}
}

public class Options
@JvmOverloads
constructor(
public val initialPath: FileFullPath? = null
)
public class Options @JvmOverloads constructor(public val initialPath: FileFullPath? = null)
}

/** This contract may throws [ActivityNotFoundException] */
Expand Down Expand Up @@ -289,10 +290,9 @@ public class StoragePermissionContract() :
input: Unit,
): SynchronousResult<Map<String, @JvmSuppressWildcards Boolean>>? {
val permissions = getPermissions()
val allGranted =
permissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
val allGranted = permissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
return if (allGranted) {
SynchronousResult(permissions.associateWith { true })
} else null
Expand Down Expand Up @@ -329,11 +329,7 @@ public class RequestStorageAccessContract(
public var expectedBasePath: String = "",
) : ActivityResultContract<RequestStorageAccessContract.Options, RequestStorageAccessResult>() {

public class Options
@JvmOverloads
constructor(
public val initialPath: FileFullPath? = null
)
public class Options @JvmOverloads constructor(public val initialPath: FileFullPath? = null)

private val appContext = context.applicationContext

Expand Down Expand Up @@ -363,7 +359,9 @@ public class RequestStorageAccessContract(
getExternalStorageRootAccessIntent(context).also {
addInitialPathToIntent(context, it, input.initialPath)
}
} else if (expectedStorageType == StorageType.SD_CARD) {
} else if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && expectedStorageType == StorageType.SD_CARD
) {
getSdCardRootAccessIntent(context)
} else {
getExternalStorageRootAccessIntent(context)
Expand Down Expand Up @@ -470,7 +468,10 @@ public class RequestStorageAccessContract(
)
} else {
var sdCardIntent: Intent? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
) {
val sm = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager
@Suppress("DEPRECATION")
sdCardIntent = sm.storageVolumes.firstOrNull { !it.isPrimary }?.createAccessIntent(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.os.StatFs
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import android.system.Os
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -116,6 +117,7 @@ public object DocumentFileCompat {
* format. This is the authoritative check; [SD_CARD_STORAGE_ID_REGEX] is only a fallback for when
* the volume list cannot be consulted.
*/
@RequiresApi(Build.VERSION_CODES.N)
@JvmStatic
public fun isMountedVolumeId(context: Context, storageId: String): Boolean {
if (storageId.isEmpty()) return false
Expand Down Expand Up @@ -593,11 +595,14 @@ public object DocumentFileCompat {
/** Intent that opens the SAF folder picker at the primary volume. */
@JvmStatic
public fun getDefaultExternalStorageIntent(context: Context): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
context.fromTreeUri(createDocumentUri(PRIMARY))?.uri,
)
return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
if (Build.VERSION.SDK_INT >= 26) {
putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
context.fromTreeUri(createDocumentUri(PRIMARY))?.uri,
)
}
}
}

/** Runtime read and write permission. Only meaningful on API 26–29. */
Expand Down Expand Up @@ -692,9 +697,13 @@ public object DocumentFileCompat {
// cards but not on every removable volume, so a mounted, granted OTG drive was invisible here.
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val mountedVolumes =
storageManager.storageVolumes
.filter { it.state == Environment.MEDIA_MOUNTED }
.mapNotNull { if (it.isPrimary) PRIMARY else it.uuid }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
storageManager.storageVolumes
.filter { it.state == Environment.MEDIA_MOUNTED }
.mapNotNull { if (it.isPrimary) PRIMARY else it.uuid }
} else {
emptyList()
}
val persistedStorageIds =
context.contentResolver.persistedUriPermissions
.filter { it.isReadPermission && it.isWritePermission && it.uri.isExternalStorageDocument }
Expand Down Expand Up @@ -732,7 +741,8 @@ public object DocumentFileCompat {
"${Environment.getExternalStorageDirectory()}/$rootFolder".trimEnd('/')
)
} else if (
isMountedVolumeId(context, storageId) || storageId.matches(SD_CARD_STORAGE_ID_REGEX)
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
isMountedVolumeId(context, storageId)) || storageId.matches(SD_CARD_STORAGE_ID_REGEX)
) {
val paths = storages[storageId] ?: HashSet()
paths.add("/storage/$storageId/$rootFolder".trimEnd('/'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2322,7 +2322,7 @@ private fun DocumentFile.tryMoveFolderByRenamingPath(
}

try {
if (!isRawFile && writableTargetParentFolder.isTreeDocumentFile) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isRawFile && writableTargetParentFolder.isTreeDocumentFile) {
val movedFileUri =
parentFile?.uri?.let {
DocumentsContract.moveDocument(
Expand Down Expand Up @@ -3287,6 +3287,7 @@ private fun DocumentFile.moveFileTo(

try {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
!isRawFile &&
writableTargetFolder.isTreeDocumentFile &&
getStorageId(context) == targetStorageId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public fun File.child(path: String): File = File(this, path)
* @see [Context.getFilesDir]
*/
public val Context.dataDirectory: File
get() = dataDir
get() = if (Build.VERSION.SDK_INT > 23) dataDir else filesDir.parentFile!!

public fun File.getBasePath(context: Context): String {
val externalStoragePath = DocumentFileCompat.externalStoragePath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.anggrayudi.storage.file

import android.content.Context
import android.net.Uri
import com.anggrayudi.storage.SimpleStorage
import android.os.Build

/**
* Created on 17/08/20
Expand Down Expand Up @@ -49,7 +49,8 @@ public enum class StorageType {
when {
storageId == StorageId.PRIMARY -> EXTERNAL
storageId == StorageId.DATA -> DATA
DocumentFileCompat.isMountedVolumeId(context, storageId) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
DocumentFileCompat.isMountedVolumeId(context, storageId)) ||
storageId.matches(DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX) -> SD_CARD
else -> UNKNOWN
}
Expand Down