Skip to content

Commit

Permalink
Merge pull request #9 from Angel-Studio/sleep-timer
Browse files Browse the repository at this point in the history
Sleep timer
  • Loading branch information
JulesPvx authored May 29, 2024
2 parents 06e6305 + 499b4eb commit 0776f01
Show file tree
Hide file tree
Showing 23 changed files with 10,001 additions and 53 deletions.
6 changes: 6 additions & 0 deletions .idea/copyright/Angel_Studio.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/copyright/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![Cover Image](/RESOURCES/Cover-1024x200.png)
![Cover Image](/RESOURCES/Cover-1024x200.png)

# SoundTap - Android Volume Media Controller App

Expand Down Expand Up @@ -37,6 +37,7 @@ enabling users to skip tracks, pause, and resume music playback with ease.
and more.
- [x] **Auto-Play**: Automatically start media playback when the volume buttons are long-pressed or
when a headset is connected.
- [x] **Sleep Timer**: Set a timer to stop playback after a certain period.
- [x] **Customizable Haptics**: Adjust the haptic feedback intensity to suit your preferences.
- [x] **Customizable Delays**: Adjust the long-press delay to fit your preferred interaction speed.
- [ ] **Localization**: Available in multiple languages for a global audience.
Expand Down Expand Up @@ -81,8 +82,8 @@ Open the project in Android Studio and build the APK file to install on your And
1. Launch the SoundTap app on your Android device.
2. Follow the on-screen instructions to grant necessary permissions.
3. Use the volume buttons on your device to control media playback:
- Long-press volume up/down to skip tracks.
- Long-press both volume buttons to pause/resume playback.
- Long-press volume up/down to skip tracks.
- Long-press both volume buttons to pause/resume playback.
4. Customize controls as desired in the app settings.

## :hammer_and_wrench: Tech Stack
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
applicationId = "fr.angel.soundtap"
minSdk = 30
targetSdk = 34
versionCode = 24
versionName = "1.0.7"
versionCode = 27
versionName = "1.0.8"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</intent>
</queries>

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
Expand Down Expand Up @@ -77,6 +78,8 @@
</intent-filter>
</service>

<service android:name=".service.SleepTimerService" />

<receiver
android:name=".service.HeadsetConnectionBroadcastReceiver"
android:enabled="true"
Expand Down
81 changes: 70 additions & 11 deletions app/src/main/java/fr/angel/soundtap/GlobalHelper.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
/*
* Copyright 2024 Angel Studio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* * Copyright (c) 2024 Angel Studio
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.angel.soundtap

import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.view.KeyEvent
import fr.angel.soundtap.service.SleepTimerService
import fr.angel.soundtap.service.media.MediaReceiver
import java.util.Locale

val supportedStartMediaPlayerPackages =
listOf(
Expand Down Expand Up @@ -80,4 +86,57 @@ object GlobalHelper {
}
context.sendOrderedBroadcast(startMediaPlayer, null)
}

fun stopMusic() {
val musicList = MediaReceiver.callbackMap.values
musicList.forEach { callback -> callback.stop() }
}

fun createStopSleepTimerIntent(context: Context): PendingIntent? {
val intent =
Intent(context, SleepTimerService::class.java).apply {
action = SleepTimerService.ACTION_STOP
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}

fun createAddTimeSleepTimerIntent(
context: Context,
time: Int,
): PendingIntent? {
val intent =
Intent(context, SleepTimerService::class.java).apply {
action = SleepTimerService.ACTION_ADD_TIME
putExtra(SleepTimerService.EXTRA_DURATION, time.toLong())
}
return PendingIntent.getService(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}

fun createNotificationOpenAppIntent(context: Context): PendingIntent? {
val intent = Intent(context, MainActivity::class.java)
return PendingIntent.getActivity(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}

fun formatTime(timeInMs: Long): String {
val seconds = timeInMs / 1000
val minutes = seconds / 60
val hours = minutes / 60

val formattedSeconds = seconds % 60
val formattedMinutes = minutes % 60

return if (hours > 0) {
String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, formattedMinutes, formattedSeconds)
} else {
String.format(Locale.getDefault(), "%02d:%02d", formattedMinutes, formattedSeconds)
}
}

fun openNotificationSettings(context: Context) {
val intent =
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
context.startActivity(intent)
}
}
65 changes: 64 additions & 1 deletion app/src/main/java/fr/angel/soundtap/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
Expand All @@ -37,14 +43,18 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -61,6 +71,7 @@ import fr.angel.soundtap.service.SoundTapAccessibilityService
import fr.angel.soundtap.ui.components.BottomControlBar
import fr.angel.soundtap.ui.theme.FontPilowlava
import fr.angel.soundtap.ui.theme.SoundTapTheme
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
Expand All @@ -75,6 +86,8 @@ class MainActivity : ComponentActivity() {
.setKeepOnScreenCondition { shouldKeepSplashScreenOn.value }

setContent {
val scope = rememberCoroutineScope()

mainViewModel = hiltViewModel<MainViewModel>()
val uiState by mainViewModel.uiState.collectAsStateWithLifecycle()

Expand All @@ -98,7 +111,24 @@ class MainActivity : ComponentActivity() {
mainViewModel.updatePermissionStates(this@MainActivity)
}

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())

val bottomSheetState =
rememberModalBottomSheetState(
skipPartiallyExpanded =
when (uiState.bottomSheetState) {
else -> false // Prevents the sheet from being partially expanded
},
confirmValueChange = {
when (uiState.bottomSheetState) {
/**
* Prevents the sheet from being expanded
*
is BottomSheetManager.Companion.BottomSheetType.CreateList -> { it != SheetValue.Expanded } // Prevents the sheet from being expanded*/
else -> true // Allows the sheet to be expanded
}
},
).also { mainViewModel.setBottomSheetState(it) }

SoundTapTheme {
Scaffold(
Expand Down Expand Up @@ -135,6 +165,7 @@ class MainActivity : ComponentActivity() {
fontWeight = FontWeight.ExtraBold,
)
},
scrollBehavior = scrollBehavior,
)
},
bottomBar = {
Expand All @@ -151,8 +182,40 @@ class MainActivity : ComponentActivity() {
) { innerPadding ->
SoundTapNavGraph(
modifier = Modifier.padding(innerPadding),
innerPadding = innerPadding,
navController = navController,
)

if (uiState.bottomSheetVisible) {
val sheetState = uiState.bottomSheetState
ModalBottomSheet(
onDismissRequest = {
sheetState.onDismiss?.invoke()
scope.launch { mainViewModel.hideBottomSheet() }
},
sheetState = bottomSheetState,
windowInsets = WindowInsets(0),
) {
Column(
modifier =
Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
sheetState.displayName?.run {
Text(
text = this,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
)
}
Spacer(modifier = Modifier.height(24.dp))
sheetState.content(sheetState)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
}
}
}
Expand Down
32 changes: 31 additions & 1 deletion app/src/main/java/fr/angel/soundtap/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ package fr.angel.soundtap
import android.content.Context
import android.content.pm.ResolveInfo
import android.os.PowerManager
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import fr.angel.soundtap.data.enums.AutoPlayMode
import fr.angel.soundtap.data.enums.HapticFeedbackLevel
import fr.angel.soundtap.data.enums.WorkingMode
import fr.angel.soundtap.data.models.BottomSheetState
import fr.angel.soundtap.data.settings.customization.CustomizationSettings
import fr.angel.soundtap.data.settings.settings.AppSettings
import fr.angel.soundtap.data.settings.stats.StatsSettings
import fr.angel.soundtap.navigation.Screens
import fr.angel.soundtap.service.SleepTimerService
import fr.angel.soundtap.service.SoundTapAccessibilityService
import javax.inject.Inject
import kotlinx.coroutines.delay
Expand All @@ -41,8 +46,9 @@ data class MainUiState(
val finishedInitializations: Boolean = false,
val hasNotificationListenerPermission: Boolean = false,
val isBackgroundOptimizationDisabled: Boolean = false,
val isOverlayPermissionGranted: Boolean = false,
val playersPackages: Set<ResolveInfo> = emptySet(),
val bottomSheetState: BottomSheetState = BottomSheetState.None,
val bottomSheetVisible: Boolean = false,
val customizationSettings: CustomizationSettings = CustomizationSettings(),
val appSettings: AppSettings = AppSettings(),
val statsSettings: StatsSettings = StatsSettings(),
Expand All @@ -55,6 +61,7 @@ data class MainUiState(
class MainViewModel
@Inject
constructor(
@ApplicationContext private val context: Context,
private val customizationSettingsDataStore: DataStore<CustomizationSettings>,
private val appSettingsDataStore: DataStore<AppSettings>,
private val statsSettingsDataStore: DataStore<StatsSettings>,
Expand All @@ -63,6 +70,9 @@ class MainViewModel
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()

@OptIn(ExperimentalMaterial3Api::class)
private lateinit var sheetState: SheetState

init {
// Load the customization settings
viewModelScope.launch {
Expand Down Expand Up @@ -178,4 +188,24 @@ class MainViewModel
customizationSettingsDataStore.updateData { settings -> settings.copy(autoPlayMode = autoPlayMode) }
}
}

fun showBottomSheet(bottomSheetState: BottomSheetState) {
_uiState.value = _uiState.value.copy(bottomSheetState = bottomSheetState, bottomSheetVisible = true)
}

@OptIn(ExperimentalMaterial3Api::class)
suspend fun hideBottomSheet() {
sheetState.hide()
_uiState.value = _uiState.value.copy(bottomSheetVisible = false, bottomSheetState = BottomSheetState.None)
}

fun setSleepTimer(duration: Long) {
// Start the sleep timer service
SleepTimerService.startService(context, duration)
}

@OptIn(ExperimentalMaterial3Api::class)
fun setBottomSheetState(sheetState: SheetState) {
this.sheetState = sheetState
}
}
Loading

0 comments on commit 0776f01

Please sign in to comment.