Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
186799b
feat: use Storage Access Framework to save recordings
madadam Jan 20, 2026
ec9e37e
chore: fix some warnings and lints
madadam Jan 20, 2026
a8afe9c
refactor: change path to uri in Recording
madadam Jan 20, 2026
530f6e1
refactor: extract RecordingStore
madadam Jan 20, 2026
550d185
feat: implement MediaStore backend for RecordingStore + add tests
madadam Jan 22, 2026
3700e61
refactor: extract RecordingStore & co. into separate module
madadam Jan 26, 2026
3343563
chore: write tests for RecordingStore's SAF backend
madadam Jan 26, 2026
e508488
chore: write more tests for RecordingStore's SAF backend
madadam Jan 26, 2026
f880774
refactor: return Sequence instead of List
madadam Jan 26, 2026
1699e06
fix: handle moving recordings between all backends combinations
madadam Jan 27, 2026
418759b
refactor: minor code cleanup
madadam Jan 27, 2026
7f814e0
feat: implement RecordingStore migration
madadam Jan 27, 2026
e0655df
fix: some test failures on older android SDKs
madadam Jan 27, 2026
34cb350
fix: all test failures on SDK 29
madadam Jan 27, 2026
3f45132
fix: storage permissions on SDK 28
madadam Jan 27, 2026
bb2a80d
fix: creating recordings in MediaStore on SDK 28
madadam Jan 27, 2026
262724c
fix: storage permissions on SDK 27
madadam Jan 28, 2026
6dd9160
chore: add doc comments
madadam Jan 28, 2026
8bdf58e
refactor: rename Kind -> Backend
madadam Jan 28, 2026
57b5f4b
fix: incorrect metadata on MediaStore backend
madadam Jan 28, 2026
7414271
fix: storage permissions on SDK <= 28
madadam Jan 29, 2026
a26901a
fix: not recognizing OGG as audio on SDK 26
madadam Jan 29, 2026
4243949
refactor: Improve error handling
madadam Feb 2, 2026
efc1eb2
refactor: change RecordingWriter to inner class of RecordingStore
madadam Feb 2, 2026
b7265e1
feat: handle recording store errors gracefully
madadam Feb 3, 2026
bef8f2c
feat: handle AuthenticationRequiredException
madadam Feb 4, 2026
ba6a997
refactor: Code cleanup
madadam Feb 4, 2026
506cf81
fix: detekt issues
madadam Feb 9, 2026
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
9 changes: 7 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.FileInputStream
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.konan.properties.Properties
import java.io.FileInputStream

plugins {
alias(libs.plugins.android)
alias(libs.plugins.androidApplication)
alias(libs.plugins.ksp)
alias(libs.plugins.detekt)


}

val keystorePropertiesFile: File = rootProject.file("keystore.properties")
Expand Down Expand Up @@ -147,4 +150,6 @@ dependencies {
implementation(libs.tandroidlame)
implementation(libs.autofittextview)
detektPlugins(libs.compose.detekt)

implementation(project(":store"))
}
6 changes: 5 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="28" />

<uses-feature
android:name="android.hardware.faketouch"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.fossify.voicerecorder.activities

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.provider.MediaStore
Expand All @@ -22,16 +21,13 @@ import org.fossify.commons.helpers.LICENSE_AUDIO_RECORD_VIEW
import org.fossify.commons.helpers.LICENSE_AUTOFITTEXTVIEW
import org.fossify.commons.helpers.LICENSE_EVENT_BUS
import org.fossify.commons.helpers.PERMISSION_RECORD_AUDIO
import org.fossify.commons.helpers.PERMISSION_WRITE_STORAGE
import org.fossify.commons.helpers.isRPlus
import org.fossify.commons.models.FAQItem
import org.fossify.voicerecorder.BuildConfig
import org.fossify.voicerecorder.R
import org.fossify.voicerecorder.adapters.ViewPagerAdapter
import org.fossify.voicerecorder.databinding.ActivityMainBinding
import org.fossify.voicerecorder.extensions.config
import org.fossify.voicerecorder.extensions.deleteExpiredTrashedRecordings
import org.fossify.voicerecorder.extensions.ensureStoragePermission
import org.fossify.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE
import org.fossify.voicerecorder.models.Events
import org.fossify.voicerecorder.services.RecorderService
Expand All @@ -40,7 +36,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode

class MainActivity : SimpleActivity() {

private var bus: EventBus? = null

override var isSearchBarEnabled = true
Expand Down Expand Up @@ -80,7 +75,7 @@ class MainActivity : SimpleActivity() {
Intent(this@MainActivity, RecorderService::class.java).apply {
try {
startService(this)
} catch (ignored: Exception) {
} catch (_: Exception) {
}
}
}
Expand Down Expand Up @@ -110,7 +105,7 @@ class MainActivity : SimpleActivity() {
action = STOP_AMPLITUDE_UPDATE
try {
startService(this)
} catch (ignored: Exception) {
} catch (_: Exception) {
}
}
}
Expand All @@ -120,7 +115,7 @@ class MainActivity : SimpleActivity() {
binding.mainMenu.closeSearch()
true
} else if (isThirdPartyIntent()) {
setResult(Activity.RESULT_CANCELED, null)
setResult(RESULT_CANCELED, null)
false
} else {
false
Expand Down Expand Up @@ -150,41 +145,24 @@ class MainActivity : SimpleActivity() {
getPagerAdapter()?.searchTextChanged(text)
}

binding.mainMenu.requireToolbar().setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return@setOnMenuItemClickListener false
binding.mainMenu.requireToolbar()
.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
return@setOnMenuItemClickListener true
}
}

