Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ plugins {
}

import com.android.build.api.variant.FilterConfiguration
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
Expand All @@ -17,14 +18,14 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
namespace 'com.maary.liveinpeace'
compileSdk 35
compileSdk 36

defaultConfig {
applicationId "com.maary.liveinpeace"
minSdk 31
targetSdk 35
targetSdk 36
versionCode 5
versionName "2025.06.29"
versionName "2025.09.07"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -93,8 +94,13 @@ android {
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = '11'
// kotlinOptions {
// jvmTarget = '11'
// }
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}

androidComponents {
Expand Down Expand Up @@ -129,34 +135,34 @@ android {

dependencies {

implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'

implementation 'androidx.activity:activity-ktx:1.10.1'
implementation 'androidx.databinding:databinding-runtime:8.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.9.1'
implementation 'androidx.databinding:databinding-runtime:8.13.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.9.3'
implementation 'androidx.activity:activity-compose:1.10.1'
implementation platform('androidx.compose:compose-bom:2025.06.01')
implementation platform('androidx.compose:compose-bom:2025.08.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3:1.4.0-alpha16'
androidTestImplementation platform('androidx.compose:compose-bom:2025.06.01')
implementation 'androidx.compose.material3:material3:1.4.0-beta03'
androidTestImplementation platform('androidx.compose:compose-bom:2025.08.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'

// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.9.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.1"
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.3"
implementation "androidx.lifecycle:lifecycle-common-java8:2.9.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.3"
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3'

implementation "com.google.dagger:hilt-android:2.56.2"
ksp "com.google.dagger:hilt-compiler:2.56.2"
implementation "com.google.dagger:hilt-android:2.57.1"
ksp "com.google.dagger:hilt-compiler:2.57.1"


implementation "androidx.room:room-runtime:2.7.2"
Expand All @@ -169,9 +175,9 @@ dependencies {
implementation("com.google.accompanist:accompanist-permissions:0.37.3")
implementation "androidx.compose.material:material-icons-extended:1.7.8"

implementation "androidx.work:work-runtime-ktx:2.10.2"
implementation "androidx.work:work-runtime-ktx:2.10.3"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_ALERT
import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_DEFAULT
import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_PROTECT
import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_SETTINGS
import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_SLEEPTIMER
import com.maary.liveinpeace.Constants.Companion.CHANNEL_ID_WELCOME
import com.maary.liveinpeace.database.ConnectionRepository
import com.maary.liveinpeace.database.ConnectionRoomDatabase
Expand Down Expand Up @@ -62,6 +63,13 @@ class LiveInPeaceApplication: Application() {
resources.getString(R.string.welcome_channel),
resources.getString(R.string.welcome_channel_description)
)

createNotificationChannel(
NotificationManager.IMPORTANCE_MIN,
CHANNEL_ID_SLEEPTIMER,
resources.getString(R.string.sleeptimer_channel),
resources.getString(R.string.sleeptimer_channel_description)
)
}

private fun createNotificationChannel(importance:Int, id: String ,name:String, descriptionText: String) {
Expand All @@ -71,7 +79,7 @@ class LiveInPeaceApplication: Application() {
}
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
getSystemService(NOTIFICATION_SERVICE) as NotificationManager

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and to avoid casting, it's recommended to use the typed getSystemService(Class<T>) method. This removes the need for as NotificationManager. You can then also simplify the variable declaration on the previous line by removing the explicit type and letting it be inferred, combining the declaration and assignment.

Suggested change
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
getSystemService(NotificationManager::class.java)

notificationManager.createNotificationChannel(channel)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ package com.maary.liveinpeace.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioAttributes.CONTENT_TYPE_MUSIC
import android.media.AudioAttributes.USAGE_MEDIA
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.AUDIOFOCUS_GAIN
import android.util.Log
import com.maary.liveinpeace.Constants.Companion.BROADCAST_ACTION_MUTE
import com.maary.liveinpeace.Constants.Companion.BROADCAST_ACTION_SLEEPTIMER_CANCEL
import com.maary.liveinpeace.Constants.Companion.BROADCAST_ACTION_SLEEPTIMER_DECREMENT
Expand All @@ -17,18 +12,18 @@ import com.maary.liveinpeace.Constants.Companion.BROADCAST_ACTION_SLEEPTIMER_TOG
import com.maary.liveinpeace.Constants.Companion.BROADCAST_ACTION_SLEEPTIMER_UPDATE
import com.maary.liveinpeace.SleepNotification.handle
import com.maary.liveinpeace.SleepNotification.toggle
import com.maary.liveinpeace.service.ForegroundService

class MuteMediaReceiver: BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
if (p1?.action == BROADCAST_ACTION_MUTE){
val audioManager = p0?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
do {
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, 0)
} while (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) > 0)

val attributes = AudioAttributes.Builder().setUsage(USAGE_MEDIA).setContentType(CONTENT_TYPE_MUSIC).build()
val focusRequest = AudioFocusRequest.Builder(AUDIOFOCUS_GAIN).setAudioAttributes(attributes).setOnAudioFocusChangeListener {}.build()
audioManager.requestAudioFocus(focusRequest)
Log.d("MuteMediaReceiver", "BROADCAST_ACTION_MUTE received. Starting ForegroundService to handle it.")
p0?.let { context ->
val intent = Intent(context, ForegroundService::class.java).apply {
action = ForegroundService.ACTION_MUTE_MEDIA
}
context.startService(intent)
}
}

if (p1?.action == BROADCAST_ACTION_SLEEPTIMER_CANCEL ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
Expand Down Expand Up @@ -79,11 +81,22 @@ class ForegroundService : Service() {

// 为未知的设备类型 `28` 定义一个有意义的常量名
private const val TYPE_UNKNOWN_DEVICE_28 = 28

const val ACTION_MUTE_MEDIA = "com.maary.liveinpeace.service.ACTION_MUTE_MEDIA"
}

private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val audioManager: AudioManager by lazy {
getSystemService(Context.AUDIO_SERVICE) as AudioManager
getSystemService(AUDIO_SERVICE) as AudioManager
}

private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
// 如果长时间失去焦点,可以考虑做一些清理工作
Log.d(TAG, "Audio focus lost permanently.")
}
}
}

@Inject
Expand Down Expand Up @@ -150,6 +163,14 @@ class ForegroundService : Service() {

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand received.")

when (intent?.action) {
ACTION_MUTE_MEDIA -> {
Log.d(TAG, "Mute media action received.")
handleMuteMedia()
}
}

// 确保服务被重新创建时,通知内容是最新的
updateForegroundNotification()
return START_STICKY
Expand All @@ -167,7 +188,7 @@ class ForegroundService : Service() {

// 停止前台服务并移除通知
stopForeground(STOP_FOREGROUND_REMOVE)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(Constants.ID_NOTIFICATION_FOREGROUND)

Log.d(TAG, "Service destroyed.")
Expand Down Expand Up @@ -335,7 +356,7 @@ class ForegroundService : Service() {
duration = duration
)
if (duration > Constants.ALERT_TIME) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(Constants.ID_NOTIFICATION_ALERT)
}
}
Expand Down Expand Up @@ -375,7 +396,7 @@ class ForegroundService : Service() {
Log.d(CALLBACK_TAG, "Ear protection applied for $deviceName.")
showProtectionNotification()
}
} catch (e: CancellationException) {
} catch (_: CancellationException) {
Log.d(CALLBACK_TAG, "Protection job for $deviceName was cancelled.")
} finally {
protectionJobs.remove(deviceName)
Expand Down Expand Up @@ -438,6 +459,42 @@ class ForegroundService : Service() {
updateForegroundNotification()
}

private fun handleMuteMedia() {
val attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()

val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attributes)
.setOnAudioFocusChangeListener(afChangeListener) // 使用我们定义的监听器
.build()

Log.d(TAG, "Attempting to request audio focus from service...")
val result = audioManager.requestAudioFocus(focusRequest)

// 1. 仍然尝试请求焦点,并记录结果用于调试,但不再将静音操作绑定到成功的分支上。
when (result) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
Log.d(TAG, "Audio focus request GRANTED from service.")
}
else -> {
// 包含 FAILED 和 DELAYED 的情况
Log.e(TAG, "Audio focus request was not granted. Result code: $result")
}
}

// 2. Failsafe: 将降低音量的操作移到 when 语句外部,确保它总是被执行。
Log.d(TAG, "Executing failsafe volume reduction.")
serviceScope.launch {
// 使用 adjustStreamVolume 循环降低音量直到为0
while (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) > 0) {
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, 0)
}
Log.d(TAG, "Volume has been set to 0.")
}
}

