blob: e883b23c556ab633983b3b629094f78d4ff8220d [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.music
import android.app.Activity
import android.content.AsyncQueryHandler
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.media.MediaPlayer
import android.media.MediaPlayer.OnCompletionListener
import android.media.MediaPlayer.OnErrorListener
import android.media.MediaPlayer.OnPreparedListener
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.MenuItem
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import android.widget.Toast
import java.io.IOException
/**
* Dialog that comes up in response to various music-related VIEW intents.
*/
class AudioPreview : Activity(), OnPreparedListener, OnErrorListener, OnCompletionListener {
private lateinit var mTextLine1: TextView
private lateinit var mTextLine2: TextView
private lateinit var mLoadingText: 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 val mProgressRefresher = Handler()
override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
val intent: Intent? = getIntent()
if (intent == null) {
finish()
return
}
mUri = intent.getData()
if (mUri == null) {
finish()
return
}
val scheme: String? = mUri?.getScheme()
setVolumeControlStream(AudioManager.STREAM_MUSIC)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.audiopreview)
mTextLine1 = findViewById(R.id.line1) as TextView
mTextLine2 = findViewById(R.id.line2) as TextView
mLoadingText = findViewById(R.id.loading) as TextView
if (scheme == "http") {
val msg: String = getString(R.string.streamloadingtext, mUri!!.getHost())
mLoadingText.setText(msg)
} else {
mLoadingText.setVisibility(View.GONE)
}
mSeekBar = findViewById(R.id.progress) as SeekBar
mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val player = getLastNonConfigurationInstance() 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: AsyncQueryHandler = object : AsyncQueryHandler(getContentResolver()) {
protected override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) {
if (cursor != null && cursor.moveToFirst()) {
val titleIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
val artistIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
val idIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
val displaynameIdx: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idIdx >= 0) {
mMediaId = cursor.getLong(idIdx)
}
if (titleIdx >= 0) {
val title: String = cursor.getString(titleIdx)
mTextLine1.setText(title)
if (artistIdx >= 0) {
val artist: String = cursor.getString(artistIdx)
mTextLine2.setText(artist)
}
} else if (displaynameIdx >= 0) {
val name: String = cursor.getString(displaynameIdx)
mTextLine1.setText(name)
} else {
// Couldn't find anything to display, what to do now?
Log.w(TAG, "Cursor had no names for us")
}
} else {
Log.w(TAG, "empty cursor")
}
cursor?.let {
it.close()
}
setNames()
}
}
if (scheme == ContentResolver.SCHEME_CONTENT) {
if (mUri!!.getAuthority() === MediaStore.AUTHORITY) {
// try to get title and artist from the media content provider
mAsyncQueryHandler.startQuery(0, null, mUri, arrayOf<String>(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: String? = mUri?.getPath()
mAsyncQueryHandler.startQuery(0, null, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, arrayOf<String>(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST), MediaStore.Audio.Media.DATA.toString() + "=?", 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) as ProgressBar
pb.setVisibility(View.GONE)
mDuration = mPlayer!!.getDuration()
if (mDuration != 0) {
mSeekBar.setMax(mDuration)
mSeekBar.setVisibility(View.VISIBLE)
if (!mSeeking) {
mSeekBar.setProgress(mPlayer!!.getCurrentPosition())
}
}
mSeekBar.setOnSeekBarChangeListener(mSeekListener)
mLoadingText.setVisibility(View.GONE)
val v: View = findViewById(R.id.titleandbuttons)
v.setVisibility(View.VISIBLE)
mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
mProgressRefresher.removeCallbacksAndMessages(null)
mProgressRefresher.postDelayed(ProgressRefresher(), 200)
updatePlayPause()
}
private val mAudioFocusListener: OnAudioFocusChangeListener = 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.getText())) {
mTextLine1.setText(mUri!!.getLastPathSegment())
}
if (TextUtils.isEmpty(mTextLine2.getText())) {
mTextLine2.setVisibility(View.GONE)
} else {
mTextLine2.setVisibility(View.VISIBLE)
}
}
internal inner class ProgressRefresher : Runnable {
override fun run() {
if (mPlayer != null && !mSeeking && mDuration != 0) {
mSeekBar.setProgress(mPlayer!!.getCurrentPosition())
}
mProgressRefresher.removeCallbacksAndMessages(null)
if (!mUiPaused) {
mProgressRefresher.postDelayed(ProgressRefresher(), 200)
}
}
}
private fun updatePlayPause() {
val b: ImageButton? = findViewById(R.id.playpause) as ImageButton?
if (b != null && mPlayer != null) {
if (mPlayer!!.isPlaying()) {
b.setImageResource(R.drawable.btn_playback_ic_pause_small)
} else {
b.setImageResource(R.drawable.btn_playback_ic_play_small)
mProgressRefresher.removeCallbacksAndMessages(null)
}
}
}
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?.let{
it.seekTo(progress)
}
}
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.setProgress(mDuration)
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: MenuItem = menu.findItem(OPEN_IN_MUSIC)
if (mMediaId >= 0) {
item.setVisible(true)
return true
}
item.setVisible(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: AudioPreview
var isPrepared = false
fun setActivity(activity: AudioPreview) {
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
}
}