/* * 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(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(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 } }