summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp2
-rw-r--r--AndroidManifest.xml2
-rw-r--r--kotlin/res/drawable/kscope_audio_preview_pause_ic.xml10
-rw-r--r--kotlin/res/drawable/kscope_audio_preview_play_ic.xml10
-rw-r--r--kotlin/res/layout/kscope_audio_preview.xml118
-rw-r--r--kotlin/res/values-night/kscope_themes.xml18
-rw-r--r--kotlin/res/values/kscope_strings.xml18
-rw-r--r--kotlin/res/values/kscope_themes.xml18
-rw-r--r--kotlin/src/ink/kscope/music/KscopeAudioPreview.kt507
9 files changed, 701 insertions, 2 deletions
diff --git a/Android.bp b/Android.bp
index 846c407..12eb1a2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -17,7 +17,7 @@ license {
android_app {
name: "Music",
- srcs: ["src/**/*.java"],
+ srcs: ["kotlin/src/**/*.kt"],
resource_dirs: ["kotlin/res"],
sdk_version: "current",
product_specific: true,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 48d416c..a529fb6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -37,7 +37,7 @@
android:launchMode="singleTop"
android:requestLegacyExternalStorage="true">
- <activity android:name="AudioPreview" android:theme="@android:style/Theme.Dialog"
+ <activity android:name="ink.kscope.music.KscopeAudioPreview" android:theme="@style/Theme.Kscope.AudioPreview"
android:taskAffinity=""
android:excludeFromRecents="true" android:exported="true" >
<intent-filter>
diff --git a/kotlin/res/drawable/kscope_audio_preview_pause_ic.xml b/kotlin/res/drawable/kscope_audio_preview_pause_ic.xml
new file mode 100644
index 0000000..6d9f364
--- /dev/null
+++ b/kotlin/res/drawable/kscope_audio_preview_pause_ic.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:tint="?android:colorControlNormal"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
+</vector>
diff --git a/kotlin/res/drawable/kscope_audio_preview_play_ic.xml b/kotlin/res/drawable/kscope_audio_preview_play_ic.xml
new file mode 100644
index 0000000..6338e3d
--- /dev/null
+++ b/kotlin/res/drawable/kscope_audio_preview_play_ic.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:tint="?android:colorControlNormal"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M8,5v14l11,-7z" />
+</vector>
diff --git a/kotlin/res/layout/kscope_audio_preview.xml b/kotlin/res/layout/kscope_audio_preview.xml
new file mode 100644
index 0000000..fb38e75
--- /dev/null
+++ b/kotlin/res/layout/kscope_audio_preview.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minWidth="500dp"
+ android:orientation="vertical"
+ android:padding="15dp">
+
+ <ProgressBar
+ android:id="@+id/spinner"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" />
+
+ <TextView
+ android:id="@+id/loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingTop="5dp"
+ android:textColor="#ffffffff"
+ android:textSize="14sp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/line1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:paddingTop="5dp"
+ android:singleLine="true"
+ android:textAppearance="?android:textAppearanceLarge" />
+
+ <TextView
+ android:id="@+id/line2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:paddingTop="5dp"
+ android:textAppearance="?android:textAppearanceSmall" />
+
+ </LinearLayout>
+
+ <SeekBar
+ android:id="@+id/progress"
+ android:layout_width="match_parent"
+ android:layout_marginTop="15dp"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/audio_played_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true" />
+
+ <TextView
+ android:id="@+id/audio_total_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/titleandbuttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <ImageButton
+ android:id="@+id/playpause"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="center_horizontal"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:onClick="playPauseClicked"
+ android:src="@drawable/kscope_audio_preview_pause_ic" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/kotlin/res/values-night/kscope_themes.xml b/kotlin/res/values-night/kscope_themes.xml
new file mode 100644
index 0000000..f3befc2
--- /dev/null
+++ b/kotlin/res/values-night/kscope_themes.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <style name="Theme.Kscope.AudioPreview" parent="android:Theme.DeviceDefault.Dialog" />
+</resources>
diff --git a/kotlin/res/values/kscope_strings.xml b/kotlin/res/values/kscope_strings.xml
new file mode 100644
index 0000000..4e52507
--- /dev/null
+++ b/kotlin/res/values/kscope_strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <string name="kscope_audio_preview_artist_unknown">Unknown Artist</string>
+</resources>
diff --git a/kotlin/res/values/kscope_themes.xml b/kotlin/res/values/kscope_themes.xml
new file mode 100644
index 0000000..36a1f65
--- /dev/null
+++ b/kotlin/res/values/kscope_themes.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <style name="Theme.Kscope.AudioPreview" parent="android:Theme.DeviceDefault.Light.Dialog" />
+</resources>
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
+ }
+}