Thanks to visit codestin.com
Credit goes to github.com

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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.kotlin/
.DS_Store
/build
/captures
Expand Down
2 changes: 1 addition & 1 deletion .idea/compiler.xml

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

4 changes: 2 additions & 2 deletions .idea/deploymentTargetSelector.xml

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

1 change: 1 addition & 0 deletions .idea/gradle.xml

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

3 changes: 2 additions & 1 deletion .idea/misc.xml

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

13 changes: 13 additions & 0 deletions .idea/runConfigurations.xml

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

10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ What are the features this app can provide, here's some:
your device.
- :bellhop_bell: **Convenient Notifications**: Control your recordings directly from
notifications,without having to return to the app.
- :file_cabinet: **File Management**: Organize, delete, share, or rename your recordings with ease.
- :file_cabinet: **File Management**: Organize, delete, share, or rename your own recordings with
ease, on api level `api-31`+ you can also read other apps recordings.
- :package: **Category Management**: Categories your recording into different category, so that you
can easily find the required one.
- :record_button: **Built-in Player**: Listen to your recordings directly within the app, complete
Expand Down Expand Up @@ -61,10 +62,13 @@ phone safe and sound.Here are the list of permission required in this app
- :musical_note: **Music and Audio Access** : Use to save and read the recordings
- :bell: **Notifications** : Yes you can control the recorder from the notification

There are some other permissions (optional ones), but they aren't necessary to the core audio
recording and playing stuff.
There are some optional permissions, but they aren't necessary to the core audio recording and
playing stuff.

- :telephone_receiver: **Phone State** : To handle incomming calls during a recording.
- :world_map: **Location** : Some mediacodec like `acc` and `three_gpp` can add a additional
location data with the recording.You can view this location data on other devices which can read
metadata.

## :hammer_and_wrench: Getting Started

