Skip to content

Commit ad31d45

Browse files
committed
work on allow deleting files from SD cards
- disabled write/favourite request code until it's rewritten - does not work correctly on Android Q currently - L-P, R-B should work well
1 parent 9ed882b commit ad31d45

File tree

9 files changed

+734
-127
lines changed

9 files changed

+734
-127
lines changed

app/src/main/java/org/akanework/gramophone/logic/GramophoneExtensions.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import org.akanework.gramophone.logic.utils.SemanticLyrics
8787
import org.akanework.gramophone.ui.MainActivity
8888
import org.jetbrains.annotations.Contract
8989
import java.io.File
90+
import java.io.FileInputStream
9091
import java.util.Locale
9192
import kotlin.math.max
9293

@@ -122,15 +123,21 @@ fun MediaItem.requireMediaStoreId(): Long {
122123

123124
fun MediaItem.getBitrate(): Int? {
124125
val retriever = MediaMetadataRetriever()
126+
val file = getFile() ?: return null
127+
var fd: FileInputStream? = null
125128
return try {
126-
val filePath = getFile()?.path ?: return null
127-
retriever.setDataSource(filePath)
129+
fd = file.inputStream()
130+
// uses this slightly less straight-forward overload to avoid a resource leak in platform
131+
retriever.setDataSource(fd.fd)
132+
fd.close()
133+
fd = null
128134
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
129135
?.toIntOrNull()
130136
} catch (e: Exception) {
131137
Log.w("MediaItem", "getBitrate failed", e)
132138
null
133139
} finally {
140+
fd?.close()
134141
retriever.release()
135142
}
136143
}

app/src/main/java/org/akanework/gramophone/logic/utils/SdScanner.kt

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -231,21 +231,11 @@ class SdScanner(private val context: Context, var progressFrequencyMs: Int = 250
231231
}
232232
}
233233
val roots = hashSetOf<File>()
234-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
235-
for (volume in context.getSystemService<StorageManager>()!!.storageVolumes) {
236-
if (volume.mediaStoreVolumeName == null) continue
237-
if (!volume.state.startsWith(Environment.MEDIA_MOUNTED)) continue
238-
roots.add(volume.directory!!)
239-
}
240-
} else {
241-
val volumes = context.getExternalFilesDirs(null)
242-
.map { it.parentFile!!.parentFile!!.parentFile!!.parentFile!! }
243-
for (volume in volumes) {
244-
if (!Environment.getExternalStorageState(volume)
245-
.startsWith(Environment.MEDIA_MOUNTED)
246-
) continue
247-
roots.add(volume)
248-
}
234+
for (volume in StorageManagerCompat(context).storageVolumes) {
235+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
236+
volume.mediaStoreVolumeName == null) continue
237+
if (!volume.state.startsWith(Environment.MEDIA_MOUNTED)) continue
238+
roots.add(volume.directory!!)
249239
}
250240
scanner.scan(roots, false)
251241
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.akanework.gramophone.logic.utils
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.os.Build
7+
import android.os.Environment
8+
import android.os.storage.StorageManager
9+
import android.os.storage.StorageVolume
10+
import android.provider.DocumentsContract
11+
import android.provider.MediaStore
12+
import androidx.annotation.RequiresApi
13+
import androidx.core.content.getSystemService
14+
import java.io.File
15+
import java.util.Locale
16+
17+
class StorageManagerCompat(context: Context) {
18+
private val storageManager = context.getSystemService<StorageManager>()!!
19+
class StorageVolumeCompat {
20+
val uuid: String?
21+
val state: String
22+
val directory: File?
23+
@RequiresApi(Build.VERSION_CODES.Q)
24+
val mediaStoreVolumeName: String?
25+
val isEmulated: Boolean
26+
@RequiresApi(Build.VERSION_CODES.N)
27+
val real: StorageVolume
28+
private val descriptionLegacy: String?
29+
30+
constructor(real: StorageVolume) {
31+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
32+
this.uuid = real.uuid
33+
this.isEmulated = real.isEmulated
34+
this.real = real
35+
this.state = real.state
36+
descriptionLegacy = null
37+
} else {
38+
this.uuid = getUuid.invoke(real) as String?
39+
this.state = getState.invoke(real) as String
40+
this.isEmulated = isEmulatedMethod.invoke(real) as Boolean
41+
@SuppressLint("NewApi")
42+
this.real = real
43+
this.descriptionLegacy = getUserLabel.invoke(real) as String?
44+
}
45+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
46+
this.directory = real.directory
47+
this.mediaStoreVolumeName = real.mediaStoreVolumeName
48+
} else {
49+
this.directory = if (state == Environment.MEDIA_MOUNTED ||
50+
state == Environment.MEDIA_MOUNTED_READ_ONLY
51+
)
52+
getPathFile.invoke(real) as File?
53+
else null
54+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
55+
this.mediaStoreVolumeName = if (real.isPrimary) {
56+
MediaStore.VOLUME_EXTERNAL_PRIMARY
57+
} else real.uuid?.lowercase(Locale.US)
58+
} else {
59+
@SuppressLint("NewApi")
60+
this.mediaStoreVolumeName = "external"
61+
}
62+
}
63+
}
64+
65+
fun getDescription(context: Context): String {
66+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
67+
real.getDescription(context)
68+
} else {
69+
descriptionLegacy ?: @SuppressLint("NewApi")
70+
getDescription.invoke(real, context) as String
71+
}
72+
}
73+
74+
fun createOpenDocumentTreeIntent(): Intent {
75+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
76+
return real.createOpenDocumentTreeIntent()
77+
} else {
78+
val rootId = if (isEmulated) "primary" else uuid
79+
// AOSP uses root uri for Q+ but I found that only document uri works on older vers
80+
val rootUri = DocumentsContract.buildDocumentUri(
81+
"com.android.externalstorage.documents", "$rootId:"
82+
)
83+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
84+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
85+
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, rootUri)
86+
} else { // try our luck
87+
intent.putExtra("android.provider.extra.INITIAL_URI", rootUri)
88+
}
89+
// there seem to be both versions out there
90+
intent.putExtra("android.provider.extra.SHOW_ADVANCED", true)
91+
intent.putExtra("android.content.extra.SHOW_ADVANCED", true)
92+
return intent
93+
}
94+
}
95+
96+
@RequiresApi(Build.VERSION_CODES.N)
97+
fun createAccessIntent(dir: String?): Intent? {
98+
@Suppress("deprecation")
99+
return real.createAccessIntent(dir)
100+
}
101+
}
102+
103+
val storageVolumes: List<StorageVolumeCompat>
104+
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
105+
storageManager.storageVolumes.map { StorageVolumeCompat(it) }
106+
} else {
107+
@Suppress("UNCHECKED_CAST")
108+
(getVolumeList.invoke(storageManager)!! as Array<StorageVolume>)
109+
.map { StorageVolumeCompat(it) }
110+
}
111+
112+
companion object {
113+
private val getVolumeList by lazy {
114+
StorageManager::class.java.getMethod("getVolumeList")
115+
}
116+
private val getPathFile by lazy {
117+
@SuppressLint("NewApi")
118+
StorageVolume::class.java.getMethod("getPathFile")
119+
}
120+
private val getUuid by lazy {
121+
@SuppressLint("NewApi")
122+
StorageVolume::class.java.getMethod("getUuid")
123+
}
124+
private val getState by lazy {
125+
@SuppressLint("NewApi")
126+
StorageVolume::class.java.getMethod("getState")
127+
}
128+
private val isEmulatedMethod by lazy {
129+
@SuppressLint("NewApi")
130+
StorageVolume::class.java.getMethod("isEmulated")
131+
}
132+
private val getUserLabel by lazy {
133+
@SuppressLint("NewApi")
134+
StorageVolume::class.java.getMethod("getUserLabel")
135+
}
136+
private val getDescription by lazy {
137+
@SuppressLint("NewApi")
138+
StorageVolume::class.java.getMethod("getDescription",
139+
Context::class.java)
140+
}
141+
}
142+
}

app/src/main/java/org/akanework/gramophone/ui/MainActivity.kt

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
package org.akanework.gramophone.ui
1919

2020
import android.annotation.SuppressLint
21+
import android.app.Activity
2122
import android.app.NotificationManager
2223
import android.app.SearchManager
2324
import android.app.assist.AssistContent
25+
import android.content.ActivityNotFoundException
2426
import android.content.ClipData
2527
import android.content.ContentResolver
2628
import android.content.ContentUris
2729
import android.content.Intent
30+
import android.content.IntentSender
2831
import android.content.pm.PackageManager
2932
import android.graphics.Color
3033
import android.net.Uri
@@ -111,10 +114,11 @@ class MainActivity : BaseActivity() {
111114
private var ready = false
112115
lateinit var playerBottomSheet: PlayerBottomSheet
113116
private set
114-
lateinit var intentSender: ActivityResultLauncher<IntentSenderRequest>
115-
private set
117+
private lateinit var intentDelete: ActivityResultLauncher<Intent>
118+
private lateinit var intentSenderDelete: ActivityResultLauncher<IntentSenderRequest>
116119
private lateinit var addToPlaylistIntentSender: ActivityResultLauncher<IntentSenderRequest>
117120
private var pendingRequest: Bundle? = null
121+
private var pendingDeleteRequest: Bundle? = null
118122

119123
fun updateLibrary(then: (() -> Unit)? = null) {
120124
// If library load takes more than 2s, exit splash to avoid ANR
@@ -140,12 +144,32 @@ class MainActivity : BaseActivity() {
140144
if (savedInstanceState?.containsKey("AddToPlaylistPendingRequest") == true) {
141145
pendingRequest = savedInstanceState.getBundle("AddToPlaylistPendingRequest")
142146
}
143-
intentSender =
144-
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {}
147+
if (savedInstanceState?.containsKey("DeletePendingRequest") == true) {
148+
pendingDeleteRequest = savedInstanceState.getBundle("DeletePendingRequest")
149+
}
150+
intentDelete =
151+
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
152+
val req = pendingDeleteRequest
153+
?: throw IllegalStateException("pending delete request is null")
154+
pendingDeleteRequest = null
155+
CoroutineScope(Dispatchers.Default).launch {
156+
ItemManipulator.continueDeleteFromIntent(this@MainActivity, it.resultCode, it.data, req)
157+
}
158+
}
159+
intentSenderDelete =
160+
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
161+
val req = pendingDeleteRequest
162+
?: throw IllegalStateException("pending delete request is null")
163+
pendingDeleteRequest = null
164+
CoroutineScope(Dispatchers.Default).launch {
165+
ItemManipulator.continueDeleteFromPendingIntent(this@MainActivity, it.resultCode, req)
166+
}
167+
}
145168
addToPlaylistIntentSender =
146169
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
147170
val req = pendingRequest
148171
?: throw IllegalStateException("pending playlist add request is null")
172+
pendingRequest = null
149173
CoroutineScope(Dispatchers.Default).launch {
150174
doAddToPlaylist(it.resultCode, req)
151175
}
@@ -304,6 +328,36 @@ class MainActivity : BaseActivity() {
304328
}
305329
}
306330

