playing in background
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class OralTrainerController internal constructor(
|
|||||||
)
|
)
|
||||||
player = ExoPlayer.Builder(appContext)
|
player = ExoPlayer.Builder(appContext)
|
||||||
.setMediaSourceFactory(mediaSourceFactory)
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||||
.build()
|
.build()
|
||||||
.also { exoPlayer ->
|
.also { exoPlayer ->
|
||||||
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -19,6 +22,11 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".TrainingPlaybackService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|||||||
@@ -44,14 +44,19 @@ class MainActivity : Activity() {
|
|||||||
private var translationRequest: CancellableTranslationRequest? = null
|
private var translationRequest: CancellableTranslationRequest? = null
|
||||||
|
|
||||||
private var progressBarDragging = false
|
private var progressBarDragging = false
|
||||||
|
private var trainingPlaybackServiceActive = false
|
||||||
|
|
||||||
private val screenActionReceiver = object : BroadcastReceiver() {
|
private val screenActionReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent?.action == Intent.ACTION_SCREEN_OFF) {
|
if (intent?.action == Intent.ACTION_SCREEN_OFF) {
|
||||||
|
// Training playback intentionally keeps running with the screen off.
|
||||||
|
// The test module still pauses because recording/assessment state is UI-dependent.
|
||||||
|
if (screenState.activeModule == Module.TEST) {
|
||||||
testSession.onScreenOff()
|
testSession.onScreenOff()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -149,8 +154,9 @@ class MainActivity : Activity() {
|
|||||||
if (bindings.hasPlayerChrome) {
|
if (bindings.hasPlayerChrome) {
|
||||||
bindings.statusText.text = "已停止配音播放"
|
bindings.statusText.text = "已停止配音播放"
|
||||||
}
|
}
|
||||||
} else {
|
} else if (screenState.activeModule != Module.TRAIN) {
|
||||||
controller.pause()
|
controller.pause()
|
||||||
|
stopTrainingPlaybackService()
|
||||||
}
|
}
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -178,6 +184,7 @@ class MainActivity : Activity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
authCoordinator.reset()
|
authCoordinator.reset()
|
||||||
testSession.release()
|
testSession.release()
|
||||||
|
stopTrainingPlaybackService()
|
||||||
controller.release()
|
controller.release()
|
||||||
explanationOverlay.release()
|
explanationOverlay.release()
|
||||||
translationRequest?.cancel()
|
translationRequest?.cancel()
|
||||||
@@ -221,6 +228,7 @@ class MainActivity : Activity() {
|
|||||||
sdk.config.userToken = null
|
sdk.config.userToken = null
|
||||||
screenState.clearCourse()
|
screenState.clearCourse()
|
||||||
controller.pause()
|
controller.pause()
|
||||||
|
stopTrainingPlaybackService()
|
||||||
screenState.activeModule = Module.COURSE
|
screenState.activeModule = Module.COURSE
|
||||||
setRootView()
|
setRootView()
|
||||||
}
|
}
|
||||||
@@ -230,6 +238,10 @@ class MainActivity : Activity() {
|
|||||||
testSession.stopDubbingIfNeeded()
|
testSession.stopDubbingIfNeeded()
|
||||||
}
|
}
|
||||||
screenState.activeModule = module
|
screenState.activeModule = module
|
||||||
|
if (module != Module.TRAIN) {
|
||||||
|
controller.pause()
|
||||||
|
stopTrainingPlaybackService()
|
||||||
|
}
|
||||||
if (module == Module.COURSE || module == Module.MINE) {
|
if (module == Module.COURSE || module == Module.MINE) {
|
||||||
catalogController.loadUserData(module) {
|
catalogController.loadUserData(module) {
|
||||||
screenFactory.refreshMyContent(sdk.userApi, authSessionStore.session?.token.orEmpty())
|
screenFactory.refreshMyContent(sdk.userApi, authSessionStore.session?.token.orEmpty())
|
||||||
@@ -305,6 +317,11 @@ class MainActivity : Activity() {
|
|||||||
|
|
||||||
private fun applyPlaybackSnapshot(snapshot: PlaybackSnapshot) {
|
private fun applyPlaybackSnapshot(snapshot: PlaybackSnapshot) {
|
||||||
if (!bindings.hasPlayerChrome) return
|
if (!bindings.hasPlayerChrome) return
|
||||||
|
if (screenState.activeModule == Module.TRAIN && snapshot.isPlaying) {
|
||||||
|
startTrainingPlaybackService()
|
||||||
|
} else if (!snapshot.isPlaying) {
|
||||||
|
stopTrainingPlaybackService()
|
||||||
|
}
|
||||||
if (!progressBarDragging) {
|
if (!progressBarDragging) {
|
||||||
bindings.timeText.text =
|
bindings.timeText.text =
|
||||||
"${PlaybackFormat.time(snapshot.positionMs)} / ${PlaybackFormat.time(snapshot.durationMs)}"
|
"${PlaybackFormat.time(snapshot.positionMs)} / ${PlaybackFormat.time(snapshot.durationMs)}"
|
||||||
@@ -314,6 +331,18 @@ class MainActivity : Activity() {
|
|||||||
bindings.statusText.text = PlaybackFormat.status(snapshot)
|
bindings.statusText.text = PlaybackFormat.status(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startTrainingPlaybackService() {
|
||||||
|
if (trainingPlaybackServiceActive) return
|
||||||
|
TrainingPlaybackService.start(this)
|
||||||
|
trainingPlaybackServiceActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopTrainingPlaybackService() {
|
||||||
|
if (!trainingPlaybackServiceActive) return
|
||||||
|
TrainingPlaybackService.stop(this)
|
||||||
|
trainingPlaybackServiceActive = false
|
||||||
|
}
|
||||||
|
|
||||||
private fun applySentence(sentence: SentenceBoundary?) {
|
private fun applySentence(sentence: SentenceBoundary?) {
|
||||||
if (!bindings.hasPlayerChrome) return
|
if (!bindings.hasPlayerChrome) return
|
||||||
if (sentence == null) {
|
if (sentence == null) {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package cn.learningpad.oraltrainer.sample
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
/** Keeps the training player alive after the screen turns off or the app backgrounds. */
|
||||||
|
class TrainingPlaybackService : Service() {
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
manager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
getString(R.string.training_playback_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_LOW,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val notification = buildNotification()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(): Notification {
|
||||||
|
val contentIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
return Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle(getString(R.string.training_playback_notification_title))
|
||||||
|
.setContentText(getString(R.string.training_playback_notification_text))
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_play)
|
||||||
|
.setContentIntent(contentIntent)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANNEL_ID = "training_playback"
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
|
||||||
|
fun start(context: Context) {
|
||||||
|
val intent = Intent(context, TrainingPlaybackService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, TrainingPlaybackService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M8,5v14l11,-7z" />
|
||||||
|
</vector>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">八哥口语</string>
|
<string name="app_name">八哥口语</string>
|
||||||
|
<string name="training_playback_channel_name">训练播放</string>
|
||||||
|
<string name="training_playback_notification_title">口语训练</string>
|
||||||
|
<string name="training_playback_notification_text">正在后台播放</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MynaTalk</string>
|
<string name="app_name">MynaTalk</string>
|
||||||
|
<string name="training_playback_channel_name">Training playback</string>
|
||||||
|
<string name="training_playback_notification_title">Oral training</string>
|
||||||
|
<string name="training_playback_notification_text">Playing in the background</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user