Skip to content

Commit 3a0b63f

Browse files
committed
improve "open in gramophone" massively
1 parent fed0631 commit 3a0b63f

File tree

6 files changed

+88
-46
lines changed

6 files changed

+88
-46
lines changed

app/src/main/kotlin/org/akanework/gramophone/ui/AudioPreviewActivity.kt

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import android.os.Handler
1010
import android.os.Looper
1111
import android.provider.MediaStore
1212
import android.provider.Settings
13+
import android.system.ErrnoException
14+
import android.system.Os
1315
import android.util.Log
1416
import android.view.View
1517
import android.widget.ImageView
@@ -48,6 +50,7 @@ import org.akanework.gramophone.logic.utils.exoplayer.GramophoneMediaSourceFacto
4850
import org.akanework.gramophone.logic.utils.exoplayer.GramophoneRenderFactory
4951
import org.akanework.gramophone.ui.components.FullBottomSheet.Companion.SLIDER_UPDATE_INTERVAL
5052
import org.akanework.gramophone.ui.components.SquigglyProgress
53+
import java.io.File
5154

5255
private const val TAG = "AudioPreviewActivity"
5356

@@ -293,54 +296,80 @@ class AudioPreviewActivity : AppCompatActivity(), View.OnClickListener {
293296
var fileUri: Uri? = null
294297
val queryUri = if (uri.scheme == "file") {
295298
fileUri = uri
296-
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
299+
null
297300
} else if (uri.scheme == "content" && uri.authority == MediaStore.AUTHORITY)
298301
uri
299302
else if (uri.scheme == "content")
300303
try {
301304
if (hasScopedStorageV1()) MediaStore.getMediaUri(this, uri) else null
302305
} catch (e: Exception) {
303-
if (e.message != "Provider for this Uri is not supported." && e !is SecurityException)
304-
throw e
306+
if (e is SecurityException || e.message == "Provider for this Uri is not supported."
307+
|| e.message?.startsWith("Invalid URI: ") == true)
308+
Log.w(TAG, e.javaClass.name + ": " + e.message)
309+
else
310+
Log.e(TAG, Log.getStackTraceString(e))
305311
null
306312
} ?: run {
307313
val lp = Uri.decode(uri.lastPathSegment)
308314
if (lp?.toUri()?.scheme == "file") { // Let's try our luck! Material Files supports this
309315
fileUri = lp.toUri()
310-
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
311-
} else null // ¯\_(ツ)_/¯
316+
} else { // ¯\_(ツ)_/¯
317+
val pfd = contentResolver.openFileDescriptor(uri, "r")
318+
if (pfd == null) return@run null
319+
val l = try {
320+
Os.readlink("/proc/self/fd/" + pfd.fd)
321+
} catch (e: ErrnoException) {
322+
Log.w(TAG, e)
323+
return@run null
324+
} finally {
325+
pfd.close()
326+
}
327+
val f = File(l)
328+
if (f.exists())
329+
fileUri = "file://$l".toUri()
330+
else
331+
Log.w(TAG, "found $l from fd but it doesn't exist")
332+
}
333+
null
312334
}
313335
else null
336+
Log.i(TAG, "Audio preview opening $uri with query=$queryUri file=$fileUri")
314337
val projection =
315338
arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DURATION)
316-
val cursor = if (queryUri != null) contentResolver.query(
317-
queryUri,
339+
val cursor = if (queryUri != null || fileUri != null) contentResolver.query(
340+
queryUri ?: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
318341
projection,
319-
if (fileUri?.scheme == "file")
342+
if (fileUri != null)
320343
MediaStore.Audio.Media.DATA + " = ?" else null,
321-
if (fileUri?.scheme == "file") arrayOf(fileUri!!.toFile().absolutePath) else null,
344+
if (fileUri != null) arrayOf(fileUri!!.toFile().absolutePath) else null,
322345
null
323346
) else null
324347
val mediaItem = MediaItem.Builder()
325-
.setUri(uri)
348+
.setUri(fileUri ?: queryUri ?: uri)
326349
if (cursor?.moveToFirst() == true) {
327350
val id = cursor.getLong(
328351
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
329352
)
330-
val durationMs = cursor.getLong(
331-
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
332-
)
333-
mediaItem.setMediaId(id.toString())
334-
mediaItem.setMediaMetadata(
335-
MediaMetadata.Builder()
336-
.setDurationMs(durationMs)
337-
.build()
338-
)
339-
openIcon.visibility = View.VISIBLE
340-
openText.visibility = View.VISIBLE
353+
if (id != 0L) {
354+
val durationMs = cursor.getLong(
355+
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
356+
)
357+
mediaItem.setMediaId(id.toString())
358+
mediaItem.setMediaMetadata(
359+
MediaMetadata.Builder()
360+
.setDurationMs(durationMs)
361+
.build()
362+
)
363+
openIcon.visibility = View.VISIBLE
364+
openText.visibility = View.VISIBLE
365+
Log.i(TAG, "Audio preview found ID $id for query=$queryUri file=$fileUri (was uri=$uri)")
366+
} else {
367+
Log.i(TAG, "Audio preview found no ID for query=$queryUri file=$fileUri (was uri=$uri)")
368+
}
341369
} else {
342370
openIcon.visibility = View.GONE
343371
openText.visibility = View.GONE
372+
Log.i(TAG, "Audio preview found no data for query=$queryUri file=$fileUri (was uri=$uri)")
344373
}
345374
try {
346375
player.setMediaItem(mediaItem.build())

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

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

2020
import android.annotation.SuppressLint
21-
import android.app.ComponentCaller
2221
import android.app.NotificationManager
2322
import android.content.Intent
2423
import android.content.pm.PackageManager
2524
import android.graphics.Color
26-
import android.net.Uri
2725
import android.os.Bundle
2826
import android.os.Handler
2927
import android.os.Looper
3028
import android.provider.Settings
29+
import android.util.Log
3130
import android.view.Choreographer
3231
import android.view.ViewGroup
3332
import android.widget.TextView
@@ -39,37 +38,35 @@ import androidx.activity.viewModels
3938
import androidx.annotation.OptIn
4039
import androidx.appcompat.app.AppCompatActivity
4140
import androidx.core.app.ActivityCompat
42-
import androidx.core.content.ContextCompat
41+
import androidx.core.net.toUri
4342
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
44-
import androidx.core.view.ViewCompat
4543
import androidx.fragment.app.Fragment
46-
import androidx.fragment.app.FragmentContainerView
4744
import androidx.fragment.app.FragmentManager
4845
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
4946
import androidx.fragment.app.commit
47+
import androidx.media3.common.C
5048
import androidx.media3.common.util.UnstableApi
5149
import androidx.media3.session.DefaultMediaNotificationProvider
5250
import coil3.imageLoader
5351
import kotlinx.coroutines.CoroutineScope
5452
import kotlinx.coroutines.Dispatchers
53+
import kotlinx.coroutines.flow.first
54+
import kotlinx.coroutines.flow.firstOrNull
55+
import kotlinx.coroutines.flow.map
5556
import kotlinx.coroutines.launch
57+
import kotlinx.coroutines.runBlocking
5658
import kotlinx.coroutines.withContext
5759
import org.akanework.gramophone.BuildConfig
5860
import org.akanework.gramophone.R
5961
import org.akanework.gramophone.logic.enableEdgeToEdgeProperly
6062
import org.akanework.gramophone.logic.gramophoneApplication
63+
import org.akanework.gramophone.logic.hasAudioPermission
6164
import org.akanework.gramophone.logic.hasScopedStorageV2
6265
import org.akanework.gramophone.logic.hasScopedStorageWithMediaTypes
6366
import org.akanework.gramophone.logic.needsMissingOnDestroyCallWorkarounds
6467
import org.akanework.gramophone.logic.postAtFrontOfQueueAsync
6568
import org.akanework.gramophone.ui.components.PlayerBottomSheet
6669
import org.akanework.gramophone.ui.fragments.BaseFragment
67-
import androidx.core.net.toUri
68-
import androidx.media3.common.C
69-
import kotlinx.coroutines.flow.first
70-
import kotlinx.coroutines.flow.map
71-
import kotlinx.coroutines.runBlocking
72-
import org.akanework.gramophone.logic.hasAudioPermission
7370

7471
/**
7572
* MainActivity:
@@ -178,16 +175,21 @@ class MainActivity : AppCompatActivity() {
178175
super.onNewIntent(intent)
179176
autoPlay = intent.extras?.getBoolean(PLAYBACK_AUTO_START_FOR_FGS, false) == true
180177
if (ready) {
181-
intent.extras?.getString(PLAYBACK_AUTO_PLAY_ID)?.let { id ->
178+
intent.extras?.getString(PLAYBACK_AUTO_PLAY_ID)?.let { theId ->
179+
val id = "MediaStore:$theId"
182180
val pos = intent.extras?.getLong(PLAYBACK_AUTO_PLAY_POSITION, C.TIME_UNSET) ?: C.TIME_UNSET
183181
controllerViewModel.addControllerCallback(lifecycle) { controller, _ ->
184182
runBlocking { reader.songListFlow
185-
.map { it.find { it.mediaId == id } }.first() }?.let { mediaItem ->
186-
controller.setMediaItem(mediaItem)
187-
controller.prepare()
188-
controller.seekTo(pos)
189-
controller.play()
190-
}
183+
.map { it.find { it.mediaId == id } }.firstOrNull() }
184+
.let { mediaItem ->
185+
if (mediaItem != null) {
186+
controller.setMediaItem(mediaItem, pos)
187+
controller.prepare()
188+
controller.play()
189+
} else {
190+
Toast.makeText(this@MainActivity, R.string.cannot_find_file, Toast.LENGTH_LONG).show()
191+
}
192+
}
191193
dispose()
192194
}
193195
}
@@ -201,7 +203,14 @@ class MainActivity : AppCompatActivity() {
201203
ready = true
202204
Choreographer.getInstance().postFrameCallback {
203205
handler.postAtFrontOfQueueAsync {
204-
super.reportFullyDrawn()
206+
try {
207+
super.reportFullyDrawn()
208+
} catch (e: SecurityException) {
209+
// samsung SM-G570M on SDK 26: Permission Denial: broadcast from android asks to run as user
210+
// -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL
211+
// or android.permission.INTERACT_ACROSS_USERS
212+
Log.w("MainActivity", e)
213+
}
205214
}
206215
}
207216
}

app/src/main/kotlin/org/akanework/gramophone/ui/adapters/BaseAdapter.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ import kotlinx.coroutines.cancel
4848
import kotlinx.coroutines.flow.Flow
4949
import kotlinx.coroutines.flow.MutableStateFlow
5050
import kotlinx.coroutines.flow.SharingStarted
51+
import kotlinx.coroutines.flow.collectLatest
5152
import kotlinx.coroutines.flow.combine
5253
import kotlinx.coroutines.flow.first
5354
import kotlinx.coroutines.flow.flatMapLatest
5455
import kotlinx.coroutines.flow.shareIn
5556
import kotlinx.coroutines.launch
5657
import kotlinx.coroutines.runBlocking
57-
import kotlinx.coroutines.sync.Semaphore
5858
import kotlinx.coroutines.withContext
5959
import me.zhanghai.android.fastscroll.PopupTextProvider
6060
import org.akanework.gramophone.R
@@ -222,15 +222,16 @@ abstract class BaseAdapter<T>(
222222
throw IllegalStateException("scope != null in onAttachedToRecyclerView")
223223
scope = CoroutineScope(Dispatchers.Default)
224224
scope!!.launch {
225-
flow.collect {
225+
flow.collectLatest {
226226
// The replay cache may cause us seeing the same list more than one. Make sure to
227227
// use === (reference equals) to avoid performance hit.
228-
if (list === it) return@collect
229-
val diff = if ((list.second.isNotEmpty<T>() && it.second.isNotEmpty<T>())
228+
val old = list
229+
if (old === it) return@collectLatest
230+
val diff = if ((old.second.isNotEmpty<T>() && it.second.isNotEmpty<T>())
230231
|| allowDiffUtils)
231-
DiffUtil.calculateDiff(SongDiffCallback(list.second, it.second))
232+
DiffUtil.calculateDiff(SongDiffCallback(old.second, it.second))
232233
else null
233-
val sizeChanged = list.second.size != it.second.size
234+
val sizeChanged = old.second.size != it.second.size
234235
withContext(Dispatchers.Main + NonCancellable) {
235236
list = it
236237
if (diff != null)

app/src/main/res/layout-land/full_player.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
android:layout_height="wrap_content"
145145
android:layout_gravity="center"
146146
android:value="0"
147+
android:valueTo="1"
147148
android:visibility="gone"
148149
android:contentDescription="@string/position_slider"
149150
app:labelBehavior="gone"

app/src/main/res/layout/full_player.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
android:layout_height="wrap_content"
146146
android:layout_gravity="center"
147147
android:value="0"
148+
android:valueTo="1"
148149
android:visibility="gone"
149150
android:contentDescription="@string/position_slider"
150151
app:labelBehavior="gone"

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,5 @@
418418
<string name="spk_encoding_dts_lbr">DTS Express</string>
419419
<string name="settings_translation_auto_word">Auto-animate translations</string>
420420
<string name="settings_translation_auto_word_summary">Apply linear gradient to translation lines in word-synced lyric playback if translations do not have word-sync metadata</string>
421+
<string name="cannot_find_file">Cannot find this file</string>
421422
</resources>

0 commit comments

Comments
 (0)