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 }