Compare commits

...

80 Commits

Author SHA1 Message Date
Tibor Kaputa db0d3e30b7
Update FUNDING.yml 2024-06-11 11:55:33 +02:00
Tibor Kaputa 6dc37ab1fd
Update README.md 2024-06-11 11:47:02 +02:00
Tibor Kaputa c2e3910cf4
Merge pull request #232 from weblate/weblate-simple-mobile-tools-simple-voice-recorder
Translations update from Hosted Weblate
2023-10-16 11:40:25 +02:00
Tibor Kaputa c143fbff1e
Update strings.xml 2023-10-16 11:40:12 +02:00
Guillaume 09e4195cd9
Translated using Weblate (Dutch)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/nl/
2023-10-16 09:36:16 +00:00
Lionel HANNEQUIN 6e1c8cfbd3
Translated using Weblate (French)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/fr/
2023-10-16 09:36:15 +00:00
Puppelimies b8ea987465
Translated using Weblate (Swedish)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/sv/
2023-10-16 09:36:14 +00:00
Tibor Kaputa e7931bff94
Merge pull request #240 from esensar/fix/clean-up-audio-recorder
Revert audio editor code
2023-10-16 11:36:07 +02:00
Ensar Sarajčić 60eda663a3 Revert audio editor code 2023-10-16 11:08:03 +02:00
tibbi 4b1c045676 updating changelog 2023-10-09 11:18:28 +02:00
tibbi 9a7a12755a update version to 5.12.3 2023-10-09 11:18:22 +02:00
tibbi 077373e6cf more commenting out 2023-10-09 11:14:14 +02:00
tibbi 8e418971a8 commenting out some audio editor stuff, it is way too huge 2023-10-09 11:14:05 +02:00
tibbi 387b9e6b00 commenting out the voice editor in progress
not sure how it got here...
2023-10-09 11:04:19 +02:00
tibbi 6ec445ee64 fixing a bug with the inability to change save folder 2023-10-09 10:59:12 +02:00
tibbi 65f22bae88 Merge branch 'master' of github.com:SimpleMobileTools/Simple-Voice-Recorder 2023-10-05 09:21:01 +02:00
Tibor Kaputa 8499afe01b
updating the f-droid icon 2023-10-05 09:19:06 +02:00
tibbi 6c9221f238 Merge branch 'feature/172-recording-editor' of github.com:esensar/Simple-Voice-Recorder 2023-09-30 20:48:36 +02:00
Ensar Sarajčić 9a8c2f9210 Prevent crashes on Android 13 2023-09-29 11:29:07 +02:00
Tibor Kaputa ffe5e8a72a
Merge pull request #225 from weblate/weblate-simple-mobile-tools-simple-voice-recorder
Translations update from Hosted Weblate
2023-09-28 23:22:00 +02:00
Lionel HANNEQUIN 00d5ab7be8
Translated using Weblate (French)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/fr/
2023-09-28 05:56:58 +02:00
Ensar Sarajčić cd62cabb52 WIP: Audio editor 2023-09-27 16:26:05 +02:00
gallegonovato 9b83060801
Translated using Weblate (Spanish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder metadata
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder-metadata/es/
2023-09-27 13:02:45 +00:00
Puppelimies 6135b529a1
Translated using Weblate (Swedish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder metadata
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder-metadata/sv/
2023-09-25 05:01:59 +00:00
Tibor Kaputa 15d23dbca1
Merge pull request #224 from weblate/weblate-simple-mobile-tools-simple-voice-recorder
Translations update from Hosted Weblate
2023-09-23 19:20:14 +02:00
gallegonovato 6f874233ce
Translated using Weblate (Spanish)
Currently translated at 100.0% (14 of 14 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder metadata
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder-metadata/es/
2023-09-22 09:23:47 +02:00
gallegonovato dfd847461f
Translated using Weblate (Spanish)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/es/
2023-09-21 11:22:20 +02:00
tibbi 2311c17a03 updating changelog 2023-09-18 19:02:43 +02:00
tibbi ebe55079fb update version to 5.12.2 2023-09-18 19:02:37 +02:00
tibbi 430ddd5994 updating commons 2023-09-18 18:56:25 +02:00
Tibor Kaputa a2461d0e89
Merge pull request #223 from esensar/fix/221-info-button-crash
Keep androidx compose runtime classes to prevent crash on about button
2023-09-18 18:49:22 +02:00
Ensar Sarajčić 5942d1f6ce Keep androidx compose runtime classes to prevent crash on about button
This closes #221
2023-09-18 16:54:44 +02:00
Tibor Kaputa 12d1bb3c3f
Merge pull request #222 from esensar/gradle-deprecation-warnings
Clean up gradle deprecation warnings
2023-09-13 15:22:43 +02:00
Ensar Sarajčić 57ee97e060 Clean up gradle deprecation warnings 2023-09-13 15:17:32 +02:00
tibbi 2cea945f43 updating changelog 2023-09-13 10:00:11 +02:00
tibbi 600630e193 update version to 5.12.1 2023-09-13 10:00:05 +02:00
Tibor Kaputa 673110e489
Merge pull request #220 from esensar/fix/tab-color
Fix inactive tab onResume update when there are 3 tabs
2023-09-13 09:46:20 +02:00
Ensar Sarajčić fca700375a Fix inactive tab onResume update when there are 3 tabs 2023-09-12 12:24:33 +02:00
Tibor Kaputa 152ef60d24
Merge pull request #218 from Usland123/patch-1
Update strings.xml
2023-09-12 09:51:13 +02:00
tibbi 616138df8c updating commons and gradle 2023-09-11 23:00:04 +02:00
Usland 16ff51f00a
Update strings.xml 2023-09-02 20:10:53 +03:00
tibbi c96a4e1c0d correcting the latest version number 2023-08-15 00:15:22 +02:00
Tibor Kaputa a53d6985d7
Merge pull request #209 from esensar/viewbinding-migration
Migrate to viewbinding and kotlin gradle scripts
2023-08-04 09:51:00 +02:00
Tibor Kaputa bb3824bef4
fixing a typo 2023-08-04 09:50:48 +02:00
Ensar Sarajčić f0737fa83c Remove alised import for commons R class 2023-08-04 08:43:55 +02:00
Ensar Sarajčić 109f8aafb7 Update gradle to 8.2.1 2023-08-03 17:09:59 +02:00
Ensar Sarajčić 89e76cc293 Migrate to viewbinding and kotlin gradle scripts
- Replaced kotlinx sythetic with viewbinding
- Update build scripts to kotlin
- Intoduced version catalog, similar to Commons
- Updated kotlin from 1.7.10 to 1.9.0
- Updated Android Gradle Plugin to 8.1.0
2023-08-03 16:58:36 +02:00
tibbi a212b51e64 updating changelog 2023-07-31 23:29:17 +02:00
tibbi eeb957c243 update version to 5.12.0 2023-07-31 23:29:11 +02:00
tibbi 7c161e13d4 updating commons 2023-07-31 23:24:02 +02:00
tibbi 7eeaa14fbb Merge branch 'master' of github.com:SimpleMobileTools/Simple-Voice-Recorder 2023-07-31 18:13:44 +02:00
Tibor Kaputa 904c40d27d
Merge pull request #206 from esensar/sdk-34-migration
Update targetSdkVersion to 34
2023-07-31 18:12:46 +02:00
Ensar Sarajčić eec47921e0 Update targetSdkVersion to 34
Regardless of updating targetSdkVersion, the foreground service notification
will now be dismissable by the user. Ongoing flag will only affect dismissing
with *Clear all* or when phone is locked.

Updating targetSdkVersion required updating the RecorderService type, since these
are now mandatory. Setting `microphone` type also required additional permission,
`FOREGROUND_SERVICE_MICROPHONE`.

No other changes from these lists affects this app:
- https://developer.android.com/about/versions/14/behavior-changes-all
- https://developer.android.com/about/versions/14/behavior-changes-14
2023-07-28 17:23:26 +02:00
tibbi 8e9b73a96e Merge branch 'master' of github.com:SimpleMobileTools/Simple-Voice-Recorder 2023-07-27 16:04:10 +02:00
Tibor Kaputa 272f5a8b21
Merge pull request #205 from esensar/feature/recycle-bin
Add support for recycle bin
2023-07-27 16:03:58 +02:00
tibbi fe2c7dfc44 Merge branch 'feature/recycle-bin' of github.com:esensar/Simple-Voice-Recorder 2023-07-27 16:02:58 +02:00
Ensar Sarajčić 3c3be72c90 Close search when navigating away from recycle bin tab 2023-07-27 15:56:04 +02:00
tibbi ff65d8949c Merge branch 'feature/recycle-bin' of github.com:esensar/Simple-Voice-Recorder 2023-07-27 15:28:36 +02:00
Ensar Sarajčić dda0a9aa87 Prevent navigating to player tab when searching recycle bin 2023-07-27 15:25:45 +02:00
Ensar Sarajčić f5079dd8bf Fix formatting in TrashFragment 2023-07-27 12:32:21 +02:00
Ensar Sarajčić e9c35fae0f Always use offscreenPageLimit of 2 2023-07-27 12:22:18 +02:00
Ensar Sarajčić f0db5d72aa Clean up IDs in TrashFragment for clarity 2023-07-27 10:56:15 +02:00
Ensar Sarajčić a0278914ee Remove needless background thread and main thread wrappers in setupAdapter 2023-07-27 10:50:04 +02:00
Ensar Sarajčić 68166c318c Increase offscreenPageLimit when recycle bin is used 2023-07-27 10:46:06 +02:00
tibbi fee947feac Merge branch 'feature/recycle-bin' of github.com:esensar/Simple-Voice-Recorder 2023-07-26 20:40:36 +02:00
Tibor Kaputa fa4d2904cf
Merge pull request #203 from weblate/weblate-simple-mobile-tools-simple-voice-recorder
Translations update from Hosted Weblate
2023-07-25 16:08:49 +02:00
Ensar Sarajčić ce855965b9 Update Simple-Commons version to include queryCursor helper 2023-07-25 15:48:04 +02:00
Ensar Sarajčić f511da5180 Prevent running recycle bin check on Q+ devices 2023-07-25 12:22:46 +02:00
Ensar Sarajčić 950f5a0207 Read using new queries on R+ devices to properly get trashed media 2023-07-25 12:13:54 +02:00
Ensar Sarajčić b9035f2612 Split deletion lines for clarity 2023-07-25 08:24:29 +02:00
Ensar Sarajčić 7aba993726 Add param name to callbacks for clarity 2023-07-25 08:24:11 +02:00
Ensar Sarajčić 5883e91bc4 Add support for recycle bin
This uses system recycle bin whenever possible, which will automatically
delete files after 30 days. When not available, moves files to hidden `.trash`
subdirectory and manually removes them after 30 days.
2023-07-24 16:24:05 +02:00
Priit Jõerüüt 31e194202b
Translated using Weblate (Estonian)
Currently translated at 100.0% (11 of 11 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder metadata
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder-metadata/et/
2023-07-21 11:09:22 +02:00
Priit Jõerüüt 2d6e7dc29e
Translated using Weblate (Estonian)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/et/
2023-07-21 11:09:22 +02:00
abc0922001 58f74c23ce
Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (11 of 11 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder metadata
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder-metadata/zh_Hant/
2023-07-17 14:50:05 +02:00
abc0922001 423f07fae5
Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/zh_Hant/
2023-07-17 14:50:05 +02:00
elgratea 439b944a1e
Translated using Weblate (Bulgarian)
Currently translated at 100.0% (25 of 25 strings)

Translation: Simple Mobile Tools/Simple Voice Recorder
Translate-URL: https://hosted.weblate.org/projects/simple-mobile-tools/simple-voice-recorder/bg/
2023-07-17 14:50:05 +02:00
tibbi bc07f3c9fe updating commons 2023-07-17 14:48:58 +02:00
tibbi 4d01b6d4a0 fixing the spanish translation 2023-07-03 17:12:51 +02:00
tibbi 37c3c3c863 replacing jcenter with mavenCentral 2023-06-23 11:34:15 +02:00
55 changed files with 1599 additions and 571 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
github: [tibbi]
patreon: tiborkaputa
custom: ["https://www.paypal.com/paypalme/simplemobiletools", "https://www.simplemobiletools.com/donate"]
custom: ["https://www.paypal.me/simplemobiletools"]

View File

@ -1,6 +1,28 @@
Changelog
==========
Version 5.12.3 *(2023-10-09)*
----------------------------
* Fixed a glitch with inability to change save folder
* Added some stability, UI and translation improvements
Version 5.12.2 *(2023-09-18)*
----------------------------
* Added some stability, UI and translation improvements
Version 5.12.1 *(2023-09-13)*
----------------------------
* Added some stability, UI and translation improvements
Version 5.12.0 *(2023-07-31)*
----------------------------
* Added a Recycle bin
* Added some stability, UI and translation improvements
Version 5.11.4 *(2023-06-20)*
----------------------------

View File

@ -15,20 +15,17 @@ It comes with material design and dark theme by default, provides great user exp
Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors.
Check out the full suite of Simple Tools here:
https://www.simplemobiletools.com
<a href="https://f-droid.org/packages/com.simplemobiletools.voicerecorder">Get it on F-Droid</a>
Facebook:
https://www.facebook.com/simplemobiletools
Reddit:
https://www.reddit.com/r/SimpleMobileTools
Telegram:
https://t.me/SimpleMobileTools
<a href='https://play.google.com/store/apps/details?id=com.simplemobiletools.voicerecorder'><img src='https://simplemobiletools.com/images/button-google-play.svg' alt='Get it on Google Play' height=45/></a>
<a href='https://f-droid.org/packages/com.simplemobiletools.voicerecorder/'><img src='https://simplemobiletools.com/images/button-f-droid.png' alt='Get it on F-Droid' height=45 ></a>
Support us:
IBAN: SK4083300000002000965231
Bitcoin: 19Hc8A7sWGud8sP19VXDC5a5j28UyJfpyJ
Ethereum: 0xB7a2DD6f2408Bce77334655CF5E7639aE31feb30
Litecoin: LYACbHTKaM9ZubKQGxJ4NRyVy1gHUuztRP
Bitcoin Cash: qz6dvmhq5vzkcsypxpp2mnur30muxdah4gvulx3y85
Tether: 0x250f9cC32863E59b87037a14955Ed64F879653F0
<a href="https://paypal.me/SimpleMobileTools?country.x=SK&locale.x=en_US">PayPal</a>
<a href="https://www.patreon.com/tiborkaputa">Patreon</a>
<div style="display:flex;">
<img alt="App image" src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg" width="30%">
<img alt="App image" src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg" width="30%">

View File

@ -1,74 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.simplemobiletools.voicerecorder"
minSdkVersion 23
targetSdkVersion 33
versionCode 37
versionName "5.11.4"
setProperty("archivesBaseName", "voice-recorder")
vectorDrawables.useSupportLibrary = true
}
signingConfigs {
if (keystorePropertiesFile.exists()) {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (keystorePropertiesFile.exists()) {
signingConfig signingConfigs.release
}
}
}
flavorDimensions "variants"
productFlavors {
core {}
fdroid {}
prepaid {}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:b9309035a9'
implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'com.github.Armen101:AudioRecordView:1.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.github.naman14:TAndroidLame:1.1'
implementation 'me.grantland:autofittextview:0.2.1'
}

104
app/build.gradle.kts Normal file
View File

@ -0,0 +1,104 @@
import java.io.FileInputStream
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.konan.properties.Properties
plugins {
alias(libs.plugins.android)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.ksp)
base
}
base {
archivesName.set("voice-recorder")
}
val keystorePropertiesFile: File = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
compileSdk = project.libs.versions.app.build.compileSDKVersion.get().toInt()
defaultConfig {
applicationId = libs.versions.app.version.appId.get()
minSdk = project.libs.versions.app.build.minimumSDK.get().toInt()
targetSdk = project.libs.versions.app.build.targetSDK.get().toInt()
versionName = project.libs.versions.app.version.versionName.get()
versionCode = project.libs.versions.app.version.versionCode.get().toInt()
vectorDrawables.useSupportLibrary = true
}
signingConfigs {
if (keystorePropertiesFile.exists()) {
register("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
}
buildFeatures {
viewBinding = true
buildConfig = true
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
}
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
}
}
flavorDimensions.add("variants")
productFlavors {
register("core")
register("fdroid")
register("prepaid")
}
sourceSets {
getByName("main").java.srcDirs("src/main/kotlin")
}
compileOptions {
val currentJavaVersionFromLibs = JavaVersion.valueOf(libs.versions.app.build.javaVersion.get().toString())
sourceCompatibility = currentJavaVersionFromLibs
targetCompatibility = currentJavaVersionFromLibs
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = project.libs.versions.app.build.kotlinJVMTarget.get()
}
namespace = libs.versions.app.version.appId.get()
lint {
checkReleaseBuilds = false
abortOnError = false
}
}
dependencies {
implementation(libs.simple.tools.commons)
implementation(libs.eventbus)
implementation(libs.audiorecordview)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.constraintlayout)
implementation(libs.tandroidlame)
implementation(libs.autofittextview)
}

View File

@ -4,3 +4,5 @@
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
-keep class androidx.compose.runtime.** { *; }

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.simplemobiletools.voicerecorder"
android:installLocation="auto">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<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.WRITE_EXTERNAL_STORAGE"
@ -106,6 +106,7 @@
<service
android:name=".services.RecorderService"
android:foregroundServiceType="microphone"
android:exported="false">
<intent-filter>
<action android:name="com.simplemobiletools.voicerecorder.action.GET_RECORDER_INFO" />

View File

@ -2,7 +2,7 @@ package com.simplemobiletools.voicerecorder.activities
import android.content.Intent
import com.simplemobiletools.commons.dialogs.PermissionRequiredDialog
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.commons.extensions.openNotificationSettings
import com.simplemobiletools.voicerecorder.services.RecorderService
class BackgroundRecordActivity : SimpleActivity() {
@ -26,7 +26,7 @@ class BackgroundRecordActivity : SimpleActivity() {
}
}
} else {
PermissionRequiredDialog(this, R.string.allow_notifications_voice_recorder)
PermissionRequiredDialog(this, com.simplemobiletools.commons.R.string.allow_notifications_voice_recorder, { openNotificationSettings() })
}
}
}

View File

@ -12,11 +12,12 @@ import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.voicerecorder.BuildConfig
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.adapters.ViewPagerAdapter
import com.simplemobiletools.voicerecorder.databinding.ActivityMainBinding
import com.simplemobiletools.voicerecorder.extensions.checkRecycleBinItems
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.services.RecorderService
import kotlinx.android.synthetic.main.activity_main.*
import me.grantland.widget.AutofitHelper
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -25,26 +26,32 @@ import org.greenrobot.eventbus.ThreadMode
class MainActivity : SimpleActivity() {
private var bus: EventBus? = null
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
appLaunched(BuildConfig.APPLICATION_ID)
setupOptionsMenu()
refreshMenuItems()
updateMaterialActivityViews(main_coordinator, main_holder, useTransparentNavigation = false, useTopSearchMenu = true)
updateMaterialActivityViews(binding.mainCoordinator, binding.mainHolder, useTransparentNavigation = false, useTopSearchMenu = true)
if (checkAppSideloading()) {
return
}
if (savedInstanceState == null) {
checkRecycleBinItems()
}
handlePermission(PERMISSION_RECORD_AUDIO) {
if (it) {
tryInitVoiceRecorder()
} else {
toast(R.string.no_audio_permissions)
toast(com.simplemobiletools.commons.R.string.no_audio_permissions)
finish()
}
}
@ -63,14 +70,17 @@ class MainActivity : SimpleActivity() {
override fun onResume() {
super.onResume()
setupTabColors()
updateMenuColors()
if (getPagerAdapter()?.showRecycleBin != config.useRecycleBin) {
setupViewPager()
}
setupTabColors()
getPagerAdapter()?.onResume()
}
override fun onPause() {
super.onPause()
config.lastUsedViewPagerPage = view_pager.currentItem
config.lastUsedViewPagerPage = binding.viewPager.currentItem
}
override fun onDestroy() {
@ -88,8 +98,8 @@ class MainActivity : SimpleActivity() {
}
override fun onBackPressed() {
if (main_menu.isSearchOpen) {
main_menu.closeSearch()
if (binding.mainMenu.isSearchOpen) {
binding.mainMenu.closeSearch()
} else if (isThirdPartyIntent()) {
setResult(Activity.RESULT_CANCELED, null)
super.onBackPressed()
@ -99,25 +109,27 @@ class MainActivity : SimpleActivity() {
}
private fun refreshMenuItems() {
main_menu.getToolbar().menu.apply {
findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(R.bool.hide_google_relations)
binding.mainMenu.getToolbar().menu.apply {
findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations)
}
}
private fun setupOptionsMenu() {
main_menu.getToolbar().inflateMenu(R.menu.menu)
main_menu.toggleHideOnScroll(false)
main_menu.setupMenu()
binding.mainMenu.getToolbar().inflateMenu(R.menu.menu)
binding.mainMenu.toggleHideOnScroll(false)
binding.mainMenu.setupMenu()
main_menu.onSearchOpenListener = {
view_pager.currentItem = 1
binding.mainMenu.onSearchOpenListener = {
if (binding.viewPager.currentItem == 0) {
binding.viewPager.currentItem = 1
}
}
main_menu.onSearchTextChangedListener = { text ->
binding.mainMenu.onSearchTextChangedListener = { text ->
getPagerAdapter()?.searchTextChanged(text)
}
main_menu.getToolbar().setOnMenuItemClickListener { menuItem ->
binding.mainMenu.getToolbar().setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.settings -> launchSettings()
@ -130,7 +142,7 @@ class MainActivity : SimpleActivity() {
private fun updateMenuColors() {
updateStatusbarColor(getProperBackgroundColor())
main_menu.updateColors()
binding.mainMenu.updateColors()
}
private fun tryInitVoiceRecorder() {
@ -148,61 +160,68 @@ class MainActivity : SimpleActivity() {
}
private fun setupViewPager() {
main_tabs_holder.removeAllTabs()
val tabDrawables = arrayOf(R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector)
val tabLabels = arrayOf(R.string.recorder, R.string.player)
binding.mainTabsHolder.removeAllTabs()
var tabDrawables = arrayOf(com.simplemobiletools.commons.R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector)
var tabLabels = arrayOf(R.string.recorder, R.string.player)
if (config.useRecycleBin) {
tabDrawables += com.simplemobiletools.commons.R.drawable.ic_delete_vector
tabLabels += com.simplemobiletools.commons.R.string.recycle_bin
}
tabDrawables.forEachIndexed { i, drawableId ->
main_tabs_holder.newTab().setCustomView(R.layout.bottom_tablayout_item).apply {
customView?.findViewById<ImageView>(R.id.tab_item_icon)?.setImageDrawable(getDrawable(drawableId))
customView?.findViewById<TextView>(R.id.tab_item_label)?.setText(tabLabels[i])
AutofitHelper.create(customView?.findViewById(R.id.tab_item_label))
main_tabs_holder.addTab(this)
binding.mainTabsHolder.newTab().setCustomView(com.simplemobiletools.commons.R.layout.bottom_tablayout_item).apply {
customView?.findViewById<ImageView>(com.simplemobiletools.commons.R.id.tab_item_icon)?.setImageDrawable(getDrawable(drawableId))
customView?.findViewById<TextView>(com.simplemobiletools.commons.R.id.tab_item_label)?.setText(tabLabels[i])
AutofitHelper.create(customView?.findViewById(com.simplemobiletools.commons.R.id.tab_item_label))
binding.mainTabsHolder.addTab(this)
}
}
main_tabs_holder.onTabSelectionChanged(
binding.mainTabsHolder.onTabSelectionChanged(
tabUnselectedAction = {
updateBottomTabItemColors(it.customView, false)
if (it.position == 1) {
main_menu.closeSearch()
if (it.position == 1 || it.position == 2) {
binding.mainMenu.closeSearch()
}
},
tabSelectedAction = {
view_pager.currentItem = it.position
binding.viewPager.currentItem = it.position
updateBottomTabItemColors(it.customView, true)
}
)
view_pager.adapter = ViewPagerAdapter(this)
view_pager.onPageChangeListener {
main_tabs_holder.getTabAt(it)?.select()
(view_pager.adapter as ViewPagerAdapter).finishActMode()
binding.viewPager.adapter = ViewPagerAdapter(this, config.useRecycleBin)
binding.viewPager.offscreenPageLimit = 2
binding.viewPager.onPageChangeListener {
binding.mainTabsHolder.getTabAt(it)?.select()
(binding.viewPager.adapter as ViewPagerAdapter).finishActMode()
}
if (isThirdPartyIntent()) {
view_pager.currentItem = 0
binding.viewPager.currentItem = 0
} else {
view_pager.currentItem = config.lastUsedViewPagerPage
main_tabs_holder.getTabAt(config.lastUsedViewPagerPage)?.select()
binding.viewPager.currentItem = config.lastUsedViewPagerPage
binding.mainTabsHolder.getTabAt(config.lastUsedViewPagerPage)?.select()
}
}
private fun setupTabColors() {
val activeView = main_tabs_holder.getTabAt(view_pager.currentItem)?.customView
val inactiveView = main_tabs_holder.getTabAt(getInactiveTabIndex())?.customView
val activeView = binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.customView
updateBottomTabItemColors(activeView, true)
updateBottomTabItemColors(inactiveView, false)
for (i in 0 until binding.mainTabsHolder.tabCount) {
if (i != binding.viewPager.currentItem) {
val inactiveView = binding.mainTabsHolder.getTabAt(i)?.customView
updateBottomTabItemColors(inactiveView, false)
}
}
main_tabs_holder.getTabAt(view_pager.currentItem)?.select()
binding.mainTabsHolder.getTabAt(binding.viewPager.currentItem)?.select()
val bottomBarColor = getBottomNavigationBackgroundColor()
main_tabs_holder.setBackgroundColor(bottomBarColor)
binding.mainTabsHolder.setBackgroundColor(bottomBarColor)
updateNavigationBarColor(bottomBarColor)
}
private fun getInactiveTabIndex() = if (view_pager.currentItem == 0) 1 else 0
private fun getPagerAdapter() = (view_pager.adapter as? ViewPagerAdapter)
private fun getPagerAdapter() = (binding.viewPager.adapter as? ViewPagerAdapter)
private fun launchSettings() {
hideKeyboard()
@ -214,12 +233,12 @@ class MainActivity : SimpleActivity() {
val faqItems = arrayListOf(
FAQItem(R.string.faq_1_title, R.string.faq_1_text),
FAQItem(R.string.faq_9_title_commons, R.string.faq_9_text_commons)
FAQItem(com.simplemobiletools.commons.R.string.faq_9_title_commons, com.simplemobiletools.commons.R.string.faq_9_text_commons)
)
if (!resources.getBoolean(R.bool.hide_google_relations)) {
faqItems.add(FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons))
faqItems.add(FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons))
if (!resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations)) {
faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_2_title_commons, com.simplemobiletools.commons.R.string.faq_2_text_commons))
faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_6_title_commons, com.simplemobiletools.commons.R.string.faq_6_text_commons))
}
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)

View File

@ -3,37 +3,41 @@ package com.simplemobiletools.voicerecorder.activities
import android.content.Intent
import android.media.MediaRecorder
import android.os.Bundle
import com.simplemobiletools.commons.dialogs.ChangeDateTimeFormatDialog
import com.simplemobiletools.commons.dialogs.FeatureLockedDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.databinding.ActivitySettingsBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.emptyTheRecycleBin
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
import com.simplemobiletools.voicerecorder.helpers.BITRATES
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_M4A
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_MP3
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_OGG
import kotlinx.android.synthetic.main.activity_settings.*
import java.util.*
import com.simplemobiletools.voicerecorder.models.Events
import org.greenrobot.eventbus.EventBus
import java.util.Locale
import kotlin.system.exitProcess
class SettingsActivity : SimpleActivity() {
private var recycleBinContentSize = 0
private lateinit var binding: ActivitySettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
updateMaterialActivityViews(settings_coordinator, settings_holder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(settings_nested_scrollview, settings_toolbar)
updateMaterialActivityViews(binding.settingsCoordinator, binding.settingsHolder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(binding.settingsNestedScrollview, binding.settingsToolbar)
}
override fun onResume() {
super.onResume()
setupToolbar(settings_toolbar, NavigationIcon.Arrow)
setupToolbar(binding.settingsToolbar, NavigationIcon.Arrow)
setupPurchaseThankYou()
setupCustomizeColors()
@ -47,29 +51,31 @@ class SettingsActivity : SimpleActivity() {
setupBitrate()
setupAudioSource()
setupRecordAfterLaunch()
updateTextColors(settings_nested_scrollview)
setupUseRecycleBin()
setupEmptyRecycleBin()
updateTextColors(binding.settingsNestedScrollview)
arrayOf(settings_color_customization_section_label, settings_general_settings_label).forEach {
arrayOf(binding.settingsColorCustomizationSectionLabel, binding.settingsGeneralSettingsLabel, binding.settingsRecycleBinLabel).forEach {
it.setTextColor(getProperPrimaryColor())
}
}
private fun setupPurchaseThankYou() {
settings_purchase_thank_you_holder.beGoneIf(isOrWasThankYouInstalled())
settings_purchase_thank_you_holder.setOnClickListener {
binding.settingsPurchaseThankYouHolder.beGoneIf(isOrWasThankYouInstalled())
binding.settingsPurchaseThankYouHolder.setOnClickListener {
launchPurchaseThankYouIntent()
}
}
private fun setupCustomizeColors() {
settings_color_customization_label.text = getCustomizeColorsString()
settings_color_customization_holder.setOnClickListener {
binding.settingsColorCustomizationLabel.text = getCustomizeColorsString()
binding.settingsColorCustomizationHolder.setOnClickListener {
handleCustomizeColorsClick()
}
}
private fun setupCustomizeWidgetColors() {
settings_widget_color_customization_holder.setOnClickListener {
binding.settingsWidgetColorCustomizationHolder.setOnClickListener {
Intent(this, WidgetRecordDisplayConfigureActivity::class.java).apply {
putExtra(IS_CUSTOMIZING_COLORS, true)
startActivity(this)
@ -78,41 +84,41 @@ class SettingsActivity : SimpleActivity() {
}
private fun setupUseEnglish() {
settings_use_english_holder.beVisibleIf((config.wasUseEnglishToggled || Locale.getDefault().language != "en") && !isTiramisuPlus())
settings_use_english.isChecked = config.useEnglish
settings_use_english_holder.setOnClickListener {
settings_use_english.toggle()
config.useEnglish = settings_use_english.isChecked
binding.settingsUseEnglishHolder.beVisibleIf((config.wasUseEnglishToggled || Locale.getDefault().language != "en") && !isTiramisuPlus())
binding.settingsUseEnglish.isChecked = config.useEnglish
binding.settingsUseEnglishHolder.setOnClickListener {
binding.settingsUseEnglish.toggle()
config.useEnglish = binding.settingsUseEnglish.isChecked
exitProcess(0)
}
}
private fun setupLanguage() {
settings_language.text = Locale.getDefault().displayLanguage
settings_language_holder.beVisibleIf(isTiramisuPlus())
settings_language_holder.setOnClickListener {
binding.settingsLanguage.text = Locale.getDefault().displayLanguage
binding.settingsLanguageHolder.beVisibleIf(isTiramisuPlus())
binding.settingsLanguageHolder.setOnClickListener {
launchChangeAppLanguageIntent()
}
}
private fun setupChangeDateTimeFormat() {
settings_change_date_time_format_holder.setOnClickListener {
binding.settingsChangeDateTimeFormatHolder.setOnClickListener {
ChangeDateTimeFormatDialog(this) {}
}
}
private fun setupHideNotification() {
settings_hide_notification.isChecked = config.hideNotification
settings_hide_notification_holder.setOnClickListener {
settings_hide_notification.toggle()
config.hideNotification = settings_hide_notification.isChecked
binding.settingsHideNotification.isChecked = config.hideNotification
binding.settingsHideNotificationHolder.setOnClickListener {
binding.settingsHideNotification.toggle()
config.hideNotification = binding.settingsHideNotification.isChecked
}
}
private fun setupSaveRecordingsFolder() {
settings_save_recordings_label.text = addLockedLabelIfNeeded(R.string.save_recordings_in)
settings_save_recordings.text = humanizePath(config.saveRecordingsFolder)
settings_save_recordings_holder.setOnClickListener {
binding.settingsSaveRecordingsLabel.text = addLockedLabelIfNeeded(R.string.save_recordings_in)
binding.settingsSaveRecordings.text = humanizePath(config.saveRecordingsFolder)
binding.settingsSaveRecordingsHolder.setOnClickListener {
if (isOrWasThankYouInstalled()) {
FilePickerDialog(this, config.saveRecordingsFolder, false, showFAB = true) {
val path = it
@ -127,7 +133,7 @@ class SettingsActivity : SimpleActivity() {
}
config.saveRecordingsFolder = path
settings_save_recordings.text = humanizePath(config.saveRecordingsFolder)
binding.settingsSaveRecordings.text = humanizePath(config.saveRecordingsFolder)
}
}
}
@ -138,8 +144,8 @@ class SettingsActivity : SimpleActivity() {
}
private fun setupExtension() {
settings_extension.text = config.getExtensionText()
settings_extension_holder.setOnClickListener {
binding.settingsExtension.text = config.getExtensionText()
binding.settingsExtensionHolder.setOnClickListener {
val items = arrayListOf(
RadioItem(EXTENSION_M4A, getString(R.string.m4a)),
RadioItem(EXTENSION_MP3, getString(R.string.mp3))
@ -151,19 +157,19 @@ class SettingsActivity : SimpleActivity() {
RadioGroupDialog(this@SettingsActivity, items, config.extension) {
config.extension = it as Int
settings_extension.text = config.getExtensionText()
binding.settingsExtension.text = config.getExtensionText()
}
}
}
private fun setupBitrate() {
settings_bitrate.text = getBitrateText(config.bitrate)
settings_bitrate_holder.setOnClickListener {
binding.settingsBitrate.text = getBitrateText(config.bitrate)
binding.settingsBitrateHolder.setOnClickListener {
val items = BITRATES.map { RadioItem(it, getBitrateText(it)) } as ArrayList
RadioGroupDialog(this@SettingsActivity, items, config.bitrate) {
config.bitrate = it as Int
settings_bitrate.text = getBitrateText(config.bitrate)
binding.settingsBitrate.text = getBitrateText(config.bitrate)
}
}
}
@ -171,21 +177,69 @@ class SettingsActivity : SimpleActivity() {
private fun getBitrateText(value: Int): String = getString(R.string.bitrate_value).format(value / 1000)
private fun setupRecordAfterLaunch() {
settings_record_after_launch.isChecked = config.recordAfterLaunch
settings_record_after_launch_holder.setOnClickListener {
settings_record_after_launch.toggle()
config.recordAfterLaunch = settings_record_after_launch.isChecked
binding.settingsRecordAfterLaunch.isChecked = config.recordAfterLaunch
binding.settingsRecordAfterLaunchHolder.setOnClickListener {
binding.settingsRecordAfterLaunch.toggle()
config.recordAfterLaunch = binding.settingsRecordAfterLaunch.isChecked
}
}
private fun setupUseRecycleBin() {
updateRecycleBinButtons()
binding.settingsUseRecycleBin.isChecked = config.useRecycleBin
binding.settingsUseRecycleBinHolder.setOnClickListener {
binding.settingsUseRecycleBin.toggle()
config.useRecycleBin = binding.settingsUseRecycleBin.isChecked
updateRecycleBinButtons()
}
}
private fun updateRecycleBinButtons() {
binding.settingsEmptyRecycleBinHolder.beVisibleIf(config.useRecycleBin)
}
private fun setupEmptyRecycleBin() {
ensureBackgroundThread {
try {
recycleBinContentSize = getAllRecordings(trashed = true).sumByInt {
it.size
}
} catch (ignored: Exception) {
}
runOnUiThread {
binding.settingsEmptyRecycleBinSize.text = recycleBinContentSize.formatSize()
}
}
binding.settingsEmptyRecycleBinHolder.setOnClickListener {
if (recycleBinContentSize == 0) {
toast(com.simplemobiletools.commons.R.string.recycle_bin_empty)
} else {
ConfirmationDialog(
this,
"",
com.simplemobiletools.commons.R.string.empty_recycle_bin_confirmation,
com.simplemobiletools.commons.R.string.yes,
com.simplemobiletools.commons.R.string.no
) {
emptyTheRecycleBin()
recycleBinContentSize = 0
binding.settingsEmptyRecycleBinSize.text = 0.formatSize()
EventBus.getDefault().post(Events.RecordingTrashUpdated())
}
}
}
}
private fun setupAudioSource() {
settings_audio_source.text = config.getAudioSourceText(config.audioSource)
settings_audio_source_holder.setOnClickListener {
binding.settingsAudioSource.text = config.getAudioSourceText(config.audioSource)
binding.settingsAudioSourceHolder.setOnClickListener {
val items = getAudioSources().map { RadioItem(it, config.getAudioSourceText(it)) } as ArrayList
RadioGroupDialog(this@SettingsActivity, items, config.audioSource) {
config.audioSource = it as Int
settings_audio_source.text = config.getAudioSourceText(config.audioSource)
binding.settingsAudioSource.text = config.getAudioSourceText(config.audioSource)
}
}
}

View File

@ -12,9 +12,9 @@ import com.simplemobiletools.commons.dialogs.FeatureLockedDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.IS_CUSTOMIZING_COLORS
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.databinding.WidgetRecordDisplayConfigBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
import kotlinx.android.synthetic.main.widget_record_display_config.*
class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
private var mWidgetAlpha = 0f
@ -22,12 +22,14 @@ class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
private var mWidgetColor = 0
private var mWidgetColorWithoutTransparency = 0
private var mFeatureLockedDialog: FeatureLockedDialog? = null
private lateinit var binding: WidgetRecordDisplayConfigBinding
public override fun onCreate(savedInstanceState: Bundle?) {
useDynamicTheme = false
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_CANCELED)
setContentView(R.layout.widget_record_display_config)
binding = WidgetRecordDisplayConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
initVariables()
val isCustomizingColors = intent.extras?.getBoolean(IS_CUSTOMIZING_COLORS) ?: false
@ -37,11 +39,11 @@ class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
finish()
}
config_save.setOnClickListener { saveConfig() }
config_widget_color.setOnClickListener { pickBackgroundColor() }
binding.configSave.setOnClickListener { saveConfig() }
binding.configWidgetColor.setOnClickListener { pickBackgroundColor() }
val primaryColor = getProperPrimaryColor()
config_widget_seekbar.setColors(getProperTextColor(), primaryColor, primaryColor)
binding.configWidgetSeekbar.setColors(getProperTextColor(), primaryColor, primaryColor)
if (!isCustomizingColors && !isOrWasThankYouInstalled()) {
mFeatureLockedDialog = FeatureLockedDialog(this) {
@ -51,8 +53,8 @@ class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
}
}
config_save.backgroundTintList = ColorStateList.valueOf(getProperPrimaryColor())
config_save.setTextColor(getProperPrimaryColor().getContrastColor())
binding.configSave.backgroundTintList = ColorStateList.valueOf(getProperPrimaryColor())
binding.configSave.setTextColor(getProperPrimaryColor().getContrastColor())
}
override fun onResume() {
@ -67,14 +69,14 @@ class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
private fun initVariables() {
mWidgetColor = config.widgetBgColor
if (mWidgetColor == resources.getColor(R.color.default_widget_bg_color) && config.isUsingSystemTheme) {
mWidgetColor = resources.getColor(R.color.you_primary_color, theme)
mWidgetColor = resources.getColor(com.simplemobiletools.commons.R.color.you_primary_color, theme)
}
mWidgetAlpha = Color.alpha(mWidgetColor) / 255.toFloat()
mWidgetColorWithoutTransparency = Color.rgb(Color.red(mWidgetColor), Color.green(mWidgetColor), Color.blue(mWidgetColor))
config_widget_seekbar.setOnSeekBarChangeListener(seekbarChangeListener)
config_widget_seekbar.progress = (mWidgetAlpha * 100).toInt()
binding.configWidgetSeekbar.setOnSeekBarChangeListener(seekbarChangeListener)
binding.configWidgetSeekbar.progress = (mWidgetAlpha * 100).toInt()
updateColors()
}
@ -107,8 +109,8 @@ class WidgetRecordDisplayConfigureActivity : SimpleActivity() {
private fun updateColors() {
mWidgetColor = mWidgetColorWithoutTransparency.adjustAlpha(mWidgetAlpha)
config_widget_color.setFillWithStroke(mWidgetColor, mWidgetColor)
config_image.background.mutate().applyColorFilter(mWidgetColor)
binding.configWidgetColor.setFillWithStroke(mWidgetColor, mWidgetColor)
binding.configImage.background.mutate().applyColorFilter(mWidgetColor)
}
private val seekbarChangeListener = object : SeekBar.OnSeekBarChangeListener {

View File

@ -1,28 +1,28 @@
package com.simplemobiletools.voicerecorder.adapters
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import android.view.*
import android.widget.PopupMenu
import android.widget.TextView
import androidx.core.net.toUri
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.voicerecorder.BuildConfig
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.databinding.ItemRecordingBinding
import com.simplemobiletools.voicerecorder.dialogs.DeleteConfirmationDialog
import com.simplemobiletools.voicerecorder.dialogs.RenameRecordingDialog
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.deleteRecordings
import com.simplemobiletools.voicerecorder.extensions.moveRecordingsToRecycleBin
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import kotlinx.android.synthetic.main.item_recording.view.*
import java.io.File
import org.greenrobot.eventbus.EventBus
class RecordingsAdapter(
activity: SimpleActivity,
@ -30,8 +30,7 @@ class RecordingsAdapter(
val refreshListener: RefreshRecordingsListener,
recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit
) :
MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate {
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate {
var currRecordingId = 0
@ -74,7 +73,9 @@ class RecordingsAdapter(
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_recording, parent)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return createViewHolder(ItemRecordingBinding.inflate(layoutInflater, parent, false).root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val recording = recordings[position]
@ -135,12 +136,21 @@ class RecordingsAdapter(
resources.getQuantityString(R.plurals.delete_recordings, itemsCnt, itemsCnt)
}
val baseString = R.string.delete_recordings_confirmation
val baseString = if (activity.config.useRecycleBin) {
com.simplemobiletools.commons.R.string.move_to_recycle_bin_confirmation
} else {
R.string.delete_recordings_confirmation
}
val question = String.format(resources.getString(baseString), items)
ConfirmationDialog(activity, question) {
DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin) { skipRecycleBin ->
ensureBackgroundThread {
deleteMediaStoreRecordings()
val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin
if (toRecycleBin) {
moveMediaStoreRecordingsToRecycleBin()
} else {
deleteMediaStoreRecordings()
}
}
}
}
@ -154,38 +164,26 @@ class RecordingsAdapter(
val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
val positions = getSelectedItemPositions()
when {
isRPlus() -> {
val fileUris = recordingsToRemove.map { recording ->
"${Media.EXTERNAL_CONTENT_URI}/${recording.id.toLong()}".toUri()
}
activity.deleteSDK30Uris(fileUris) { success ->
if (success) {
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
}
}
}
isQPlus() -> {
recordingsToRemove.forEach {
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val selection = "${Media._ID} = ?"
val selectionArgs = arrayOf(it.id.toString())
val result = activity.contentResolver.delete(uri, selection, selectionArgs)
if (result == 0) {
recordingsToRemove.forEach {
activity.deleteFile(File(it.path).toFileDirItem(activity))
}
}
}
activity.deleteRecordings(recordingsToRemove) { success ->
if (success) {
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
}
else -> {
recordingsToRemove.forEach {
activity.deleteFile(File(it.path).toFileDirItem(activity))
}
}
}
private fun moveMediaStoreRecordingsToRecycleBin() {
if (selectedKeys.isEmpty()) {
return
}
val oldRecordingIndex = recordings.indexOfFirst { it.id == currRecordingId }
val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
val positions = getSelectedItemPositions()
activity.moveRecordingsToRecycleBin(recordingsToRemove) { success ->
if (success) {
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
EventBus.getDefault().post(Events.RecordingTrashUpdated())
}
}
}
@ -218,30 +216,30 @@ class RecordingsAdapter(
private fun getSelectedItems() = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
private fun setupView(view: View, recording: Recording) {
view.apply {
setupViewBackground(activity)
recording_frame?.isSelected = selectedKeys.contains(recording.id)
ItemRecordingBinding.bind(view).apply {
root.setupViewBackground(activity)
recordingFrame.isSelected = selectedKeys.contains(recording.id)
arrayListOf<TextView>(recording_title, recording_date, recording_duration, recording_size).forEach {
arrayListOf<TextView>(recordingTitle, recordingDate, recordingDuration, recordingSize).forEach {
it.setTextColor(textColor)
}
if (recording.id == currRecordingId) {
recording_title.setTextColor(context.getProperPrimaryColor())
recordingTitle.setTextColor(root.context.getProperPrimaryColor())
}
recording_title.text = recording.title
recording_date.text = recording.timestamp.formatDate(context)
recording_duration.text = recording.duration.getFormattedDuration()
recording_size.text = recording.size.formatSize()
recordingTitle.text = recording.title
recordingDate.text = recording.timestamp.formatDate(root.context)
recordingDuration.text = recording.duration.getFormattedDuration()
recordingSize.text = recording.size.formatSize()
overflow_menu_icon.drawable.apply {
overflowMenuIcon.drawable.apply {
mutate()
setTint(activity.getProperTextColor())
}
overflow_menu_icon.setOnClickListener {
showPopupMenu(overflow_menu_anchor, recording)
overflowMenuIcon.setOnClickListener {
showPopupMenu(overflowMenuAnchor, recording)
}
}
}
@ -269,16 +267,19 @@ class RecordingsAdapter(
renameRecording()
}
}
R.id.cab_share -> {
executeItemMenuOperation(recordingId) {
shareRecordings()
}
}
R.id.cab_open_with -> {
executeItemMenuOperation(recordingId) {
openRecordingWith()
}
}
R.id.cab_delete -> {
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
askConfirmDelete()

View File

@ -0,0 +1,221 @@
package com.simplemobiletools.voicerecorder.adapters
import android.view.*
import android.widget.PopupMenu
import android.widget.TextView
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.databinding.ItemRecordingBinding
import com.simplemobiletools.voicerecorder.extensions.deleteRecordings
import com.simplemobiletools.voicerecorder.extensions.restoreRecordings
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import org.greenrobot.eventbus.EventBus
class TrashAdapter(
activity: SimpleActivity,
var recordings: ArrayList<Recording>,
val refreshListener: RefreshRecordingsListener,
recyclerView: MyRecyclerView
) :
MyRecyclerViewAdapter(activity, recyclerView, {}), RecyclerViewFastScroller.OnPopupTextUpdate {
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_trash
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_restore -> restoreRecordings()
R.id.cab_delete -> askConfirmDelete()
R.id.cab_select_all -> selectAll()
}
}
override fun getSelectableItemCount() = recordings.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = recordings.getOrNull(position)?.id
override fun getItemKeyPosition(key: Int) = recordings.indexOfFirst { it.id == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return createViewHolder(ItemRecordingBinding.inflate(layoutInflater, parent, false).root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val recording = recordings[position]
holder.bindView(recording, true, true) { itemView, layoutPosition ->
setupView(itemView, recording)
}
bindViewHolder(holder)
}
override fun getItemCount() = recordings.size
private fun getItemWithKey(key: Int): Recording? = recordings.firstOrNull { it.id == key }
fun updateItems(newItems: ArrayList<Recording>) {
if (newItems.hashCode() != recordings.hashCode()) {
recordings = newItems
notifyDataSetChanged()
finishActMode()
}
}
private fun restoreRecordings() {
if (selectedKeys.isEmpty()) {
return
}
val recordingsToRestore = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
val positions = getSelectedItemPositions()
activity.restoreRecordings(recordingsToRestore) { success ->
if (success) {
doDeleteAnimation(recordingsToRestore, positions)
EventBus.getDefault().post(Events.RecordingTrashUpdated())
}
}
}
private fun askConfirmDelete() {
val itemsCnt = selectedKeys.size
val firstItem = getSelectedItems().firstOrNull() ?: return
val items = if (itemsCnt == 1) {
"\"${firstItem.title}\""
} else {
resources.getQuantityString(R.plurals.delete_recordings, itemsCnt, itemsCnt)
}
val baseString = R.string.delete_recordings_confirmation
val question = String.format(resources.getString(baseString), items)
ConfirmationDialog(activity, question) {
ensureBackgroundThread {
deleteMediaStoreRecordings()
}
}
}
private fun deleteMediaStoreRecordings() {
if (selectedKeys.isEmpty()) {
return
}
val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
val positions = getSelectedItemPositions()
activity.deleteRecordings(recordingsToRemove) { success ->
if (success) {
doDeleteAnimation(recordingsToRemove, positions)
}
}
}
private fun doDeleteAnimation(recordingsToRemove: ArrayList<Recording>, positions: ArrayList<Int>) {
recordings.removeAll(recordingsToRemove.toSet())
activity.runOnUiThread {
if (recordings.isEmpty()) {
refreshListener.refreshRecordings()
finishActMode()
} else {
positions.sortDescending()
removeSelectedItems(positions)
}
}
}
private fun getSelectedItems() = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
private fun setupView(view: View, recording: Recording) {
ItemRecordingBinding.bind(view).apply {
root.setupViewBackground(activity)
recordingFrame.isSelected = selectedKeys.contains(recording.id)
arrayListOf<TextView>(recordingTitle, recordingDate, recordingDuration, recordingSize).forEach {
it.setTextColor(textColor)
}
recordingTitle.text = recording.title
recordingDate.text = recording.timestamp.formatDate(root.context)
recordingDuration.text = recording.duration.getFormattedDuration()
recordingSize.text = recording.size.formatSize()
overflowMenuIcon.drawable.apply {
mutate()
setTint(activity.getProperTextColor())
}
overflowMenuIcon.setOnClickListener {
showPopupMenu(overflowMenuAnchor, recording)
}
}
}
override fun onChange(position: Int) = recordings.getOrNull(position)?.title ?: ""
private fun showPopupMenu(view: View, recording: Recording) {
if (selectedKeys.isNotEmpty()) {
selectedKeys.clear()
notifyDataSetChanged()
}
finishActMode()
val theme = activity.getPopupMenuTheme()
val contextTheme = ContextThemeWrapper(activity, theme)
PopupMenu(contextTheme, view, Gravity.END).apply {
inflate(getActionMenuId())
menu.findItem(R.id.cab_select_all).isVisible = false
menu.findItem(R.id.cab_restore).title = resources.getString(com.simplemobiletools.commons.R.string.restore_this_file)
setOnMenuItemClickListener { item ->
val recordingId = recording.id
when (item.itemId) {
R.id.cab_restore -> {
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
restoreRecordings()
}
}
R.id.cab_delete -> {
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
askConfirmDelete()
}
}
}
true
}
show()
}
}
private fun executeItemMenuOperation(callId: Int, removeAfterCallback: Boolean = true, callback: () -> Unit) {
selectedKeys.add(callId)
callback()
if (removeAfterCallback) {
selectedKeys.remove(callId)
}
}
}

View File

@ -8,12 +8,18 @@ import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.fragments.MyViewPagerFragment
import com.simplemobiletools.voicerecorder.fragments.PlayerFragment
import com.simplemobiletools.voicerecorder.fragments.TrashFragment
class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() {
class ViewPagerAdapter(private val activity: SimpleActivity, val showRecycleBin: Boolean) : PagerAdapter() {
private val mFragments = SparseArray<MyViewPagerFragment>()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val layout = if (position == 0) R.layout.fragment_recorder else R.layout.fragment_player
val layout = when (position) {
0 -> R.layout.fragment_recorder
1 -> R.layout.fragment_player
2 -> R.layout.fragment_trash
else -> throw IllegalArgumentException("Invalid position. Count = $count, requested position = $position")
}
val view = activity.layoutInflater.inflate(layout, container, false)
container.addView(view)
@ -26,7 +32,11 @@ class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() {
container.removeView(item as View)
}
override fun getCount() = 2
override fun getCount() = if (showRecycleBin) {
3
} else {
2
}
override fun isViewFromObject(view: View, item: Any) = view == item
@ -42,9 +52,17 @@ class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() {
}
}
fun finishActMode() = (mFragments[1] as? PlayerFragment)?.finishActMode()
fun finishActMode() {
(mFragments[1] as? PlayerFragment)?.finishActMode()
if (showRecycleBin) {
(mFragments[2] as? TrashFragment)?.finishActMode()
}
}
fun searchTextChanged(text: String) {
(mFragments[1] as? PlayerFragment)?.onSearchTextChanged(text)
if (showRecycleBin) {
(mFragments[2] as? TrashFragment)?.onSearchTextChanged(text)
}
}
}

View File

@ -0,0 +1,38 @@
package com.simplemobiletools.voicerecorder.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.beGoneIf
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.voicerecorder.databinding.DialogDeleteConfirmationBinding
class DeleteConfirmationDialog(
private val activity: Activity,
private val message: String,
private val showSkipRecycleBinOption: Boolean,
private val callback: (skipRecycleBin: Boolean) -> Unit
) {
private var dialog: AlertDialog? = null
val binding = DialogDeleteConfirmationBinding.inflate(activity.layoutInflater)
val view = binding.root
init {
binding.deleteRememberTitle.text = message
binding.skipTheRecycleBinCheckbox.beGoneIf(!showSkipRecycleBinOption)
activity.getAlertDialogBuilder()
.setPositiveButton(com.simplemobiletools.commons.R.string.yes) { _, _ -> dialogConfirmed() }
.setNegativeButton(com.simplemobiletools.commons.R.string.no, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
}
}
}
private fun dialogConfirmed() {
dialog?.dismiss()
callback(binding.skipTheRecycleBinCheckbox.isChecked)
}
}

View File

@ -7,36 +7,36 @@ import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.databinding.DialogRenameRecordingBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import kotlinx.android.synthetic.main.dialog_rename_recording.view.*
import org.greenrobot.eventbus.EventBus
import java.io.File
class RenameRecordingDialog(val activity: BaseSimpleActivity, val recording: Recording, val callback: () -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_rename_recording, null).apply {
rename_recording_title.setText(recording.title.substringBeforeLast('.'))
val binding = DialogRenameRecordingBinding.inflate(activity.layoutInflater).apply {
renameRecordingTitle.setText(recording.title.substringBeforeLast('.'))
}
val view = binding.root
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(com.simplemobiletools.commons.R.string.ok, null)
.setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.rename) { alertDialog ->
alertDialog.showKeyboard(view.rename_recording_title)
activity.setupDialogStuff(view, this, com.simplemobiletools.commons.R.string.rename) { alertDialog ->
alertDialog.showKeyboard(binding.renameRecordingTitle)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val newTitle = view.rename_recording_title.value
val newTitle = binding.renameRecordingTitle.value
if (newTitle.isEmpty()) {
activity.toast(R.string.empty_name)
activity.toast(com.simplemobiletools.commons.R.string.empty_name)
return@setOnClickListener
}
if (!newTitle.isAValidFilename()) {
activity.toast(R.string.invalid_name)
activity.toast(com.simplemobiletools.commons.R.string.invalid_name)
return@setOnClickListener
}

View File

@ -0,0 +1,183 @@
package com.simplemobiletools.voicerecorder.extensions
import android.content.ContentValues
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.deleteFile
import com.simplemobiletools.commons.extensions.getParentPath
import com.simplemobiletools.commons.extensions.toFileDirItem
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.models.Recording
import java.io.File
fun BaseSimpleActivity.deleteRecordings(recordingsToRemove: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToRemove.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
deleteSDK30Uris(fileUris, callback)
}
isQPlus() -> {
recordingsToRemove.forEach {
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val selection = "${Media._ID} = ?"
val selectionArgs = arrayOf(it.id.toString())
val result = contentResolver.delete(uri, selection, selectionArgs)
if (result == 0) {
val fileDirItem = File(it.path).toFileDirItem(this)
deleteFile(fileDirItem)
}
}
callback(true)
}
else -> {
recordingsToRemove.forEach {
val fileDirItem = File(it.path).toFileDirItem(this)
deleteFile(fileDirItem)
}
callback(true)
}
}
}
fun BaseSimpleActivity.restoreRecordings(recordingsToRestore: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToRestore.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
trashSDK30Uris(fileUris, false, callback)
}
isQPlus() -> {
var wait = false
recordingsToRestore.forEach {
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val selection = "${Media._ID} = ?"
val selectionArgs = arrayOf(it.id.toString())
val values = ContentValues().apply {
put(Media.IS_TRASHED, 0)
}
val result = contentResolver.update(uri, values, selection, selectionArgs)
if (result == 0) {
wait = true
copyMoveFilesTo(
fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)),
source = it.path.getParentPath(),
destination = config.saveRecordingsFolder,
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
if (!wait) {
callback(true)
}
}
else -> {
copyMoveFilesTo(
fileDirItems = recordingsToRestore.map { File(it.path).toFileDirItem(this) }.toMutableList() as ArrayList<FileDirItem>,
source = recordingsToRestore.first().path.getParentPath(),
destination = config.saveRecordingsFolder,
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
}
fun BaseSimpleActivity.moveRecordingsToRecycleBin(recordingsToMove: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToMove.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
trashSDK30Uris(fileUris, true, callback)
}
isQPlus() -> {
var wait = false
recordingsToMove.forEach {
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val selection = "${Media._ID} = ?"
val selectionArgs = arrayOf(it.id.toString())
val values = ContentValues().apply {
put(Media.IS_TRASHED, 1)
}
val result = contentResolver.update(uri, values, selection, selectionArgs)
if (result == 0) {
wait = true
copyMoveFilesTo(
fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)),
source = it.path.getParentPath(),
destination = getOrCreateTrashFolder(),
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
if (!wait) {
callback(true)
}
}
else -> {
copyMoveFilesTo(
fileDirItems = recordingsToMove.map { File(it.path).toFileDirItem(this) }.toMutableList() as ArrayList<FileDirItem>,
source = recordingsToMove.first().path.getParentPath(),
destination = getOrCreateTrashFolder(),
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
}
fun BaseSimpleActivity.checkRecycleBinItems() {
if (isQPlus()) {
// System is handling recycle bin on Q+ devices
return
}
if (config.useRecycleBin && config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) {
config.lastRecycleBinCheck = System.currentTimeMillis()
ensureBackgroundThread {
try {
val recordingsToRemove = getLegacyRecordings(trashed = true).filter { it.timestamp < System.currentTimeMillis() - MONTH_SECONDS * 1000L }
if (recordingsToRemove.isNotEmpty()) {
deleteRecordings(recordingsToRemove) {}
}
} catch (e: Exception) {
}
}
}
}
fun BaseSimpleActivity.emptyTheRecycleBin() {
deleteRecordings(getAllRecordings(trashed = true)) {}
}

View File

@ -1,20 +1,28 @@
package com.simplemobiletools.voicerecorder.extensions
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import com.simplemobiletools.commons.extensions.internalStoragePath
import com.simplemobiletools.commons.helpers.isQPlus
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.helpers.Config
import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
import com.simplemobiletools.voicerecorder.helpers.TOGGLE_WIDGET_UI
import com.simplemobiletools.voicerecorder.helpers.*
import com.simplemobiletools.voicerecorder.models.Recording
import java.io.File
import kotlin.math.roundToLong
val Context.config: Config get() = Config.newInstance(applicationContext)
@ -51,3 +59,182 @@ fun Context.getDefaultRecordingsRelativePath(): String {
getString(R.string.app_name)
}
}
@SuppressLint("InlinedApi")
fun Context.getNewMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val projection = arrayOf(
Media._ID,
Media.DISPLAY_NAME,
Media.DATE_ADDED,
Media.DURATION,
Media.SIZE
)
val bundle = Bundle().apply {
putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(Media.DATE_ADDED))
putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${Media.OWNER_PACKAGE_NAME} = ?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(packageName))
if (config.useRecycleBin) {
val trashedValue = if (trashed) MediaStore.MATCH_ONLY else MediaStore.MATCH_EXCLUDE
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, trashedValue)
}
}
queryCursor(uri, projection, bundle, true) { cursor ->
val recording = readRecordingFromCursor(cursor)
recordings.add(recording)
}
return recordings
}
@SuppressLint("InlinedApi")
fun Context.getMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val projection = arrayOf(
Media._ID,
Media.DISPLAY_NAME,
Media.DATE_ADDED,
Media.DURATION,
Media.SIZE
)
var selection = "${Media.OWNER_PACKAGE_NAME} = ?"
var selectionArgs = arrayOf(packageName)
val sortOrder = "${Media.DATE_ADDED} DESC"
if (config.useRecycleBin) {
val trashedValue = if (trashed) 1 else 0
selection += " AND ${Media.IS_TRASHED} = ?"
selectionArgs = selectionArgs.plus(trashedValue.toString())
}
queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor ->
val recording = readRecordingFromCursor(cursor)
recordings.add(recording)
}
return recordings
}
fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val folder = if (trashed) {
trashFolder
} else {
config.saveRecordingsFolder
}
val files = File(folder).listFiles() ?: return recordings
files.filter { it.isAudioFast() }.forEach {
val id = it.hashCode()
val title = it.name
val path = it.absolutePath
val timestamp = (it.lastModified() / 1000).toInt()
val duration = getDuration(it.absolutePath) ?: 0
val size = it.length().toInt()
val recording = Recording(id, title, path, timestamp, duration, size)
recordings.add(recording)
}
return recordings
}
fun Context.getSAFRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val folder = if (trashed) {
trashFolder
} else {
config.saveRecordingsFolder
}
val files = getDocumentSdk30(folder)?.listFiles() ?: return recordings
files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach {
val id = it.hashCode()
val title = it.name!!
val path = it.uri.toString()
val timestamp = (it.lastModified() / 1000).toInt()
val duration = getDurationFromUri(it.uri)
val size = it.length().toInt()
val recording = Recording(id, title, path, timestamp, duration.toInt(), size)
recordings.add(recording)
}
recordings.sortByDescending { it.timestamp }
return recordings
}
fun Context.getAllRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
return when {
isRPlus() -> {
recordings.addAll(getNewMediaStoreRecordings(trashed))
recordings.addAll(getSAFRecordings(trashed))
recordings
}
isQPlus() -> {
recordings.addAll(getMediaStoreRecordings(trashed))
recordings.addAll(getLegacyRecordings(trashed))
recordings
}
else -> {
recordings.addAll(getLegacyRecordings(trashed))
recordings
}
}
}
val Context.trashFolder
get() = "${config.saveRecordingsFolder}/.trash"
fun Context.getOrCreateTrashFolder(): String {
val folder = File(trashFolder)
if (!folder.exists()) {
folder.mkdir()
}
return trashFolder
}
private fun Context.readRecordingFromCursor(cursor: Cursor): Recording {
val id = cursor.getIntValue(Media._ID)
val title = cursor.getStringValue(Media.DISPLAY_NAME)
val timestamp = cursor.getIntValue(Media.DATE_ADDED)
var duration = cursor.getLongValue(Media.DURATION) / 1000
var size = cursor.getIntValue(Media.SIZE)
if (duration == 0L) {
duration = getDurationFromUri(getAudioFileContentUri(id.toLong()))
}
if (size == 0) {
size = getSizeFromUri(id.toLong())
}
return Recording(id, title, "", timestamp, duration.toInt(), size)
}
private fun Context.getSizeFromUri(id: Long): Int {
val recordingUri = getAudioFileContentUri(id)
return try {
contentResolver.openInputStream(recordingUri)?.available() ?: 0
} catch (e: Exception) {
0
}
}
private fun Context.getDurationFromUri(uri: Uri): Long {
return try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(this, uri)
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!
(time.toLong() / 1000.toDouble()).roundToLong()
} catch (e: Exception) {
0L
}
}

View File

@ -1,39 +1,34 @@
package com.simplemobiletools.voicerecorder.fragments
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.MediaMetadataRetriever
import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import android.util.AttributeSet
import android.widget.SeekBar
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
import com.simplemobiletools.voicerecorder.databinding.FragmentPlayerBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import kotlinx.android.synthetic.main.fragment_player.view.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.*
import kotlin.math.roundToLong
import java.util.Stack
import java.util.Timer
import java.util.TimerTask
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
private val FAST_FORWARD_SKIP_MS = 10000
@ -45,18 +40,25 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
private var lastSearchQuery = ""
private var bus: EventBus? = null
private var prevSavePath = ""
private var prevRecycleBinState = context.config.useRecycleBin
private var playOnPreparation = true
private lateinit var binding: FragmentPlayerBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = FragmentPlayerBinding.bind(this)
}
override fun onResume() {
setupColors()
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath) {
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath || context.config.useRecycleBin != prevRecycleBinState) {
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
} else {
getRecordingsAdapter()?.updateTextColor(context.getProperTextColor())
}
storePrevPath()
storePrevState()
}
override fun onDestroy() {
@ -78,27 +80,27 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
setupAdapter(itemsIgnoringSearch)
initMediaPlayer()
setupViews()
storePrevPath()
storePrevState()
}
private fun setupViews() {
play_pause_btn.setOnClickListener {
if (playedRecordingIDs.empty() || player_progressbar.max == 0) {
next_btn.callOnClick()
binding.playPauseBtn.setOnClickListener {
if (playedRecordingIDs.empty() || binding.playerProgressbar.max == 0) {
binding.nextBtn.callOnClick()
} else {
togglePlayPause()
}
}
player_progress_current.setOnClickListener {
binding.playerProgressCurrent.setOnClickListener {
skip(false)
}
player_progress_max.setOnClickListener {
binding.playerProgressMax.setOnClickListener {
skip(true)
}
previous_btn.setOnClickListener {
binding.previousBtn.setOnClickListener {
if (playedRecordingIDs.isEmpty()) {
return@setOnClickListener
}
@ -114,14 +116,14 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
playRecording(prevRecording, true)
}
player_title.setOnLongClickListener {
if (player_title.value.isNotEmpty()) {
context.copyToClipboard(player_title.value)
binding.playerTitle.setOnLongClickListener {
if (binding.playerTitle.value.isNotEmpty()) {
context.copyToClipboard(binding.playerTitle.value)
}
true
}
next_btn.setOnClickListener {
binding.nextBtn.setOnClickListener {
val adapter = getRecordingsAdapter()
if (adapter == null || adapter.recordings.isEmpty()) {
return@setOnClickListener
@ -141,164 +143,49 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
private fun setupAdapter(recordings: ArrayList<Recording>) {
ensureBackgroundThread {
Handler(Looper.getMainLooper()).post {
recordings_fastscroller.beVisibleIf(recordings.isNotEmpty())
recordings_placeholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
if (isQPlus()) {
R.string.no_recordings_found
} else {
R.string.no_recordings_in_folder_found
}
} else {
R.string.no_items_found
}
recordings_placeholder.text = context.getString(stringId)
resetProgress(null)
player?.stop()
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
RecordingsAdapter(context as SimpleActivity, recordings, this, recordings_list) {
playRecording(it as Recording, true)
if (playedRecordingIDs.isEmpty() || playedRecordingIDs.peek() != it.id) {
playedRecordingIDs.push(it.id)
}
}.apply {
recordings_list.adapter = this
}
if (context.areSystemAnimationsEnabled) {
recordings_list.scheduleLayoutAnimation()
}
binding.recordingsFastscroller.beVisibleIf(recordings.isNotEmpty())
binding.recordingsPlaceholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
if (isQPlus()) {
R.string.no_recordings_found
} else {
adapter.updateItems(recordings)
R.string.no_recordings_in_folder_found
}
} else {
com.simplemobiletools.commons.R.string.no_items_found
}
binding.recordingsPlaceholder.text = context.getString(stringId)
resetProgress(null)
player?.stop()
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
RecordingsAdapter(context as SimpleActivity, recordings, this, binding.recordingsList) {
playRecording(it as Recording, true)
if (playedRecordingIDs.isEmpty() || playedRecordingIDs.peek() != it.id) {
playedRecordingIDs.push(it.id)
}
}.apply {
binding.recordingsList.adapter = this
}
if (context.areSystemAnimationsEnabled) {
binding.recordingsList.scheduleLayoutAnimation()
}
} else {
adapter.updateItems(recordings)
}
}
private fun getRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
return when {
isRPlus() -> {
recordings.addAll(getMediaStoreRecordings())
recordings.addAll(getSAFRecordings())
recordings
}
isQPlus() -> {
recordings.addAll(getMediaStoreRecordings())
recordings.addAll(getLegacyRecordings())
recordings
}
else -> {
recordings.addAll(getLegacyRecordings())
recordings
}
}.apply {
return context.getAllRecordings().apply {
sortByDescending { it.timestamp }
}
}
@SuppressLint("InlinedApi")
private fun getMediaStoreRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val projection = arrayOf(
Media._ID,
Media.DISPLAY_NAME,
Media.DATE_ADDED,
Media.DURATION,
Media.SIZE
)
val selection = "${Media.OWNER_PACKAGE_NAME} = ?"
val selectionArgs = arrayOf(context.packageName)
val sortOrder = "${Media.DATE_ADDED} DESC"
context.queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor ->
val id = cursor.getIntValue(Media._ID)
val title = cursor.getStringValue(Media.DISPLAY_NAME)
val timestamp = cursor.getIntValue(Media.DATE_ADDED)
var duration = cursor.getLongValue(Media.DURATION) / 1000
var size = cursor.getIntValue(Media.SIZE)
if (duration == 0L) {
duration = getDurationFromUri(getAudioFileContentUri(id.toLong()))
}
if (size == 0) {
size = getSizeFromUri(id.toLong())
}
val recording = Recording(id, title, "", timestamp, duration.toInt(), size)
recordings.add(recording)
}
return recordings
}
private fun getLegacyRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val files = File(context.config.saveRecordingsFolder).listFiles() ?: return recordings
files.filter { it.isAudioFast() }.forEach {
val id = it.hashCode()
val title = it.name
val path = it.absolutePath
val timestamp = (it.lastModified() / 1000).toInt()
val duration = context.getDuration(it.absolutePath) ?: 0
val size = it.length().toInt()
val recording = Recording(id, title, path, timestamp, duration, size)
recordings.add(recording)
}
return recordings
}
private fun getSAFRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val files = context.getDocumentSdk30(context.config.saveRecordingsFolder)?.listFiles() ?: return recordings
files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach {
val id = it.hashCode()
val title = it.name!!
val path = it.uri.toString()
val timestamp = (it.lastModified() / 1000).toInt()
val duration = getDurationFromUri(it.uri)
val size = it.length().toInt()
val recording = Recording(id, title, path, timestamp, duration.toInt(), size)
recordings.add(recording)
}
recordings.sortByDescending { it.timestamp }
return recordings
}
private fun getDurationFromUri(uri: Uri): Long {
return try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, uri)
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!
(time.toLong() / 1000.toDouble()).roundToLong()
} catch (e: Exception) {
0L
}
}
private fun getSizeFromUri(id: Long): Int {
val recordingUri = getAudioFileContentUri(id)
return try {
context.contentResolver.openInputStream(recordingUri)?.available() ?: 0
} catch (e: Exception) {
0
}
}
private fun initMediaPlayer() {
player = MediaPlayer().apply {
setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
@ -306,9 +193,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
setOnCompletionListener {
progressTimer.cancel()
player_progressbar.progress = player_progressbar.max
player_progress_current.text = player_progress_max.text
play_pause_btn.setImageDrawable(getToggleButtonIcon(false))
binding.playerProgressbar.progress = binding.playerProgressbar.max
binding.playerProgressCurrent.text = binding.playerProgressMax.text
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
setOnPreparedListener {
@ -324,7 +211,7 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
override fun playRecording(recording: Recording, playOnPrepared: Boolean) {
resetProgress(recording)
(recordings_list.adapter as RecordingsAdapter).updateCurrentRecording(recording.id)
(binding.recordingsList.adapter as RecordingsAdapter).updateCurrentRecording(recording.id)
playOnPreparation = playOnPrepared
player!!.apply {
@ -336,9 +223,11 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
DocumentsContract.isDocumentUri(context, uri) -> {
setDataSource(context, uri)
}
recording.path.isEmpty() -> {
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
}
else -> {
setDataSource(recording.path)
}
@ -356,12 +245,12 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
}
play_pause_btn.setImageDrawable(getToggleButtonIcon(playOnPreparation))
player_progressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(playOnPreparation))
binding.playerProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser && !playedRecordingIDs.isEmpty()) {
player?.seekTo(progress * 1000)
player_progress_current.text = progress.getFormattedDuration()
binding.playerProgressCurrent.text = progress.getFormattedDuration()
resumePlayback()
}
}
@ -384,22 +273,22 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
if (player != null) {
val progress = Math.round(player!!.currentPosition / 1000.toDouble()).toInt()
updateCurrentProgress(progress)
player_progressbar.progress = progress
binding.playerProgressbar.progress = progress
}
}
}
}
private fun updateCurrentProgress(seconds: Int) {
player_progress_current.text = seconds.getFormattedDuration()
binding.playerProgressCurrent.text = seconds.getFormattedDuration()
}
private fun resetProgress(recording: Recording?) {
updateCurrentProgress(0)
player_progressbar.progress = 0
player_progressbar.max = recording?.duration ?: 0
player_title.text = recording?.title ?: ""
player_progress_max.text = (recording?.duration ?: 0).getFormattedDuration()
binding.playerProgressbar.progress = 0
binding.playerProgressbar.max = recording?.duration ?: 0
binding.playerTitle.text = recording?.title ?: ""
binding.playerProgressMax.text = (recording?.duration ?: 0).getFormattedDuration()
}
fun onSearchTextChanged(text: String) {
@ -418,18 +307,18 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
private fun pausePlayback() {
player?.pause()
play_pause_btn.setImageDrawable(getToggleButtonIcon(false))
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
progressTimer.cancel()
}
private fun resumePlayback() {
player?.start()
play_pause_btn.setImageDrawable(getToggleButtonIcon(true))
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(true))
setupProgressTimer()
}
private fun getToggleButtonIcon(isPlaying: Boolean): Drawable {
val drawable = if (isPlaying) R.drawable.ic_pause_vector else R.drawable.ic_play_vector
val drawable = if (isPlaying) com.simplemobiletools.commons.R.drawable.ic_pause_vector else com.simplemobiletools.commons.R.drawable.ic_play_vector
return resources.getColoredDrawableWithColor(drawable, context.getProperPrimaryColor().getContrastColor())
}
@ -450,24 +339,25 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
private fun getIsPlaying() = player?.isPlaying == true
private fun getRecordingsAdapter() = recordings_list.adapter as? RecordingsAdapter
private fun getRecordingsAdapter() = binding.recordingsList.adapter as? RecordingsAdapter
private fun storePrevPath() {
private fun storePrevState() {
prevSavePath = context!!.config.saveRecordingsFolder
prevRecycleBinState = context.config.useRecycleBin
}
private fun setupColors() {
val properPrimaryColor = context.getProperPrimaryColor()
recordings_fastscroller.updateColors(properPrimaryColor)
context.updateTextColors(player_holder)
binding.recordingsFastscroller.updateColors(properPrimaryColor)
context.updateTextColors(binding.playerHolder)
val textColor = context.getProperTextColor()
arrayListOf(previous_btn, next_btn).forEach {
arrayListOf(binding.previousBtn, binding.nextBtn).forEach {
it.applyColorFilter(textColor)
}
play_pause_btn.background.applyColorFilter(properPrimaryColor)
play_pause_btn.setImageDrawable(getToggleButtonIcon(false))
binding.playPauseBtn.background.applyColorFilter(properPrimaryColor)
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
fun finishActMode() = getRecordingsAdapter()?.finishActMode()
@ -476,4 +366,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
fun recordingCompleted(event: Events.RecordingCompleted) {
refreshRecordings()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
refreshRecordings()
}
}

View File

@ -10,20 +10,26 @@ import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.dialogs.PermissionRequiredDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.databinding.FragmentRecorderBinding
import com.simplemobiletools.voicerecorder.helpers.*
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.services.RecorderService
import kotlinx.android.synthetic.main.fragment_recorder.view.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import java.util.Timer
import java.util.TimerTask
class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet) {
private var status = RECORDING_STOPPED
private var pauseBlinkTimer = Timer()
private var bus: EventBus? = null
private lateinit var binding: FragmentRecorderBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = FragmentRecorderBinding.bind(this)
}
override fun onResume() {
setupColors()
@ -42,22 +48,24 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setupColors()
recorder_visualizer.recreate()
binding.recorderVisualizer.recreate()
bus = EventBus.getDefault()
bus!!.register(this)
updateRecordingDuration(0)
toggle_recording_button.setOnClickListener {
binding.toggleRecordingButton.setOnClickListener {
(context as? BaseSimpleActivity)?.handleNotificationPermission { granted ->
if (granted) {
toggleRecording()
} else {
PermissionRequiredDialog(context as BaseSimpleActivity, R.string.allow_notifications_voice_recorder)
PermissionRequiredDialog(context as BaseSimpleActivity, com.simplemobiletools.commons.R.string.allow_notifications_voice_recorder, {
(context as BaseSimpleActivity).openNotificationSettings()
})
}
}
}
toggle_pause_button.setOnClickListener {
binding.togglePauseButton.setOnClickListener {
Intent(context, RecorderService::class.java).apply {
action = TOGGLE_PAUSE
context.startService(this)
@ -75,26 +83,27 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
private fun setupColors() {
val properPrimaryColor = context.getProperPrimaryColor()
toggle_recording_button.apply {
binding.toggleRecordingButton.apply {
setImageDrawable(getToggleButtonIcon())
background.applyColorFilter(properPrimaryColor)
}
toggle_pause_button.apply {
setImageDrawable(resources.getColoredDrawableWithColor(R.drawable.ic_pause_vector, properPrimaryColor.getContrastColor()))
binding.togglePauseButton.apply {
setImageDrawable(resources.getColoredDrawableWithColor(com.simplemobiletools.commons.R.drawable.ic_pause_vector, properPrimaryColor.getContrastColor()))
background.applyColorFilter(properPrimaryColor)
}
recorder_visualizer.chunkColor = properPrimaryColor
recording_duration.setTextColor(context.getProperTextColor())
binding.recorderVisualizer.chunkColor = properPrimaryColor
binding.recordingDuration.setTextColor(context.getProperTextColor())
}
private fun updateRecordingDuration(duration: Int) {
recording_duration.text = duration.getFormattedDuration()
binding.recordingDuration.text = duration.getFormattedDuration()
}
private fun getToggleButtonIcon(): Drawable {
val drawable = if (status == RECORDING_RUNNING || status == RECORDING_PAUSED) R.drawable.ic_stop_vector else R.drawable.ic_microphone_vector
val drawable =
if (status == RECORDING_RUNNING || status == RECORDING_PAUSED) com.simplemobiletools.commons.R.drawable.ic_stop_vector else com.simplemobiletools.commons.R.drawable.ic_microphone_vector
return resources.getColoredDrawableWithColor(drawable, context.getProperPrimaryColor().getContrastColor())
}
@ -105,12 +114,12 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
RECORDING_RUNNING
}
toggle_recording_button.setImageDrawable(getToggleButtonIcon())
binding.toggleRecordingButton.setImageDrawable(getToggleButtonIcon())
if (status == RECORDING_RUNNING) {
startRecording()
} else {
toggle_pause_button.beGone()
binding.togglePauseButton.beGone()
stopRecording()
}
}
@ -119,7 +128,7 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
Intent(context, RecorderService::class.java).apply {
context.startService(this)
}
recorder_visualizer.recreate()
binding.recorderVisualizer.recreate()
}
private fun stopRecording() {
@ -133,15 +142,15 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
if (status == RECORDING_PAUSED) {
// update just the alpha so that it will always be clickable
Handler(Looper.getMainLooper()).post {
toggle_pause_button.alpha = if (toggle_pause_button.alpha == 0f) 1f else 0f
binding.togglePauseButton.alpha = if (binding.togglePauseButton.alpha == 0f) 1f else 0f
}
}
}
}
private fun refreshView() {
toggle_recording_button.setImageDrawable(getToggleButtonIcon())
toggle_pause_button.beVisibleIf(status != RECORDING_STOPPED && isNougatPlus())
binding.toggleRecordingButton.setImageDrawable(getToggleButtonIcon())
binding.togglePauseButton.beVisibleIf(status != RECORDING_STOPPED && isNougatPlus())
pauseBlinkTimer.cancel()
if (status == RECORDING_PAUSED) {
@ -150,7 +159,7 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
}
if (status == RECORDING_RUNNING) {
toggle_pause_button.alpha = 1f
binding.togglePauseButton.alpha = 1f
}
}
@ -169,7 +178,7 @@ class RecorderFragment(context: Context, attributeSet: AttributeSet) : MyViewPag
fun gotAmplitudeEvent(event: Events.RecordingAmplitude) {
val amplitude = event.amplitude
if (status == RECORDING_RUNNING) {
recorder_visualizer.update(amplitude)
binding.recorderVisualizer.update(amplitude)
}
}
}

View File

@ -0,0 +1,121 @@
package com.simplemobiletools.voicerecorder.fragments
import android.content.Context
import android.util.AttributeSet
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.adapters.TrashAdapter
import com.simplemobiletools.voicerecorder.databinding.FragmentTrashBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class TrashFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
private var itemsIgnoringSearch = ArrayList<Recording>()
private var lastSearchQuery = ""
private var bus: EventBus? = null
private var prevSavePath = ""
private lateinit var binding: FragmentTrashBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = FragmentTrashBinding.bind(this)
}
override fun onResume() {
setupColors()
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath) {
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
} else {
getRecordingsAdapter()?.updateTextColor(context.getProperTextColor())
}
storePrevPath()
}
override fun onDestroy() {
bus?.unregister(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
bus = EventBus.getDefault()
bus!!.register(this)
setupColors()
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
storePrevPath()
}
override fun refreshRecordings() {
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
}
override fun playRecording(recording: Recording, playOnPrepared: Boolean) {}
private fun setupAdapter(recordings: ArrayList<Recording>) {
binding.trashFastscroller.beVisibleIf(recordings.isNotEmpty())
binding.trashPlaceholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
com.simplemobiletools.commons.R.string.recycle_bin_empty
} else {
com.simplemobiletools.commons.R.string.no_items_found
}
binding.trashPlaceholder.text = context.getString(stringId)
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
TrashAdapter(context as SimpleActivity, recordings, this, binding.trashList).apply {
binding.trashList.adapter = this
}
if (context.areSystemAnimationsEnabled) {
binding.trashList.scheduleLayoutAnimation()
}
} else {
adapter.updateItems(recordings)
}
}
private fun getRecordings(): ArrayList<Recording> {
return context.getAllRecordings(trashed = true).apply {
sortByDescending { it.timestamp }
}
}
fun onSearchTextChanged(text: String) {
lastSearchQuery = text
val filtered = itemsIgnoringSearch.filter { it.title.contains(text, true) }.toMutableList() as ArrayList<Recording>
setupAdapter(filtered)
}
private fun getRecordingsAdapter() = binding.trashList.adapter as? TrashAdapter
private fun storePrevPath() {
prevSavePath = context!!.config.saveRecordingsFolder
}
private fun setupColors() {
val properPrimaryColor = context.getProperPrimaryColor()
binding.trashFastscroller.updateColors(properPrimaryColor)
context.updateTextColors(binding.trashHolder)
}
fun finishActMode() = getRecordingsAdapter()?.finishActMode()
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
refreshRecordings()
}
}

View File

@ -72,4 +72,12 @@ class Config(context: Context) : BaseConfig(context) {
EXTENSION_OGG -> MediaRecorder.AudioEncoder.OPUS
else -> MediaRecorder.AudioEncoder.AAC
}
var useRecycleBin: Boolean
get() = prefs.getBoolean(USE_RECYCLE_BIN, true)
set(useRecycleBin) = prefs.edit().putBoolean(USE_RECYCLE_BIN, useRecycleBin).apply()
var lastRecycleBinCheck: Long
get() = prefs.getLong(LAST_RECYCLE_BIN_CHECK, 0L)
set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck).apply()
}

View File

@ -36,6 +36,8 @@ const val EXTENSION = "extension"
const val AUDIO_SOURCE = "audio_source"
const val BITRATE = "bitrate"
const val RECORD_AFTER_LAUNCH = "record_after_launch"
const val USE_RECYCLE_BIN = "use_recycle_bin"
const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check"
@SuppressLint("InlinedApi")
fun getAudioFileContentUri(id: Long): Uri {

View File

@ -57,7 +57,7 @@ class MyWidgetRecordDisplayProvider : AppWidgetProvider() {
}
private fun getColoredIcon(context: Context, color: Int, alpha: Int): Bitmap {
val drawable = context.resources.getColoredDrawableWithColor(R.drawable.ic_microphone_vector, color, alpha)
val drawable = context.resources.getColoredDrawableWithColor(com.simplemobiletools.commons.R.drawable.ic_microphone_vector, color, alpha)
return context.drawableToBitmap(drawable)
}
}

View File

@ -7,5 +7,6 @@ class Events {
class RecordingStatus internal constructor(val status: Int)
class RecordingAmplitude internal constructor(val amplitude: Int)
class RecordingCompleted internal constructor()
class RecordingTrashUpdated internal constructor()
class RecordingSaved internal constructor(val uri: Uri?)
}

View File

@ -195,7 +195,7 @@ class RecorderService : Service() {
val newUri = contentResolver.insert(audioCollection, newSongDetails)
if (newUri == null) {
toast(R.string.unknown_error_occurred)
toast(com.simplemobiletools.commons.R.string.unknown_error_occurred)
return
}
@ -257,7 +257,7 @@ class RecorderService : Service() {
}
var priority = Notification.PRIORITY_DEFAULT
var icon = R.drawable.ic_microphone_vector
var icon = com.simplemobiletools.commons.R.drawable.ic_microphone_vector
var title = label
var visibility = NotificationCompat.VISIBILITY_PUBLIC
var text = getString(R.string.recording)
@ -273,7 +273,7 @@ class RecorderService : Service() {
visibility = NotificationCompat.VISIBILITY_SECRET
}
val builder = NotificationCompat.Builder(this)
val builder = NotificationCompat.Builder(this, channelId)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(icon)
@ -283,7 +283,6 @@ class RecorderService : Service() {
.setSound(null)
.setOngoing(true)
.setAutoCancel(true)
.setChannelId(channelId)
return builder.build()
}

View File

@ -268,6 +268,55 @@
tools:text="128 kbps" />
</RelativeLayout>
<include
android:id="@+id/settings_general_settings_divider"
layout="@layout/divider" />
<TextView
android:id="@+id/settings_recycle_bin_label"
style="@style/SettingsSectionLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/recycle_bin" />
<RelativeLayout
android:id="@+id/settings_use_recycle_bin_holder"
style="@style/SettingsHolderCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/settings_use_recycle_bin"
style="@style/SettingsCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/move_items_into_recycle_bin" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/settings_empty_recycle_bin_holder"
style="@style/SettingsHolderTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_label"
style="@style/SettingsTextLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_recycle_bin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_size"
style="@style/SettingsTextValueStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/settings_empty_recycle_bin_label"
tools:text="0 B" />
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/delete_remember_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/big_margin"
android:paddingTop="@dimen/big_margin"
android:paddingRight="@dimen/big_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/delete_remember_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingBottom="@dimen/activity_margin"
android:text="@string/delete_recordings_confirmation"
android:textSize="@dimen/bigger_text_size" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/skip_the_recycle_bin_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/delete_remember_title"
android:text="@string/skip_the_recycle_bin" />
</RelativeLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.voicerecorder.fragments.TrashFragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/trash_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/trash_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.8"
android:gravity="center"
android:lineSpacingExtra="@dimen/small_margin"
android:padding="@dimen/activity_margin"
android:text="@string/recycle_bin_empty"
android:textSize="@dimen/bigger_text_size"
android:textStyle="italic"
android:visibility="visible" />
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/trash_fastscroller"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent">
<com.simplemobiletools.commons.views.MyRecyclerView
android:id="@+id/trash_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:layoutAnimation="@anim/layout_animation"
android:scrollbars="none"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager" />
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</com.simplemobiletools.voicerecorder.fragments.TrashFragment>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource,AlwaysShowAction">
<item
android:id="@+id/cab_delete"
android:icon="@drawable/ic_delete_vector"
android:showAsAction="always"
android:title="@string/delete" />
<item
android:id="@+id/cab_restore"
android:showAsAction="never"
android:title="@string/restore_selected_files"
app:showAsAction="never" />
<item
android:id="@+id/cab_select_all"
android:icon="@drawable/ic_select_all_vector"
android:title="@string/select_all"
app:showAsAction="ifRoom" />
</menu>

View File

@ -21,17 +21,17 @@
<!-- Settings -->
<string name="try_hiding_notification">Опитайте да скриете известието за запис</string>
<string name="save_recordings_in">Запазете записите в</string>
<string name="audio_source">Audio source</string>
<string name="audio_source">Аудио източник</string>
<string name="bitrate">Битрейт</string>
<string name="record_after_launch">Започнете да записвате автоматично след стартиране на приложението</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Camera</string>
<string name="audio_source_default">Android default</string>
<string name="audio_source_unprocessed">Unprocessed</string>
<string name="audio_source_microphone">Microphone</string>
<string name="audio_source_voice_recognition">Voice recognition</string>
<string name="audio_source_voice_communication">Voice communication</string>
<string name="audio_source_voice_performance">Voice performance</string>
<string name="audio_source_camcorder">Камера</string>
<string name="audio_source_default">Android по подразбиране</string>
<string name="audio_source_unprocessed">Необработени</string>
<string name="audio_source_microphone">Микрофон</string>
<string name="audio_source_voice_recognition">Гласово разпознаване</string>
<string name="audio_source_voice_communication">Гласова комуникация</string>
<string name="audio_source_voice_performance">Гласово изпълнение</string>
<!-- FAQ -->
<string name="faq_1_title">Мога ли да скрия иконата за уведомяване по време на запис\?</string>
<string name="faq_1_text">Е, зависи. Докато използвате устройството си, вече не е възможно да скриете напълно известията на подобни приложения. Ако поставите отметка на съответния елемент от настройката, приложението ще направи всичко възможно да го скрие. Все пак можете да го скриете на заключения екран, ако забраните показването на чувствителни известия в настройките на устройството си.</string>
@ -39,4 +39,4 @@
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>
</resources>

View File

@ -4,10 +4,9 @@
<string name="app_launcher_name">Grabadora de voz</string>
<string name="recording_saved_successfully">Grabación guardada con éxito</string>
<string name="recording">Grabando</string>
<string name="no_recordings_found">No se han encontrado grabaciones creadas por esta aplicación
<string name="no_recordings_found">No se han encontrado grabaciones
\nse han encontrado</string>
<string name="no_recordings_in_folder_found">No se han encontrado grabaciones
\nen la carpeta seleccionada</string>
<string name="no_recordings_in_folder_found">No se han encontrado grabaciones\nen la carpeta seleccionada</string>
<string name="paused">En pausa</string>
<string name="recorder">Grabación</string>
<string name="player">Reproductor</string>

View File

@ -9,7 +9,7 @@
<string name="no_recordings_in_folder_found">Valitud kaustas ei leidu
\nühtegi salvestust</string>
<string name="paused">Peatatud</string>
<string name="recorder">Salvesti</string>
<string name="recorder">Salvestaja</string>
<string name="player">Mängija</string>
<!-- Confirmation dialog -->
<!-- Are you sure you want to delete 5 recordings? -->
@ -26,7 +26,7 @@
<string name="record_after_launch">Rakenduse käivitamisel alusta kohe salvestamist</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Kaamera</string>
<string name="audio_source_default">Vaikimisi Androidi audio</string>
<string name="audio_source_default">Vaikimisi Androidi helisisend</string>
<string name="audio_source_unprocessed">Töötlemata</string>
<string name="audio_source_microphone">Mikrofon</string>
<string name="audio_source_voice_recognition">Häältuvastus</string>

View File

@ -2,7 +2,7 @@
<resources>
<string name="app_name">Enregistreur vocal simple</string>
<string name="app_launcher_name">Enregistreur vocal</string>
<string name="recording_saved_successfully">Enregistrement sauvegardé avec succès</string>
<string name="recording_saved_successfully">Sauvegarde de l\'enregistrement réussie</string>
<string name="recording">Enregistrement</string>
<string name="no_recordings_found">Aucun enregistrement créé par cette application
\nna été trouvé</string>
@ -26,7 +26,7 @@
<string name="bitrate">Débit binaire</string>
<string name="record_after_launch">Démarrer lenregistrement automatiquement après le lancement de lapplication</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Caméra</string>
<string name="audio_source_camcorder">Objectif vidéo</string>
<string name="audio_source_default">Par défaut Android</string>
<string name="audio_source_unprocessed">Non traitée</string>
<string name="audio_source_microphone">Microphone</string>

View File

@ -4,13 +4,13 @@
<string name="app_launcher_name">Voicerecorder</string>
<string name="recording_saved_successfully">Opname is opgeslagen</string>
<string name="recording">Opnemen</string>
<string name="no_recordings_found">Geen opnames gemaakt door
\ndeze app gevonden</string>
<string name="no_recordings_found">Geen opnames gevonden
\ndie zijn gemaakt door deze app</string>
<string name="no_recordings_in_folder_found">Geen opnames gevonden
\nin de gekozen map</string>
<string name="paused">Gepauzeerd</string>
<string name="recorder">Recorder</string>
<string name="player">Speler</string>
<string name="recorder">Opnemen</string>
<string name="player">Afspelen</string>
<!-- Confirmation dialog -->
<string name="delete_recordings_confirmation">%s verwijderen\?</string>
<!-- Are you sure you want to delete 5 recordings? -->
@ -19,19 +19,19 @@
<item quantity="other">%d opnames</item>
</plurals>
<!-- Settings -->
<string name="try_hiding_notification">Tijdens de opname proberen de notificatie te verbergen</string>
<string name="try_hiding_notification">Tijdens opnames proberen de notificatie te verbergen</string>
<string name="save_recordings_in">Opnames opslaan in</string>
<string name="audio_source">Audio source</string>
<string name="audio_source">Geluidsbron</string>
<string name="bitrate">Bitrate</string>
<string name="record_after_launch">Bij het starten van de app direct een opname beginnen</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Camera</string>
<string name="audio_source_default">Android default</string>
<string name="audio_source_unprocessed">Unprocessed</string>
<string name="audio_source_microphone">Microphone</string>
<string name="audio_source_voice_recognition">Voice recognition</string>
<string name="audio_source_voice_communication">Voice communication</string>
<string name="audio_source_voice_performance">Voice performance</string>
<string name="audio_source_default">Android-standaard</string>
<string name="audio_source_unprocessed">Onverwerkt</string>
<string name="audio_source_microphone">Microfoon</string>
<string name="audio_source_voice_recognition">Spraakherkenning</string>
<string name="audio_source_voice_communication">Gesproken communicatie</string>
<string name="audio_source_voice_performance">Optreden</string>
<!-- FAQ -->
<string name="faq_1_title">Kan ik de notificatie tijdens het opnemen verbergen\?</string>
<string name="faq_1_text">Dat ligt eraan. Tijdens het gebruik is het niet langer mogelijk in Android om meldingen van dit soort apps volledig te verbergen. De app bevat wel een instelling waarmee er toch zal worden geprobeerd om dit te bewerkstelligen. In de schermvergrendeling kan de melding wél worden verborgen door het tonen van gevoelige notificaties in de systeeminstellingen uit te schakelen.</string>
@ -39,4 +39,4 @@
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>
</resources>

View File

@ -39,4 +39,4 @@
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>
</resources>

View File

@ -9,8 +9,8 @@
<string name="no_recordings_in_folder_found">В обраній теці, не знайдено
\nжодного запису</string>
<string name="paused">Пауза</string>
<string name="recorder">Recorder</string>
<string name="player">Грач</string>
<string name="recorder">Диктофон</string>
<string name="player">Програвач</string>
<!-- Confirmation dialog -->
<string name="delete_recordings_confirmation">Видалити %s\?</string>
<!-- Are you sure you want to delete 5 recordings? -->
@ -24,7 +24,7 @@
<string name="try_hiding_notification">Намагатися приховати сповіщення про запис</string>
<string name="save_recordings_in">Місце зберігання записів</string>
<string name="audio_source">Джерело звуку</string>
<string name="bitrate">Bitrate</string>
<string name="bitrate">Бітова швидкість</string>
<string name="record_after_launch">Автоматично розпочинати запис після запуску програми</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Камера</string>
@ -41,4 +41,4 @@
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>
</resources>

View File

@ -20,17 +20,17 @@
<!-- Settings -->
<string name="try_hiding_notification">儘量隱藏錄音通知</string>
<string name="save_recordings_in">錄音儲存至</string>
<string name="audio_source">Audio source</string>
<string name="audio_source">音訊來源</string>
<string name="bitrate">位元率</string>
<string name="record_after_launch">啟動 app 後自動開始錄音</string>
<!-- Settings Audio source selection -->
<string name="audio_source_camcorder">Camera</string>
<string name="audio_source_default">Android default</string>
<string name="audio_source_unprocessed">Unprocessed</string>
<string name="audio_source_microphone">Microphone</string>
<string name="audio_source_voice_recognition">Voice recognition</string>
<string name="audio_source_voice_communication">Voice communication</string>
<string name="audio_source_voice_performance">Voice performance</string>
<string name="audio_source_camcorder">相機</string>
<string name="audio_source_default">Android 預設</string>
<string name="audio_source_unprocessed">未經處理</string>
<string name="audio_source_microphone">麥克風</string>
<string name="audio_source_voice_recognition">語音辨識</string>
<string name="audio_source_voice_communication">語音通訊</string>
<string name="audio_source_voice_performance">聲音表現</string>
<!-- FAQ -->
<string name="faq_1_title">有辦法隱藏錄音時出現的通知圖示嗎?</string>
<string name="faq_1_text">嗯……看情況。當您正在使用裝置時,沒辦法完全隱藏這類 app 的通知。若您有開啟相應的選項,本 app 會盡其所能來隱藏。不過鎖定畫面上的可以隱藏,只要在裝置的設定中停用「敏感通知」即可。</string>
@ -38,4 +38,4 @@
Haven't found some strings? There's more at
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
-->
</resources>
</resources>

View File

@ -1,29 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

5
build.gradle.kts Normal file
View File

@ -0,0 +1,5 @@
plugins {
alias(libs.plugins.android).apply(false)
alias(libs.plugins.kotlinAndroid).apply(false)
alias(libs.plugins.ksp).apply(false)
}

View File

@ -0,0 +1,2 @@
* Added a Recycle bin
* Added some stability, UI and translation improvements

View File

@ -0,0 +1 @@
* Added some stability, UI and translation improvements

View File

@ -0,0 +1 @@
* Added some stability, UI and translation improvements

View File

@ -0,0 +1,2 @@
* Fixed a glitch with inability to change save folder
* Added some stability, UI and translation improvements

View File

@ -1 +1 @@
Grabadora de Voz Sencilla
Grabadora de Voz Simple

View File

@ -1 +1 @@
Salvestage selle lihtsa helisalvesti abil hõlpsalt heli või looge memosid
Salvesta selle lihtsa helisalvesti abil hõlpsalt heli või koosta audiomemosid

View File

@ -1 +1 @@
Simpel röstinspelare
Simple Voice Recorder

View File

@ -0,0 +1,25 @@
🎙 您是否曾希望能記住對方所說的話?您的願望現在成真了!透過這款簡易語音錄音器,您可以輕鬆地錄製高品質的音訊和其他語音備忘錄!
這款錄音應用程式讓您可以錄製不同的音訊。您可以將這款音訊錄音器和語音錄音器當作音樂錄音室使用。這款錄音應用程式讓您自由地錄製周遭發生的每一件事情,並稍後進行視覺化。
這款免費應用程式直截了當,不含您不會使用的花俏功能。只有您和語音錄音器或音訊錄音器。它以清晰直觀的用戶界面呈現目前的聲音音量,您可以在其中享受很多樂趣。提供極為直覺且整潔的用戶界面,不太容易出錯。您還可以從這款錄音應用程式中製作音訊筆記或保存語音備忘錄以供稍後收聽。這款錄音應用程式還提供了一個獨特且簡單的音樂錄音室功能,您可以將其作為音訊錄音器和音樂錄音器。
此錄音應用程式還提供了一個實用的音訊錄音器和語音錄音器播放器,讓您可以在錄音應用程式中快速收聽您的錄音,也可以重新命名或刪除它們。您可以自定顯示的日期和時間格式,以提供更好的用戶體驗。
您可以選擇在錄音期間隱藏頂部通知,以保護您的隱私。它還提供了一個實用且可自定義的小工具,用於快速錄音。這款語音錄音器讓您可以自由地按照自己的需求使用此應用程式。
它預設採用物料設計和暗黑主題,為簡單使用提供出色的用戶體驗。相比其他應用程式,由於無需網路連接,它具有更高的隱私、安全性和穩定性。
不包含廣告或不必要的權限。它是完全開源的,提供可自定義的顏色。
在此處查看完整的 Simple Tools 套件:
https://www.simplemobiletools.com
Facebook
https://www.facebook.com/simplemobiletools
Reddit
https://www.reddit.com/r/SimpleMobileTools
Telegram
https://t.me/SimpleMobileTools

View File

@ -0,0 +1 @@
輕鬆使用這款簡易語音錄音器,錄製任何音訊或創建備忘錄

View File

@ -0,0 +1 @@
簡易語音錄音器

View File

@ -19,3 +19,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.nonTransitiveRClass=true

61
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,61 @@
[versions]
#jetbrains
kotlin = "1.9.0"
#KSP
ksp = "1.9.0-1.0.12"
#AndroidX
androidx-constraintlayout = "2.1.4"
androidx-documentfile = "1.0.1"
androidx-swiperefreshlayout = "1.1.0"
#Eventbus
eventbus = "3.3.1"
#Room
room = "2.6.0-alpha02"
#Simple tools
simple-commons = "8e7dd4508e"
#AudioRecordView
audiorecordview = "1.0.4"
#TAndroidLame
tandroidlame = "1.1"
#AutofitTextView
autofittextview = "0.2.1"
#Gradle
gradlePlugins-agp = "8.1.1"
#build
app-build-compileSDKVersion = "34"
app-build-targetSDK = "34"
app-build-minimumSDK = "23"
app-build-javaVersion = "VERSION_17"
app-build-kotlinJVMTarget = "17"
#versioning
app-version-appId = "com.simplemobiletools.voicerecorder"
app-version-versionCode = "41"
app-version-versionName = "5.12.3"
[libraries]
#AndroidX
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" }
#Room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
#Simple Mobile Tools
simple-tools-commons = { module = "com.github.SimpleMobileTools:Simple-Commons", version.ref = "simple-commons" }
#EventBus
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
#AudioRecordView
audiorecordview = { module = "com.github.Armen101:AudioRecordView", version.ref = "audiorecordview" }
#TAndroidLame
tandroidlame = { module = "com.github.naman14:TAndroidLame", version.ref = "tandroidlame" }
#AutofitTextView
autofittextview = { module = "me.grantland:autofittextview", version.ref = "autofittextview" }
[bundles]
room = [
"androidx-room-ktx",
"androidx-room-runtime",
]
[plugins]
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip

View File

@ -1 +0,0 @@
include ':app'

16
settings.gradle.kts Normal file
View File

@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { setUrl("https://jitpack.io") }
}
}
include(":app")