diff --git a/app/build.gradle b/app/build.gradle index f6162c2..d89358e 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -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() @@ -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" } @@ -93,8 +94,13 @@ android { targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = '11' +// kotlinOptions { +// jvmTarget = '11' +// } + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } } androidComponents { @@ -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" @@ -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' } \ No newline at end of file diff --git a/app/src/main/java/com/maary/liveinpeace/LiveInPeaceApplication.kt b/app/src/main/java/com/maary/liveinpeace/LiveInPeaceApplication.kt index 157bd1d..29493d9 100644 --- a/app/src/main/java/com/maary/liveinpeace/LiveInPeaceApplication.kt +++ b/app/src/main/java/com/maary/liveinpeace/LiveInPeaceApplication.kt @@ -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 @@ -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) { @@ -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 notificationManager.createNotificationChannel(channel) } } \ No newline at end of file diff --git a/app/src/main/java/com/maary/liveinpeace/receiver/MuteMediaReceiver.kt b/app/src/main/java/com/maary/liveinpeace/receiver/MuteMediaReceiver.kt index c13842f..5bdf0bb 100644 --- a/app/src/main/java/com/maary/liveinpeace/receiver/MuteMediaReceiver.kt +++ b/app/src/main/java/com/maary/liveinpeace/receiver/MuteMediaReceiver.kt @@ -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 @@ -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 || diff --git a/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt b/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt index 78fdb54..e9a7079 100644 --- a/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt +++ b/app/src/main/java/com/maary/liveinpeace/service/ForegroundService.kt @@ -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 @@ -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 @@ -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 @@ -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.") @@ -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) } } @@ -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) @@ -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( @@ -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 ) diff --git a/build.gradle b/build.gradle index 23e565f..2c95c4c 100755 --- a/build.gradle +++ b/build.gradle @@ -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 } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eaaae2b..a27fe3f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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