Expand Down
8 changes: 6 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ android {
applicationId = "com.eva.recorderapp"
minSdk = 29
targetSdk = 35
versionCode = 3
versionName = "1.1.1"
versionCode = 4
versionName = "1.1.2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand All @@ -32,6 +32,7 @@ android {
applicationIdSuffix = ".release"
isShrinkResources = true
multiDexEnabled = true
signingConfig = signingConfigs.getByName("debug")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand All @@ -47,6 +48,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
packaging {
resources {
Expand Down Expand Up @@ -92,6 +94,8 @@ dependencies {
implementation(libs.kotlinx.datetime)
//kotlinx-immutable
implementation(libs.kotlinx.collections.immutable)
//location
implementation(libs.gms.play.services.location)
//hilt
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

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

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/eva/recorderapp/RecorderApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.eva.recorderapp.common.NotificationConstants
import com.eva.recorderapp.voice_recorder.data.worker.RemoveTrashRecordingWorker
import com.eva.recorderapp.voice_recorder.data.worker.UpdateRecordingPathWorker
import com.eva.recorderapp.voice_recorder.domain.util.AppShortcutFacade
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
Expand Down Expand Up @@ -71,6 +72,7 @@ class RecorderApp : Application(), Configuration.Provider {

//start workers
RemoveTrashRecordingWorker.startRepeatWorker(applicationContext)

// update path worker
UpdateRecordingPathWorker.startWorker(applicationContext)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ object LocalTimeFormats {
char(':')
minute()
char(' ')
amPmMarker("am", "pm")
}

val LOCALTIME_HH_MM_SS_FORMAT = LocalTime.Format {
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/eva/recorderapp/common/Resource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ sealed class Resource<out S, out E> {

data class Success<out S, out E>(
val data: S,
val message: String? = null
val message: String? = null,
) : Resource<S, E>()

data class Error<out S, out E : Exception>(
val error: E,
val message: String? = null
val message: String? = null,
val data: S? = null,
) : Resource<S, E>()

data object Loading : Resource<Nothing, Nothing>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ fun RecorderSettingsProto.toDomain(): RecorderAudioSettings = RecorderAudioSetti
encoders = encoder.toDomain,
quality = quality.toDomain,
pauseRecordingOnCall = pauseDuringCalls,
enableStero = isStereoMode,
enableStereo = isStereoMode,
skipSilences = skipSilences,
useBluetoothMic = useBluetoothMic
useBluetoothMic = useBluetoothMic,
addLocationInfoInRecording = allowLocationInfoIfAvailable,
)

fun FileSettingsProto.toDomain(): RecorderFileSettings = RecorderFileSettings(
name = prefix,
format = format.toDomain
format = format.toDomain,
allowExternalRead = allowExternalRead
)

val RecorderQualityProto.toDomain: RecordQuality
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ import kotlinx.coroutines.runBlocking
import java.io.InputStream
import java.io.OutputStream

class RecorderAudioSettingsRepoImpl(
private val context: Context
) : RecorderAudioSettingsRepo {
class RecorderAudioSettingsRepoImpl(private val context: Context) : RecorderAudioSettingsRepo {

override val audioSettingsFlow: Flow<RecorderAudioSettings>
get() = context.recorderSettings.data.map(RecorderSettingsProto::toDomain)
Expand Down Expand Up @@ -74,6 +72,14 @@ class RecorderAudioSettingsRepoImpl(
.build()
}
}

override suspend fun onAddLocationEnabled(isEnabled: Boolean) {
context.recorderSettings.updateData { settings ->
settings.toBuilder()
.setAllowLocationInfoIfAvailable(isEnabled)
.build()
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import java.io.InputStream
import java.io.OutputStream

class RecorderFileSettingsRepoImpl(
private val context: Context
private val context: Context,
) : RecorderFileSettingsRepo {

override val fileSettingsFlow: Flow<RecorderFileSettings>
Expand All @@ -41,6 +41,14 @@ class RecorderFileSettingsRepoImpl(
.build()
}
}

override suspend fun onAllowExternalFileRead(isAllowed: Boolean) {
context.recorderFileSettings.updateData { settings ->
settings.toBuilder()
.setAllowExternalRead(isAllowed)
.build()
}
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.eva.recorderapp.voice_recorder.data.location

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.location.LocationManager
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.core.content.getSystemService
import com.eva.recorderapp.common.Resource
import com.eva.recorderapp.voice_recorder.domain.location.BaseLocationModel
import com.eva.recorderapp.voice_recorder.domain.location.LocationProvider
import com.eva.recorderapp.voice_recorder.domain.location.exceptions.CannotFoundLastLocationException
import com.eva.recorderapp.voice_recorder.domain.location.exceptions.CurrentLocationTimeoutException
import com.eva.recorderapp.voice_recorder.domain.location.exceptions.LocationNotEnabledException
import com.eva.recorderapp.voice_recorder.domain.location.exceptions.LocationPermissionNotFoundException
import com.eva.recorderapp.voice_recorder.domain.location.exceptions.LocationProviderNotFoundException
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.Granularity
import com.google.android.gms.location.LastLocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

class CoarseLocationProviderImpl(
private val context: Context,
) : LocationProvider {

private val locationManager by lazy { context.getSystemService<LocationManager>() }

private val locationProvider: FusedLocationProviderClient
get() = LocationServices.getFusedLocationProviderClient(context.applicationContext)

private val _hasLocationPermission: Boolean
get() = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PermissionChecker.PERMISSION_GRANTED

private val lastLocationRequest: LastLocationRequest
get() = LastLocationRequest.Builder()
.setGranularity(Granularity.GRANULARITY_COARSE)
.setMaxUpdateAgeMillis(10_000)
.build()

private val currentLocationRequest: CurrentLocationRequest
get() = CurrentLocationRequest.Builder()
.setGranularity(Granularity.GRANULARITY_COARSE)
.setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)
.setDurationMillis(1_000)
.build()

private val isGpsEnabled: Boolean
get() = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false

private val isLocationEnabled: Boolean
get() = locationManager?.isLocationEnabled ?: false

private val isNetworkProviderEnabled: Boolean
get() = locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ?: false

@SuppressLint("MissingPermission")
private suspend fun getCurrentLocation(): Resource<BaseLocationModel, Exception> {
return suspendCancellableCoroutine { cont ->
locationProvider.getCurrentLocation(currentLocationRequest, null).apply {
addOnCompleteListener {
addOnSuccessListener { location ->
location?.let {
val evaluatedLocation = BaseLocationModel(
latitude = location.latitude,
longitude = location.longitude
)
cont.resume(value = Resource.Success(evaluatedLocation))
return@addOnSuccessListener
}
cont.resume(
value = Resource.Error(CurrentLocationTimeoutException()),
)
}
addOnFailureListener { exp ->
cont.resume(value = Resource.Error(exp))
}
}
addOnCanceledListener {
cont.cancel()
}
}
}
}

@SuppressLint("MissingPermission")
private suspend fun getLastKnownLocation(): Resource<BaseLocationModel, Exception> {
return suspendCancellableCoroutine { cont ->
locationProvider.getLastLocation(lastLocationRequest).apply {
addOnCompleteListener {
addOnSuccessListener { location ->
location?.let {
val evaluatedLocation = BaseLocationModel(
latitude = location.latitude,
longitude = location.longitude
)
cont.resume(value = Resource.Success(evaluatedLocation))
return@addOnSuccessListener
}
cont.resume(value = Resource.Error(CannotFoundLastLocationException()))
}
addOnFailureListener { exp ->
cont.resume(value = Resource.Error(exp))
}
}
addOnCanceledListener {
cont.cancel()
}
}
}
}

override suspend fun invoke(): Resource<BaseLocationModel, Exception> {
if (!_hasLocationPermission) {
return Resource.Error(LocationPermissionNotFoundException())
}
if (!isLocationEnabled) {
return Resource.Error(LocationNotEnabledException())
}
if (!isNetworkProviderEnabled || !isGpsEnabled) {
return Resource.Error(LocationProviderNotFoundException())
}
// has permission so gets the last known location
return when (val lastLocation = getLastKnownLocation()) {
is Resource.Error -> {
// if its cannot found last location then try to get the current location
if (lastLocation.error is CannotFoundLastLocationException)
getCurrentLocation()
else lastLocation
}

else -> lastLocation
}

}
}
Loading