summaryrefslogtreecommitdiff
path: root/kotlin/src
diff options
context:
space:
mode:
authorCol_or <1638876934@qq.com>2022-01-21 01:42:01 +0800
committeralk3pInjection <webmaster@raspii.tech>2022-06-29 16:18:42 +0800
commit70d89081e21469119a4d5bbf33a81d3f7bc0df3a (patch)
tree6d4b9388736f21a6df1b8419ad289392caeae62f /kotlin/src
parent1cd251136a4b35739aced207937bf53d543993f0 (diff)
Music: New audio preview UIHEADsugisawa-mr1
Change-Id: I5e546f90f94fd4b1635ca5bc84b70cd715554a6c
Diffstat (limited to 'kotlin/src')
-rw-r--r--kotlin/src/ink/kscope/music/KscopeAudioPreview.kt507
1 files changed, 507 insertions, 0 deletions
diff --git a/kotlin/src/ink/kscope/music/KscopeAudioPreview.kt b/kotlin/src/ink/kscope/music/KscopeAudioPreview.kt
new file mode 100644
index 0000000..038d84a
--- /dev/null
+++ b/kotlin/src/ink/kscope/music/KscopeAudioPreview.kt
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2022 Project Kaleidoscope
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ink.kscope.music
+
+import android.app.Activity
+import android.content.AsyncQueryHandler
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.media.AudioManager
+import android.media.AudioManager.OnAudioFocusChangeListener
+import android.media.MediaMetadataRetriever
+import android.media.MediaPlayer
+import android.media.MediaPlayer.*
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import android.text.TextUtils
+import android.util.Log
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.View
+import android.view.Window
+import android.widget.*
+import android.widget.SeekBar.OnSeekBarChangeListener
+import com.android.music.R
+import java.io.IOException
+import java.time.Duration
+import java.util.*
+
+/**
+ * Dialog that comes up in response to various music-related VIEW intents.
+ */
+class KscopeAudioPreview : Activity(), OnPreparedListener, OnErrorListener, OnCompletionListener {
+
+ private lateinit var mTextLine1: TextView
+ private lateinit var mTextLine2: TextView
+ private lateinit var mLoadingText: TextView
+ private lateinit var mAudioPlayedTimeText: TextView
+ private lateinit var mAudioTotalTimeText: TextView
+ private lateinit var mSeekBar: SeekBar
+ private lateinit var mAudioManager: AudioManager
+
+ private var mPlayer: PreviewPlayer? = null
+ private var mSeeking = false
+ private var mUiPaused = true
+ private var mDuration = 0
+ private var mUri: Uri? = null
+ private var mMediaId: Long = -1
+ private var mPausedByTransientLossOfFocus = false
+
+ private lateinit var mProgressRefresher: Handler
+
+ override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+ mProgressRefresher = Handler(mainLooper)
+ val intent = intent
+ if (intent == null) {
+ finish()
+ return
+ }
+ mUri = intent.data
+ if (mUri == null) {
+ finish()
+ return
+ }
+ val scheme = mUri?.scheme
+ volumeControlStream = AudioManager.STREAM_MUSIC
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setContentView(R.layout.kscope_audio_preview)
+ mTextLine1 = findViewById(R.id.line1)
+ mTextLine2 = findViewById(R.id.line2)
+ mLoadingText = findViewById(R.id.loading)
+ mAudioPlayedTimeText = findViewById(R.id.audio_played_time)
+ mAudioTotalTimeText = findViewById(R.id.audio_total_time)
+ if (scheme == "http") {
+ val msg = getString(R.string.streamloadingtext, mUri!!.host)
+ mLoadingText.text = msg
+ } else {
+ mLoadingText.visibility = View.GONE
+ }
+ mSeekBar = findViewById(R.id.progress)
+ mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val player = lastNonConfigurationInstance as PreviewPlayer?
+ if (player == null) {
+ mPlayer = PreviewPlayer()
+ mPlayer!!.setActivity(this)
+ try {
+ mPlayer!!.setDataSourceAndPrepare(mUri!!)
+ } catch (ex: Exception) {
+ // catch generic Exception, since we may be called with a media
+ // content URI, another content provider's URI, a file URI,
+ // an http URI, and there are different exceptions associated
+ // with failure to open each of those.
+ Log.d(TAG, "Failed to open file: $ex")
+ Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show()
+ finish()
+ return
+ }
+ } else {
+ mPlayer = player
+ mPlayer!!.setActivity(this)
+ // onResume will update the UI
+ }
+ val mAsyncQueryHandler = object : AsyncQueryHandler(contentResolver) {
+ override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) {
+ if (cursor != null && cursor.moveToFirst()) {
+ val titleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val artistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
+ val idIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ val displaynameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+
+ val mediaMetadataRetriever = MediaMetadataRetriever()
+ mediaMetadataRetriever.setDataSource(this@KscopeAudioPreview, mUri)
+ val mediaTitle = mediaMetadataRetriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_TITLE)
+ val mediaArtist = mediaMetadataRetriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_ARTIST)
+ if (idIdx >= 0) {
+ mMediaId = cursor.getLong(idIdx)
+ }
+ if (titleIdx >= 0) {
+ val title = cursor.getString(titleIdx)
+ mTextLine1.text = title
+ if (artistIdx >= 0) {
+ val artist = cursor.getString(artistIdx)
+ mTextLine2.text = artist
+ }
+ } else if (mediaTitle != null) {
+ mTextLine1.text = mediaTitle
+ } else {
+ if (displaynameIdx >= 0) {
+ val name = cursor.getString(displaynameIdx)
+ mTextLine1.text = name
+ }
+ // Couldn't find anything to display, what to do now?
+ Log.w(TAG, "Cursor had no names for us")
+ }
+ if (mediaArtist != null) {
+ mTextLine2.text = mediaArtist
+ } else {
+ mTextLine2.text = getString(R.string.kscope_audio_preview_artist_unknown)
+ }
+ } else {
+ Log.w(TAG, "empty cursor")
+ }
+ cursor?.close()
+ setNames()
+ }
+ }
+ if (scheme == ContentResolver.SCHEME_CONTENT) {
+ if (mUri!!.authority === MediaStore.AUTHORITY) {
+ // try to get title and artist from the media content provider
+ mAsyncQueryHandler.startQuery(0, null, mUri,
+ arrayOf(MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST),
+ null, null, null)
+ } else {
+ // Try to get the display name from another content provider.
+ // Don't specifically ask for the display name though, since the
+ // provider might not actually support that column.
+ mAsyncQueryHandler.startQuery(0, null, mUri, null, null, null, null)
+ }
+ } else if (scheme == "file") {
+ // check if this file is in the media database (clicking on a download
+ // in the download manager might follow this path
+ val path = mUri?.path
+ mAsyncQueryHandler.startQuery(0, null, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ARTIST), MediaStore.Audio.Media.DATA + "=?",
+ arrayOf(path), null)
+ } else {
+ // We can't get metadata from the file/stream itself yet, because
+ // that API is hidden, so instead we display the URI being played
+ if (mPlayer!!.isPrepared) {
+ setNames()
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mUiPaused = true
+ mProgressRefresher.removeCallbacksAndMessages(null)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mUiPaused = false
+ if (mPlayer!!.isPrepared) {
+ showPostPrepareUI()
+ }
+ }
+
+ override fun onRetainNonConfigurationInstance(): Any? {
+ val player = mPlayer
+ mPlayer = null
+ return player
+ }
+
+ override fun onDestroy() {
+ stopPlayback()
+ super.onDestroy()
+ }
+
+ private fun stopPlayback() {
+ mProgressRefresher.removeCallbacksAndMessages(null)
+ if (mPlayer != null) {
+ mPlayer?.release()
+ mPlayer = null
+ mAudioManager.abandonAudioFocus(mAudioFocusListener)
+ }
+ }
+
+ override fun onUserLeaveHint() {
+ stopPlayback()
+ finish()
+ super.onUserLeaveHint()
+ }
+
+ override fun onPrepared(mp: MediaPlayer?) {
+ if (isFinishing) return
+ mPlayer = mp as PreviewPlayer
+ setNames()
+ mPlayer?.start()
+ showPostPrepareUI()
+ }
+
+ private fun showPostPrepareUI() {
+ val pb: ProgressBar = findViewById(R.id.spinner)
+ pb.visibility = View.GONE
+ mDuration = mPlayer!!.duration
+ mAudioTotalTimeText.text = updateAudioTime(mDuration.toLong())
+ if (mDuration != 0) {
+ mSeekBar.max = mDuration
+ mSeekBar.visibility = View.VISIBLE
+ if (!mSeeking) {
+ mSeekBar.progress = mPlayer!!.currentPosition
+ }
+ }
+ mSeekBar.setOnSeekBarChangeListener(mSeekListener)
+ mLoadingText.visibility = View.GONE
+ val v: View = findViewById(R.id.titleandbuttons)
+ v.visibility = View.VISIBLE
+ mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+ mProgressRefresher.removeCallbacksAndMessages(null)
+ mProgressRefresher.postDelayed(ProgressRefresher(), 200)
+ updatePlayPause()
+ }
+
+ private val mAudioFocusListener = object : OnAudioFocusChangeListener {
+ override fun onAudioFocusChange(focusChange: Int) {
+ if (mPlayer == null) {
+ // this activity has handed its MediaPlayer off to the next activity
+ // (e.g. portrait/landscape switch) and should abandon its focus
+ mAudioManager.abandonAudioFocus(this)
+ return
+ }
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ mPausedByTransientLossOfFocus = false
+ mPlayer?.pause()
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ->
+ if (mPlayer!!.isPlaying) {
+ mPausedByTransientLossOfFocus = true
+ mPlayer?.pause()
+ }
+ AudioManager.AUDIOFOCUS_GAIN -> if (mPausedByTransientLossOfFocus) {
+ mPausedByTransientLossOfFocus = false
+ start()
+ }
+ }
+ updatePlayPause()
+ }
+ }
+
+ private fun start() {
+ mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+ mPlayer?.start()
+ mProgressRefresher.postDelayed(ProgressRefresher(), 200)
+ }
+
+ private fun setNames() {
+ if (TextUtils.isEmpty(mTextLine1.text)) {
+ mTextLine1.text = mUri!!.lastPathSegment
+ }
+ mTextLine2.visibility = if (TextUtils.isEmpty(mTextLine2.text)) View.GONE else View.VISIBLE
+ }
+
+ internal inner class ProgressRefresher : Runnable {
+ override fun run() {
+ if (mPlayer != null && mDuration != 0) {
+ mSeekBar.progress = mPlayer!!.currentPosition
+ }
+ mProgressRefresher.removeCallbacksAndMessages(null)
+ if (!mUiPaused) {
+ mProgressRefresher.postDelayed(ProgressRefresher(), 200)
+ }
+ mAudioPlayedTimeText.text = updateAudioTime(mPlayer!!.currentPosition.toLong())
+ }
+ }
+
+ private fun updatePlayPause() {
+ val b: ImageButton? = findViewById(R.id.playpause)
+ if (b != null && mPlayer != null) {
+ if (mPlayer!!.isPlaying) {
+ b.setImageResource(R.drawable.kscope_audio_preview_pause_ic)
+ } else {
+ b.setImageResource(R.drawable.kscope_audio_preview_play_ic)
+ mProgressRefresher.removeCallbacksAndMessages(null)
+ }
+ }
+ }
+
+ private fun updateAudioTime(time: Long): String {
+ val duration = Duration.ofMillis(time)
+ val seconds = duration.toSecondsPart()
+ val minutes = duration.toMinutesPart()
+ val hours = duration.toHoursPart()
+ val days = duration.toDaysPart()
+ StringBuilder().apply {
+ if (days > 0) {
+ if (days < 10) {
+ append("0")
+ }
+ append("$days:")
+ }
+ if (hours > 0) {
+ if (hours < 10) {
+ append("0")
+ }
+ append("$hours:")
+ }
+ if (minutes > 0) {
+ if (minutes < 10) {
+ append("0")
+ }
+ append("$minutes:")
+ } else {
+ append("00:")
+ }
+ if (seconds > 0) {
+ if (seconds < 10) {
+ append("0")
+ }
+ append(seconds)
+ } else {
+ append("00")
+ }
+ return toString()
+ }
+
+ }
+
+ private val mSeekListener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {
+ override fun onStartTrackingTouch(bar: SeekBar?) {
+ mSeeking = true
+ }
+
+ override fun onProgressChanged(bar: SeekBar?, progress: Int, fromuser: Boolean) {
+ if (!fromuser) {
+ return
+ }
+ // Protection for case of simultaneously tapping on seek bar and exit
+ mPlayer?.seekTo(progress)
+ mAudioPlayedTimeText.text = updateAudioTime(progress.toLong())
+ }
+
+ override fun onStopTrackingTouch(bar: SeekBar?) {
+ mSeeking = false
+ }
+ }
+
+ override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
+ Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show()
+ finish()
+ return true
+ }
+
+ override fun onCompletion(mp: MediaPlayer?) {
+ mSeekBar.progress = mDuration
+ mAudioPlayedTimeText.text = updateAudioTime(mDuration.toLong())
+ updatePlayPause()
+ }
+
+ fun playPauseClicked(v: View?) {
+ // Protection for case of simultaneously tapping on play/pause and exit
+ mPlayer?.let {
+ if (it.isPlaying) {
+ it.pause()
+ } else {
+ start()
+ }
+ updatePlayPause()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ super.onCreateOptionsMenu(menu)
+ // TODO: if mMediaId != -1, then the playing file has an entry in the media
+ // database, and we could open it in the full music app instead.
+ // Ideally, we would hand off the currently running mediaplayer
+ // to the music UI, which can probably be done via a public static
+ menu.add(0, OPEN_IN_MUSIC, 0, "open in music")
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ val item = menu.findItem(OPEN_IN_MUSIC)
+ if (mMediaId >= 0) {
+ item.isVisible = true
+ return true
+ }
+ item.isVisible = false
+ return false
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ when (keyCode) {
+ KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
+ if (mPlayer!!.isPlaying) {
+ mPlayer?.pause()
+ } else {
+ start()
+ }
+ updatePlayPause()
+ return true
+ }
+ KeyEvent.KEYCODE_MEDIA_PLAY -> {
+ start()
+ updatePlayPause()
+ return true
+ }
+ KeyEvent.KEYCODE_MEDIA_PAUSE -> {
+ if (mPlayer!!.isPlaying) {
+ mPlayer?.pause()
+ }
+ updatePlayPause()
+ return true
+ }
+ KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+ KeyEvent.KEYCODE_MEDIA_NEXT,
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+ KeyEvent.KEYCODE_MEDIA_REWIND -> return true
+ KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_BACK -> {
+ stopPlayback()
+ finish()
+ return true
+ }
+ else -> return super.onKeyDown(keyCode, event)
+ }
+ }
+
+ /*
+ * Wrapper class to help with handing off the MediaPlayer to the next instance
+ * of the activity in case of orientation change, without losing any state.
+ */
+ private class PreviewPlayer : MediaPlayer(), OnPreparedListener {
+ private lateinit var mActivity: KscopeAudioPreview
+ var isPrepared = false
+
+ fun setActivity(activity: KscopeAudioPreview) {
+ mActivity = activity
+ setOnPreparedListener(this)
+ setOnErrorListener(mActivity)
+ setOnCompletionListener(mActivity)
+ }
+
+ @Throws(IllegalArgumentException::class, SecurityException::class,
+ IllegalStateException::class, IOException::class)
+ fun setDataSourceAndPrepare(uri: Uri) {
+ setDataSource(mActivity, uri)
+ prepareAsync()
+ }
+
+ /* (non-Javadoc)
+ * @see android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media.MediaPlayer)
+ */
+ override fun onPrepared(mp: MediaPlayer?) {
+ isPrepared = true
+ mActivity.onPrepared(mp)
+ }
+ }
+
+ companion object {
+ private const val TAG = "AudioPreview"
+ private const val OPEN_IN_MUSIC = 1
+ }
+}