private fun updateMenuColors() {
binding.mainMenu.updateColors()
}

private fun tryInitVoiceRecorder() {
if (isRPlus()) {
ensureStoragePermission { granted ->
if (granted) {
setupViewPager()
} else {
toast(org.fossify.commons.R.string.no_storage_permissions)
finish()
}
}
} else {
handlePermission(PERMISSION_WRITE_STORAGE) {
if (it) {
setupViewPager()
} else {
toast(org.fossify.commons.R.string.no_storage_permissions)
finish()
}
}
}
setupViewPager()
}

private fun setupViewPager() {
Expand All @@ -201,18 +179,16 @@ class MainActivity : SimpleActivity() {

tabDrawables.forEachIndexed { i, drawableId ->
binding.mainTabsHolder.newTab()
.setCustomView(org.fossify.commons.R.layout.bottom_tablayout_item).apply {
customView
?.findViewById<ImageView>(org.fossify.commons.R.id.tab_item_icon)
.setCustomView(org.fossify.commons.R.layout.bottom_tablayout_item)
.apply {
customView?.findViewById<ImageView>(org.fossify.commons.R.id.tab_item_icon)
?.setImageDrawable(
AppCompatResources.getDrawable(
this@MainActivity,
drawableId
this@MainActivity, drawableId
)
)

customView
?.findViewById<TextView>(org.fossify.commons.R.id.tab_item_label)
customView?.findViewById<TextView>(org.fossify.commons.R.id.tab_item_label)
?.setText(tabLabels[i])

AutofitHelper.create(
Expand All @@ -223,18 +199,15 @@ class MainActivity : SimpleActivity() {
}
}

binding.mainTabsHolder.onTabSelectionChanged(
tabUnselectedAction = {
updateBottomTabItemColors(it.customView, false)
if (it.position == 1 || it.position == 2) {
binding.mainMenu.closeSearch()
}
},
tabSelectedAction = {
binding.viewPager.currentItem = it.position
updateBottomTabItemColors(it.customView, true)
binding.mainTabsHolder.onTabSelectionChanged(tabUnselectedAction = {
updateBottomTabItemColors(it.customView, false)
if (it.position == 1 || it.position == 2) {
binding.mainMenu.closeSearch()
}
)
}, tabSelectedAction = {
binding.viewPager.currentItem = it.position
updateBottomTabItemColors(it.customView, true)
})

binding.viewPager.adapter = ViewPagerAdapter(this, config.useRecycleBin)
binding.viewPager.offscreenPageLimit = 2
Expand All @@ -247,16 +220,19 @@ class MainActivity : SimpleActivity() {
binding.viewPager.currentItem = 0
} else {
binding.viewPager.currentItem = config.lastUsedViewPagerPage
binding.mainTabsHolder.getTabAt(config.lastUsedViewPagerPage)?.select()
binding.mainTabsHolder.getTabAt(config.lastUsedViewPagerPage)
?.select()
}
}

private fun setupTabColors() {
val activeView = binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.customView
val activeView =
binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.customView
updateBottomTabItemColors(activeView, true)
for (i in 0 until binding.mainTabsHolder.tabCount) {
if (i != binding.viewPager.currentItem) {
val inactiveView = binding.mainTabsHolder.getTabAt(i)?.customView
val inactiveView =
binding.mainTabsHolder.getTabAt(i)?.customView
updateBottomTabItemColors(inactiveView, false)
}
}
Expand All @@ -266,25 +242,22 @@ class MainActivity : SimpleActivity() {
binding.mainTabsHolder.setBackgroundColor(bottomBarColor)
}

private fun getPagerAdapter() = (binding.viewPager.adapter as? ViewPagerAdapter)
private fun getPagerAdapter() =
(binding.viewPager.adapter as? ViewPagerAdapter)

private fun launchSettings() {
hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}

private fun launchAbout() {
val licenses = LICENSE_EVENT_BUS or
LICENSE_AUDIO_RECORD_VIEW or
LICENSE_ANDROID_LAME or
LICENSE_AUTOFITTEXTVIEW
val licenses =
LICENSE_EVENT_BUS or LICENSE_AUDIO_RECORD_VIEW or LICENSE_ANDROID_LAME or LICENSE_AUTOFITTEXTVIEW

val faqItems = arrayListOf(
FAQItem(
title = R.string.faq_1_title,
text = R.string.faq_1_text
),
FAQItem(
title = R.string.faq_1_title, text = R.string.faq_1_text
), FAQItem(
title = org.fossify.commons.R.string.faq_9_title_commons,
text = org.fossify.commons.R.string.faq_9_text_commons
)
Expand Down Expand Up @@ -314,18 +287,25 @@ class MainActivity : SimpleActivity() {
)
}

private fun isThirdPartyIntent() = intent?.action == MediaStore.Audio.Media.RECORD_SOUND_ACTION
private fun isThirdPartyIntent() =
intent?.action == MediaStore.Audio.Media.RECORD_SOUND_ACTION

@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingSaved(event: Events.RecordingSaved) {
if (isThirdPartyIntent()) {
Intent().apply {
data = event.uri!!
data = event.uri
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
setResult(Activity.RESULT_OK, this)
setResult(RESULT_OK, this)
}
finish()
}
}

@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingFailed(event: Events.RecordingFailed) {
handleRecordingStoreError(event.exception)
}
}
Loading
Loading