331+
fun runIntentForDelete(intent: Intent, bundle: Bundle) {
332+
try {
333+
intentDelete.launch(intent)
334+
pendingDeleteRequest = bundle
335+
} catch (e: ActivityNotFoundException) {
336+
Log.e("MainActivity", "error launching intent", e)
337+
CoroutineScope(Dispatchers.Default).launch {
338+
ItemManipulator.continueDeleteFromIntent(
339+
this@MainActivity, RESULT_CANCELED,
340+
Intent(), bundle
341+
)
342+
}
343+
}
344+
}
345+
346+
fun runIntentForDelete(intent: IntentSender, bundle: Bundle) {
347+
try {
348+
intentSenderDelete.launch(IntentSenderRequest.Builder(intent).build())
349+
pendingDeleteRequest = bundle
350+
} catch (e: ActivityNotFoundException) {
351+
Log.e("MainActivity", "error launching intent", e)
352+
CoroutineScope(Dispatchers.Default).launch {
353+
ItemManipulator.continueDeleteFromPendingIntent(
354+
this@MainActivity, RESULT_CANCELED,
355+
bundle
356+
)
357+
}
358+
}
359+
}
360+
307361
private suspend fun doAddToPlaylist(resultCode: Int, data: Bundle) {
308362
if (resultCode == RESULT_OK) {
309363
val add = data.getBoolean("AddToEnd")
@@ -343,6 +397,9 @@ class MainActivity : BaseActivity() {
343397
if (pendingRequest != null) {
344398
outState.putBundle("AddToPlaylistPendingRequest", pendingRequest)
345399
}
400+
if (pendingDeleteRequest != null) {
401+
outState.putBundle("DeletePendingRequest", pendingDeleteRequest)
402+
}
346403
}
347404

348405
override fun onNewIntent(intent: Intent) {

app/src/main/java/org/akanework/gramophone/ui/adapters/PlaylistAdapter.kt

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -146,41 +146,25 @@ class PlaylistAdapter(
146146
).show()
147147
return@setOnMenuItemClickListener true
148148
}
149-
val res = ItemManipulator.deletePlaylist(context, item.id!!)
150-
if (res.continueAction != null) {
151-
MaterialAlertDialogBuilder(context)
152-
.setTitle(R.string.delete)
153-
.setMessage(context.getString(R.string.delete_really, item.title))
154-
.setPositiveButton(R.string.yes) { _, _ ->
155-
CoroutineScope(Dispatchers.IO).launch {
156-
try {
157-
res.continueAction.invoke()
158-
} catch (e: DeleteFailedPleaseTryDeleteRequestException) {
159-
withContext(Dispatchers.Main) {
160-
mainActivity.intentSender.launch(
161-
IntentSenderRequest.Builder(e.pendingIntent).build()
162-
)
163-
}
164-
} catch (e: Exception) {
165-
Log.e("PlaylistAdapter", Log.getThrowableString(e)!!)
166-
withContext(Dispatchers.Main) {
167-
Toast.makeText(
168-
context, context.getString(
169-
R.string.delete_failed_playlist,
170-
e.javaClass.name + ": " + e.message
171-
),
172-
Toast.LENGTH_LONG
173-
).show()
174-
}
149+
CoroutineScope(Dispatchers.Default).launch {
150+
val res = ItemManipulator.deletePlaylist(mainActivity, item.id!!)
151+
if (res != null) {
152+
withContext(Dispatchers.Main) {
153+
MaterialAlertDialogBuilder(context)
154+
.setTitle(R.string.delete)
155+
.setMessage(
156+
context.getString(
157+
R.string.delete_really,
158+
item.title
159+
)
160+
)
161+
.setPositiveButton(R.string.yes) { _, _ ->
162+
res.invoke()
175163
}
176-
}
164+
.setNegativeButton(R.string.no) { _, _ -> }
165+
.show()
177166
}
178-
.setNegativeButton(R.string.no) { _, _ -> }
179-
.show()
180-
} else {
181-
mainActivity.intentSender.launch(
182-
IntentSenderRequest.Builder(res.startSystemDialog!!).build()
183-
)
167+
}
184168
}
185169
}
186170

@@ -242,9 +226,9 @@ class PlaylistAdapter(
242226
ItemManipulator.renamePlaylist(context, File(path), newName)
243227
} catch (e: DeleteFailedPleaseTryDeleteRequestException) {
244228
withContext(Dispatchers.Main) {
245-
mainActivity.intentSender.launch(
229+
/*mainActivity.intentSender.launch(
246230
IntentSenderRequest.Builder(e.pendingIntent).build()
247-
)
231+
) TODO(ASAP)*/
248232
}
249233
} catch (e: Exception) {
250234
Log.e("PlaylistAdapter", Log.getThrowableString(e)!!)

0 commit comments

Comments
 (0)