private fun broadcastConnectionsUpdate() {
val intent = Intent(Constants.BROADCAST_ACTION_CONNECTIONS_UPDATE).apply {
putParcelableArrayListExtra(
Expand Down Expand Up @@ -550,12 +607,13 @@ class ForegroundService : Service() {
}

private fun createMutePendingIntent(context: Context): PendingIntent {
val intent = Intent(context, MuteMediaReceiver::class.java).apply {
action = Constants.BROADCAST_ACTION_MUTE
val intent = Intent(context, ForegroundService::class.java).apply {
action = ACTION_MUTE_MEDIA
}
return PendingIntent.getBroadcast(
// 注意:这里需要使用 getService,而不是 getBroadcast
return PendingIntent.getService(
context,
REQUEST_CODE_MUTE,
REQUEST_CODE_MUTE, // 可以复用这个请求码
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
Expand Down
12 changes: 6 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.9.3' apply false
id 'com.android.library' version '8.9.3' apply false
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
id 'com.google.devtools.ksp' version "2.0.21-1.0.26" apply false
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.21' apply false
id 'com.google.dagger.hilt.android' version '2.56.2' apply false
id 'com.android.application' version '8.11.1' apply false
id 'com.android.library' version '8.11.1' apply false
id 'org.jetbrains.kotlin.android' version '2.2.10' apply false
id 'com.google.devtools.ksp' version "2.2.10-2.0.2" apply false
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.10' apply false
id 'com.google.dagger.hilt.android' version '2.57.1' apply false
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri May 12 10:24:55 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading