Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boot reminder #46

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
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
19 changes: 13 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {

android {
namespace 'app.myzel394.alibi'
compileSdk 34
compileSdk 33

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
Expand Down Expand Up @@ -34,14 +34,21 @@ android {
multiDexEnabled true
applicationId "app.myzel394.alibi"
minSdk 24
targetSdk 34
targetSdk 33
versionCode 7
versionName "0.3.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}

configurations.all {
resolutionStrategy {
force("androidx.emoji2:emoji2-views-helper:1.3.0")
force("androidx.emoji2:emoji2:1.3.0")
}
}
}

signingConfigs {
Expand Down Expand Up @@ -91,16 +98,16 @@ android {
}

dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-ktx:1.10.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.activity:activity-compose:1.0.0'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.material:material-icons-extended:1.5.1"
implementation "androidx.compose.material:material-icons-extended:1.5.4"
implementation 'androidx.appcompat:appcompat:1.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
Expand All @@ -110,7 +117,7 @@ dependencies {
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'

implementation "androidx.navigation:navigation-compose:2.7.2"
implementation "androidx.navigation:navigation-compose:2.5.0"

implementation 'com.google.dagger:hilt-android:2.46.1'
annotationProcessor 'com.google.dagger:hilt-compiler:2.46.1'
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:name=".UpdateSettingsApp"
android:allowBackup="true"
Expand Down Expand Up @@ -45,6 +47,7 @@
</receiver>
<service
android:name=".services.AudioRecorderService"
android:exported="false"
android:foregroundServiceType="microphone" />

<!-- Change locale for Android <= 12 -->
Expand All @@ -56,6 +59,14 @@
android:name="autoStoreLocales"
android:value="true" />
</service>

<receiver
android:name=".receivers.BootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>

</manifest>
28 changes: 20 additions & 8 deletions app/src/main/java/app/myzel394/alibi/NotificationHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,30 @@ import androidx.annotation.RequiresApi
object NotificationHelper {
const val RECORDER_CHANNEL_ID = "recorder"
const val RECORDER_CHANNEL_NOTIFICATION_ID = 1
const val BOOT_CHANNEL_ID = "boot"
const val BOOT_CHANNEL_NOTIFICATION_ID = 2

@RequiresApi(Build.VERSION_CODES.O)
fun createChannels(context: Context) {
val channel = NotificationChannel(
RECORDER_CHANNEL_ID,
context.resources.getString(R.string.notificationChannels_recorder_name),
android.app.NotificationManager.IMPORTANCE_LOW,
)
channel.description = context.resources.getString(R.string.notificationChannels_recorder_description)

val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
notificationManager.createNotificationChannel(
NotificationChannel(
RECORDER_CHANNEL_ID,
context.getString(R.string.notificationChannels_recorder_name),
NotificationManager.IMPORTANCE_LOW,
).apply {
description = context.getString(R.string.notificationChannels_recorder_description)
}
)
notificationManager.createNotificationChannel(
NotificationChannel(
BOOT_CHANNEL_ID,
context.getString(R.string.notificationChannels_boot_name),
NotificationManager.IMPORTANCE_LOW,
).apply {
description = context.getString(R.string.notificationChannels_boot_description)
}
)
}

}
16 changes: 16 additions & 0 deletions app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class AppSettings(
val showAdvancedSettings: Boolean = false,
val theme: Theme = Theme.SYSTEM,
val lastRecording: RecordingInformation? = null,
val bootBehavior: BootBehavior? = BootBehavior.CONTINUE_RECORDING,
) {
fun setShowAdvancedSettings(showAdvancedSettings: Boolean): AppSettings {
return copy(showAdvancedSettings = showAdvancedSettings)
Expand All @@ -44,12 +45,27 @@ data class AppSettings(
return copy(lastRecording = lastRecording)
}

fun setBootBehavior(bootBehavior: BootBehavior?): AppSettings {
return copy(bootBehavior = bootBehavior)
}

enum class Theme {
SYSTEM,
LIGHT,
DARK,
}

enum class BootBehavior {
// Always start recording, no matter if it was interrupted or not
START_RECORDING,

// Only start recording if it was interrupted
CONTINUE_RECORDING,

// Show a notification if interrupted
SHOW_NOTIFICATION,
}

fun exportToString(): String {
return Json.encodeToString(serializer(), this)
}
Expand Down
128 changes: 128 additions & 0 deletions app/src/main/java/app/myzel394/alibi/receivers/BootReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package app.myzel394.alibi.receivers

import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import app.myzel394.alibi.MainActivity
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService
import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json

class BootReceiver : BroadcastReceiver() {
private var job = SupervisorJob()
private var scope = CoroutineScope(Dispatchers.IO + job)

private fun startRecording(context: Context, settings: AppSettings) {
val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder ->
recorder.startRecording()
}
}

override fun onServiceDisconnected(arg0: ComponentName) {
}
}

println("BootReceiver.startRecording()")
val intent = Intent(context, AudioRecorderService::class.java).apply {
action = "init"

putExtra(
"startImmediately",
true,
)

if (settings.notificationSettings != null) {
putExtra(
"notificationDetails",
Json.encodeToString(
RecorderNotificationHelper.NotificationDetails.serializer(),
RecorderNotificationHelper.NotificationDetails.fromNotificationSettings(
context,
settings.notificationSettings
)
),
)
}
}
ContextCompat.startForegroundService(context, intent)
}

private fun showNotification(context: Context) {
if (!AudioRecorderExporter.hasRecordingsAvailable(context)) {
// Nothing interrupted, so no notification needs to be shown
return
}

val notification = NotificationCompat.Builder(context, NotificationHelper.BOOT_CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setSmallIcon(R.drawable.launcher_monochrome_noopacity)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
)
.setOnlyAlertOnce(true)
.setContentTitle(context.getString(R.string.notification_boot_title))
.setContentText(context.getString(R.string.notification_boot_message))
.build()

val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

notificationManager.notify(NotificationHelper.BOOT_CHANNEL_NOTIFICATION_ID, notification)
}

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null) {
return
}

println("BootReceiver.onReceive()")

scope.launch {
context.dataStore.data.collectLatest { settings ->
println("BootBehavior: ${settings.bootBehavior}")
when (settings.bootBehavior) {
AppSettings.BootBehavior.CONTINUE_RECORDING -> {
if (AudioRecorderExporter.hasRecordingsAvailable(context)) {
startRecording(context, settings)
}
}

AppSettings.BootBehavior.START_RECORDING -> startRecording(context, settings)
AppSettings.BootBehavior.SHOW_NOTIFICATION -> showNotification(context)
null -> {
// Nothing to do
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.myzel394.alibi.services

import android.media.MediaMetadataRetriever
import android.media.MediaRecorder
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings
Expand Down Expand Up @@ -58,10 +59,45 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
}
}

private fun fetchCounterValue() {
val files = outputFolder.listFiles()?.filter {
val name = it.nameWithoutExtension

name.toIntOrNull() != null
}?.toList() ?: emptyList()

counter = files.size
}

private fun fetchRecordingTime() {
var oldAmount = 0L

for (file in outputFolder.listFiles() ?: emptyArray()) {
if (file.nameWithoutExtension.toIntOrNull() == null) {
continue
}

// It's better to at least get an approximate value, than to crash
runCatching {
val amount = MediaMetadataRetriever().run {
setDataSource(file.absolutePath)
extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toIntOrNull()
?: 0
}

oldAmount += amount
}
}

recordingTime = oldAmount
}

override fun start() {
super.start()

outputFolder.mkdirs()
fetchCounterValue()
fetchRecordingTime()

scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
Expand Down
Loading