1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.music 17 18 import android.app.Activity 19 import android.content.AsyncQueryHandler 20 import android.content.ContentResolver 21 import android.content.Context 22 import android.content.Intent 23 import android.database.Cursor 24 import android.media.AudioManager 25 import android.media.AudioManager.OnAudioFocusChangeListener 26 import android.media.MediaPlayer 27 import android.media.MediaPlayer.OnCompletionListener 28 import android.media.MediaPlayer.OnErrorListener 29 import android.media.MediaPlayer.OnPreparedListener 30 import android.net.Uri 31 import android.os.Bundle 32 import android.os.Handler 33 import android.provider.MediaStore 34 import android.provider.OpenableColumns 35 import android.text.TextUtils 36 import android.util.Log 37 import android.view.KeyEvent 38 import android.view.Menu 39 import android.view.MenuItem 40 import android.view.View 41 import android.view.Window 42 import android.view.WindowManager 43 import android.widget.ImageButton 44 import android.widget.ProgressBar 45 import android.widget.SeekBar 46 import android.widget.SeekBar.OnSeekBarChangeListener 47 import android.widget.TextView 48 import android.widget.Toast 49 import java.io.IOException 50 51 /** 52 * Dialog that comes up in response to various music-related VIEW intents. 53 */ 54 class AudioPreview : Activity(), OnPreparedListener, OnErrorListener, OnCompletionListener { 55 private lateinit var mTextLine1: TextView 56 private lateinit var mTextLine2: TextView 57 private lateinit var mLoadingText: TextView 58 private lateinit var mSeekBar: SeekBar 59 private lateinit var mAudioManager: AudioManager 60 61 private var mPlayer: PreviewPlayer? = null 62 private var mSeeking = false 63 private var mUiPaused = true 64 private var mDuration = 0 65 private var mUri: Uri? = null 66 private var mMediaId: Long = -1 67 private var mPausedByTransientLossOfFocus = false 68 69 private val mProgressRefresher = Handler() 70 onCreatenull71 override fun onCreate(icicle: Bundle?) { 72 super.onCreate(icicle) 73 val intent: Intent? = getIntent() 74 if (intent == null) { 75 finish() 76 return 77 } 78 mUri = intent.getData() 79 if (mUri == null) { 80 finish() 81 return 82 } 83 val scheme: String? = mUri?.getScheme() 84 setVolumeControlStream(AudioManager.STREAM_MUSIC) 85 requestWindowFeature(Window.FEATURE_NO_TITLE) 86 setContentView(R.layout.audiopreview) 87 mTextLine1 = findViewById(R.id.line1) as TextView 88 mTextLine2 = findViewById(R.id.line2) as TextView 89 mLoadingText = findViewById(R.id.loading) as TextView 90 if (scheme == "http") { 91 val msg: String = getString(R.string.streamloadingtext, mUri!!.getHost()) 92 mLoadingText.setText(msg) 93 } else { 94 mLoadingText.setVisibility(View.GONE) 95 } 96 mSeekBar = findViewById(R.id.progress) as SeekBar 97 mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager 98 val player = getLastNonConfigurationInstance() as PreviewPlayer? 99 if (player == null) { 100 mPlayer = PreviewPlayer() 101 mPlayer!!.setActivity(this) 102 try { 103 mPlayer!!.setDataSourceAndPrepare(mUri!!) 104 } catch (ex: Exception) { 105 // catch generic Exception, since we may be called with a media 106 // content URI, another content provider's URI, a file URI, 107 // an http URI, and there are different exceptions associated 108 // with failure to open each of those. 109 Log.d(TAG, "Failed to open file: $ex") 110 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show() 111 finish() 112 return 113 } 114 } else { 115 mPlayer = player 116 mPlayer!!.setActivity(this) 117 // onResume will update the UI 118 } 119 val mAsyncQueryHandler: AsyncQueryHandler = object : AsyncQueryHandler(getContentResolver()) { 120 protected override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) { 121 if (cursor != null && cursor.moveToFirst()) { 122 val titleIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE) 123 val artistIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST) 124 val idIdx: Int = cursor.getColumnIndex(MediaStore.Audio.Media._ID) 125 val displaynameIdx: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) 126 if (idIdx >= 0) { 127 mMediaId = cursor.getLong(idIdx) 128 } 129 if (titleIdx >= 0) { 130 val title: String = cursor.getString(titleIdx) 131 mTextLine1.setText(title) 132 if (artistIdx >= 0) { 133 val artist: String = cursor.getString(artistIdx) 134 mTextLine2.setText(artist) 135 } 136 } else if (displaynameIdx >= 0) { 137 val name: String = cursor.getString(displaynameIdx) 138 mTextLine1.setText(name) 139 } else { 140 // Couldn't find anything to display, what to do now? 141 Log.w(TAG, "Cursor had no names for us") 142 } 143 } else { 144 Log.w(TAG, "empty cursor") 145 } 146 cursor?.let { 147 it.close() 148 } 149 setNames() 150 } 151 } 152 if (scheme == ContentResolver.SCHEME_CONTENT) { 153 if (mUri!!.getAuthority() === MediaStore.AUTHORITY) { 154 // try to get title and artist from the media content provider 155 mAsyncQueryHandler.startQuery(0, null, mUri, arrayOf<String>(MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST), 156 null, null, null) 157 } else { 158 // Try to get the display name from another content provider. 159 // Don't specifically ask for the display name though, since the 160 // provider might not actually support that column. 161 mAsyncQueryHandler.startQuery(0, null, mUri, null, null, null, null) 162 } 163 } else if (scheme == "file") { 164 // check if this file is in the media database (clicking on a download 165 // in the download manager might follow this path 166 val path: String? = mUri?.getPath() 167 mAsyncQueryHandler.startQuery(0, null, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, arrayOf<String>(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, 168 MediaStore.Audio.Media.ARTIST), MediaStore.Audio.Media.DATA.toString() + "=?", arrayOf(path), null) 169 } else { 170 // We can't get metadata from the file/stream itself yet, because 171 // that API is hidden, so instead we display the URI being played 172 if (mPlayer!!.isPrepared) { 173 setNames() 174 } 175 } 176 } 177 onPausenull178 override fun onPause() { 179 super.onPause() 180 mUiPaused = true 181 mProgressRefresher.removeCallbacksAndMessages(null) 182 } 183 onResumenull184 override fun onResume() { 185 super.onResume() 186 mUiPaused = false 187 if (mPlayer!!.isPrepared) { 188 showPostPrepareUI() 189 } 190 } 191 onRetainNonConfigurationInstancenull192 override fun onRetainNonConfigurationInstance(): Any? { 193 val player = mPlayer 194 mPlayer = null 195 return player 196 } 197 onDestroynull198 override fun onDestroy() { 199 stopPlayback() 200 super.onDestroy() 201 } 202 stopPlaybacknull203 private fun stopPlayback() { 204 mProgressRefresher.removeCallbacksAndMessages(null) 205 if (mPlayer != null) { 206 mPlayer?.release() 207 mPlayer = null 208 mAudioManager.abandonAudioFocus(mAudioFocusListener) 209 } 210 } 211 onUserLeaveHintnull212 override fun onUserLeaveHint() { 213 stopPlayback() 214 finish() 215 super.onUserLeaveHint() 216 } 217 onPreparednull218 override fun onPrepared(mp: MediaPlayer?) { 219 if (isFinishing()) return 220 mPlayer = mp as PreviewPlayer 221 setNames() 222 mPlayer?.start() 223 showPostPrepareUI() 224 } 225 showPostPrepareUInull226 private fun showPostPrepareUI() { 227 val pb: ProgressBar = findViewById(R.id.spinner) as ProgressBar 228 pb.setVisibility(View.GONE) 229 mDuration = mPlayer!!.getDuration() 230 if (mDuration != 0) { 231 mSeekBar.setMax(mDuration) 232 mSeekBar.setVisibility(View.VISIBLE) 233 if (!mSeeking) { 234 mSeekBar.setProgress(mPlayer!!.getCurrentPosition()) 235 } 236 } 237 mSeekBar.setOnSeekBarChangeListener(mSeekListener) 238 mLoadingText.setVisibility(View.GONE) 239 val v: View = findViewById(R.id.titleandbuttons) 240 v.setVisibility(View.VISIBLE) 241 mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, 242 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) 243 mProgressRefresher.removeCallbacksAndMessages(null) 244 mProgressRefresher.postDelayed(ProgressRefresher(), 200) 245 updatePlayPause() 246 } 247 248 private val mAudioFocusListener: OnAudioFocusChangeListener = object : OnAudioFocusChangeListener { onAudioFocusChangenull249 override fun onAudioFocusChange(focusChange: Int) { 250 if (mPlayer == null) { 251 // this activity has handed its MediaPlayer off to the next activity 252 // (e.g. portrait/landscape switch) and should abandon its focus 253 mAudioManager.abandonAudioFocus(this) 254 return 255 } 256 when (focusChange) { 257 AudioManager.AUDIOFOCUS_LOSS -> { 258 mPausedByTransientLossOfFocus = false 259 mPlayer?.pause() 260 } 261 AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> if (mPlayer!!.isPlaying()) { 262 mPausedByTransientLossOfFocus = true 263 mPlayer?.pause() 264 } 265 AudioManager.AUDIOFOCUS_GAIN -> if (mPausedByTransientLossOfFocus) { 266 mPausedByTransientLossOfFocus = false 267 start() 268 } 269 } 270 updatePlayPause() 271 } 272 } 273 startnull274 private fun start() { 275 mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, 276 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) 277 mPlayer?.start() 278 mProgressRefresher.postDelayed(ProgressRefresher(), 200) 279 } 280 setNamesnull281 private fun setNames() { 282 if (TextUtils.isEmpty(mTextLine1.getText())) { 283 mTextLine1.setText(mUri!!.getLastPathSegment()) 284 } 285 if (TextUtils.isEmpty(mTextLine2.getText())) { 286 mTextLine2.setVisibility(View.GONE) 287 } else { 288 mTextLine2.setVisibility(View.VISIBLE) 289 } 290 } 291 292 internal inner class ProgressRefresher : Runnable { runnull293 override fun run() { 294 if (mPlayer != null && !mSeeking && mDuration != 0) { 295 mSeekBar.setProgress(mPlayer!!.getCurrentPosition()) 296 } 297 mProgressRefresher.removeCallbacksAndMessages(null) 298 if (!mUiPaused) { 299 mProgressRefresher.postDelayed(ProgressRefresher(), 200) 300 } 301 } 302 } 303 updatePlayPausenull304 private fun updatePlayPause() { 305 val b: ImageButton? = findViewById(R.id.playpause) as ImageButton? 306 if (b != null && mPlayer != null) { 307 if (mPlayer!!.isPlaying()) { 308 b.setImageResource(R.drawable.btn_playback_ic_pause_small) 309 } else { 310 b.setImageResource(R.drawable.btn_playback_ic_play_small) 311 mProgressRefresher.removeCallbacksAndMessages(null) 312 } 313 } 314 } 315 316 private val mSeekListener: OnSeekBarChangeListener = object : OnSeekBarChangeListener { onStartTrackingTouchnull317 override fun onStartTrackingTouch(bar: SeekBar?) { 318 mSeeking = true 319 } 320 onProgressChangednull321 override fun onProgressChanged(bar: SeekBar?, progress: Int, fromuser: Boolean) { 322 if (!fromuser) { 323 return 324 } 325 // Protection for case of simultaneously tapping on seek bar and exit 326 mPlayer?.let{ 327 it.seekTo(progress) 328 } 329 } 330 onStopTrackingTouchnull331 override fun onStopTrackingTouch(bar: SeekBar?) { 332 mSeeking = false 333 } 334 } 335 onErrornull336 override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { 337 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show() 338 finish() 339 return true 340 } 341 onCompletionnull342 override fun onCompletion(mp: MediaPlayer?) { 343 mSeekBar.setProgress(mDuration) 344 updatePlayPause() 345 } 346 playPauseClickednull347 fun playPauseClicked(v: View?) { 348 // Protection for case of simultaneously tapping on play/pause and exit 349 mPlayer?.let { 350 if (it.isPlaying()) { 351 it.pause() 352 } else { 353 start() 354 } 355 updatePlayPause() 356 } 357 } 358 onCreateOptionsMenunull359 override fun onCreateOptionsMenu(menu: Menu): Boolean { 360 super.onCreateOptionsMenu(menu) 361 // TODO: if mMediaId != -1, then the playing file has an entry in the media 362 // database, and we could open it in the full music app instead. 363 // Ideally, we would hand off the currently running mediaplayer 364 // to the music UI, which can probably be done via a public static 365 menu.add(0, OPEN_IN_MUSIC, 0, "open in music") 366 return true 367 } 368 onPrepareOptionsMenunull369 override fun onPrepareOptionsMenu(menu: Menu): Boolean { 370 val item: MenuItem = menu.findItem(OPEN_IN_MUSIC) 371 if (mMediaId >= 0) { 372 item.setVisible(true) 373 return true 374 } 375 item.setVisible(false) 376 return false 377 } 378 onKeyDownnull379 override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 380 when (keyCode) { 381 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { 382 if (mPlayer!!.isPlaying()) { 383 mPlayer?.pause() 384 } else { 385 start() 386 } 387 updatePlayPause() 388 return true 389 } 390 KeyEvent.KEYCODE_MEDIA_PLAY -> { 391 start() 392 updatePlayPause() 393 return true 394 } 395 KeyEvent.KEYCODE_MEDIA_PAUSE -> { 396 if (mPlayer!!.isPlaying()) { 397 mPlayer?.pause() 398 } 399 updatePlayPause() 400 return true 401 } 402 KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, 403 KeyEvent.KEYCODE_MEDIA_NEXT, 404 KeyEvent.KEYCODE_MEDIA_PREVIOUS, 405 KeyEvent.KEYCODE_MEDIA_REWIND -> return true 406 KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_BACK -> { 407 stopPlayback() 408 finish() 409 return true 410 } 411 else -> return super.onKeyDown(keyCode, event) 412 } 413 } 414 415 /* 416 * Wrapper class to help with handing off the MediaPlayer to the next instance 417 * of the activity in case of orientation change, without losing any state. 418 */ 419 private class PreviewPlayer : MediaPlayer(), OnPreparedListener { 420 private lateinit var mActivity: AudioPreview 421 var isPrepared = false 422 setActivitynull423 fun setActivity(activity: AudioPreview) { 424 mActivity = activity 425 setOnPreparedListener(this) 426 setOnErrorListener(mActivity) 427 setOnCompletionListener(mActivity) 428 } 429 430 @Throws(IllegalArgumentException::class, SecurityException::class, IllegalStateException::class, IOException::class) setDataSourceAndPreparenull431 fun setDataSourceAndPrepare(uri: Uri) { 432 setDataSource(mActivity, uri) 433 prepareAsync() 434 } 435 436 /* (non-Javadoc) 437 * @see android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media.MediaPlayer) 438 */ onPreparednull439 override fun onPrepared(mp: MediaPlayer?) { 440 isPrepared = true 441 mActivity.onPrepared(mp) 442 } 443 } 444 445 companion object { 446 private const val TAG = "AudioPreview" 447 private const val OPEN_IN_MUSIC = 1 448 } 449 }