diff --git a/.github/workflows/crowdin-badge.yml b/.github/workflows/crowdin-badge.yml new file mode 100644 index 0000000..3adff63 --- /dev/null +++ b/.github/workflows/crowdin-badge.yml @@ -0,0 +1,54 @@ +name: Update Crowdin Badge + +on: + workflow_dispatch: + schedule: + - cron: "0 * * * *" + +jobs: + update-badge: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Repo auschecken + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT_TOKEN }} + + - name: Crowdin Fortschritt holen + run: | + curl -s -H "Authorization: Bearer ${{ secrets.CROWDIN_API_TOKEN }}" \ + https://api.crowdin.com/api/v2/projects/883718/languages/progress \ + > progress.json + + - name: Show progress.json + run: cat progress.json + + - name: Prozent berechnen + Badge erstellen + run: | + percent=$(jq '[.data[].data.translationProgress] | add / length' progress.json) + percent_rounded=$(printf "%.0f" $percent) + if [ "$percent_rounded" -lt 50 ]; then + color="red" + elif [ "$percent_rounded" -lt 80 ]; then + color="yellow" + else + color="brightgreen" + fi + cat < badge.json + { + "schemaVersion": 1, + "label": "translation", + "message": "${percent_rounded}%", + "color": "$color" + } + EOF + + - name: Commit & Push + run: | + git config user.name "github-actions" + git config user.email "actions@github.com" + git add badge.json + git commit -m "Update badge" || echo "No changes" + git push diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..6a45a26 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/.kotlin/errors/errors-1775316432688.log b/.kotlin/errors/errors-1775316432688.log new file mode 100644 index 0000000..2379c27 --- /dev/null +++ b/.kotlin/errors/errors-1775316432688.log @@ -0,0 +1,216 @@ +kotlin version: 2.2.10 +error message: Incremental compilation failed: D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\classpath-snapshot\shrunk-classpath-snapshot.bin (Das System kann die angegebene Datei nicht finden) +java.io.FileNotFoundException: D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\classpath-snapshot\shrunk-classpath-snapshot.bin (Das System kann die angegebene Datei nicht finden) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(Unknown Source) + at java.base/java.io.FileInputStream.(Unknown Source) + at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.loadFromFile(externalizers.kt:184) + at org.jetbrains.kotlin.incremental.snapshots.LazyClasspathSnapshot.getSavedShrunkClasspathAgainstPreviousLookups(LazyClasspathSnapshot.kt:86) + at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:267) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally$lambda$10$compile(IncrementalCompilerRunner.kt:254) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:272) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:124) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:679) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:93) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1806) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) + at java.base/java.lang.reflect.Method.invoke(Unknown Source) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) + at java.base/java.lang.Thread.run(Unknown Source) + + +error message: Daemon compilation failed: null +java.lang.Exception + at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) + at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin: internal-name-to-source.tab + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:152) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:679) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:93) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1806) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) + at java.base/java.lang.reflect.Method.invoke(Unknown Source) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) + at java.base/java.lang.Thread.run(Unknown Source) +Caused by: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin: internal-name-to-source.tab + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + ... 22 more + Suppressed: java.lang.IllegalStateException: Storage for [D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin\internal-name-to-source.tab] is already registered + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + ... 24 more + Suppressed: java.lang.Exception: Storage[D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin\internal-name-to-source.tab] registration stack trace + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:133) + ... 18 more + Suppressed: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups: id-to-file.tab + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155) + ... 23 more + Suppressed: java.lang.IllegalStateException: Storage for [D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups\id-to-file.tab] is already registered + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + ... 25 more + Suppressed: java.lang.Exception: Storage[D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups\id-to-file.tab] registration stack trace + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:133) + ... 18 more + + diff --git a/.kotlin/errors/errors-1775316751903.log b/.kotlin/errors/errors-1775316751903.log new file mode 100644 index 0000000..109424e --- /dev/null +++ b/.kotlin/errors/errors-1775316751903.log @@ -0,0 +1,182 @@ +kotlin version: 2.2.10 +error message: Daemon compilation failed: null +java.lang.Exception + at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69) + at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:240) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin: class-attributes.tab + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:133) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:679) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:93) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1806) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) + at java.base/java.lang.reflect.Method.invoke(Unknown Source) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source) + at java.base/java.security.AccessController.doPrivileged(Unknown Source) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) + at java.base/java.lang.Thread.run(Unknown Source) +Caused by: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin: class-attributes.tab + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + ... 22 more + Suppressed: java.lang.IllegalStateException: Storage for [D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin\class-attributes.tab] is already registered + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + ... 24 more + Suppressed: java.lang.Exception: Storage[D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\jvm\kotlin\class-attributes.tab] registration stack trace + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:152) + ... 18 more + Suppressed: java.lang.Exception: Could not close incremental caches in D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups: lookups.tab + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155) + ... 23 more + Suppressed: java.lang.IllegalStateException: Storage for [D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups\lookups.tab] is already registered + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + ... 25 more + Suppressed: java.lang.Exception: Storage[D:\Users\TestUser\AndroidStudioProjects\Phony\app\build\kotlin\compileDebugKotlin\cacheable\caches-jvm\lookups\lookups.tab] registration stack trace + at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437) + at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72) + at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46) + at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59) + at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108) + at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179) + at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136) + at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87) + at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53) + at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155) + at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205) + at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55) + at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:298) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:152) + ... 18 more + + diff --git a/.kotlin/errors/errors-1776179006183.log b/.kotlin/errors/errors-1776179006183.log new file mode 100644 index 0000000..75d8262 --- /dev/null +++ b/.kotlin/errors/errors-1776179006183.log @@ -0,0 +1,45 @@ +kotlin version: 2.2.10 +error message: Failed connecting to the daemon in 4 retries + +error message: Daemon compilation failed: Could not connect to Kotlin compile daemon +java.lang.RuntimeException: Could not connect to Kotlin compile daemon + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) + + diff --git a/LICENSE b/LICENSE index e69de29..3877ae0 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 4e4f15e..4ae2f40 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@

[![GitHub stars](https://img.shields.io/github/stars/DDOneApps/FakeCall?style=for-the-badge)](https://github.com/DDOneApps/FakeCall/stargazers) [![GitHub forks](https://img.shields.io/github/forks/DDOneApps/FakeCall?style=for-the-badge)](https://github.com/DDOneApps/FakeCall/network) [![GitHub issues](https://img.shields.io/github/issues/DDOneApps/FakeCall?style=for-the-badge)](https://github.com/DDOneApps/FakeCall/issues) -[![Downloads](https://img.shields.io/github/downloads/DDOneApps/FakeCall/total?color=green&style=for-the-badge)](https://github.com/DDOneApps/FakeCall/releases/latest) - -[![GitHub license](https://img.shields.io/badge/license-GPL%20v3-red?style=for-the-badge)](LICENSE) +[![Downloads](https://img.shields.io/github/downloads/DDOneApps/FakeCall/total?color=green&style=for-the-badge)](https://github.com/DDOneApps/FakeCall/releases/latest) +[![Crowdin](https://img.shields.io/badge/dynamic/json?color=red&label=translation&query=$.message&style=for-the-badge&url=https://raw.githubusercontent.com/DDOneApps/FakeCall/main/badge.json&logo=crowdin)](https://crowdin.com/project/fakecall) +[![GitHub license](https://img.shields.io/badge/license-GPL%20v3-red?style=for-the-badge)](LICENSE)

**An open-source Android application to simulate incoming calls, featuring a modern Material 3 UI with dynamic Monet support.** @@ -23,7 +23,7 @@ Ever wanted to get [that Feature of old Samsung phones](https://www.youtube.com/ Introducing FakeCall. Unlike other apps that merely mock a UI, this app integrates directly with the Android Telecom Framework to provide an indistinguishable calling experience. It has many features to make the call as real as possible.

- + ## Features

@@ -92,11 +92,14 @@ You can also save up to five quick trigger presets from the same section: ## Screenshots -![Screenshot 1](https://github.com/DDOneApps/FakeCall/blob/main/Screenshots/Screenshot_20260308-211426_Fake%20Call.png) +![Screenshot 1](https://github.com/DDOneApps/FakeCall/blob/main/metadata/en-US/images/phoneScreenshots/1.png) _Main screen_ -![Screenshot 3](https://github.com/DDOneApps/FakeCall/blob/main/Screenshots/Screenshot_20260308-212114_Telefon.png) -_Call interface_ +![Screenshot 3](https://github.com/DDOneApps/FakeCall/blob/main/metadata/en-US/images/phoneScreenshots/3.png) +_settings screen_ + +![Screenshot 4](https://github.com/DDOneApps/FakeCall/blob/main/metadata/en-US/images/phoneScreenshots/2.png) +_Incoming call (example)_ ## Tech Stack @@ -155,7 +158,7 @@ FakeCall/ We welcome contributions to FakeCall! - +If you want to help translating, do it [Here](https://crowdin.com/project/fakecall/invite?h=ad1b7ff358ecf52e9f823b4f7f691f1d2725120) via crowdin ## License This Project is licenced under GNU General Public License. @@ -172,11 +175,15 @@ Read it [Here](https://raw.githubusercontent.com/DDOneApps/FakeCall/refs/heads/m --- -
+ +**☕️ If you want to support me, you can do it via monero:** +`42eQ1sZtR1USUcGRwc4hiTLNvL7q8U9XSCVVtuhRFKKREpevd4F1X3aN8X4UzkqNTy3n4BsfUooLvj1ydjpem5Ee9SRA2dZ` +![9a569a26-b6f7-4425-84b6-0584a914ae54](https://github.com/user-attachments/assets/10f6a5b6-bf4c-43c1-8929-21e7cc3951e2) + **⭐ Star this repo if you find it helpful!** Made with ❤️(and AI 🤖) -
+ diff --git a/Screenshots/screen-20260328-083803-1774683464338.mp4 b/Screenshots/screen-20260328-083803-1774683464338.mp4 new file mode 100644 index 0000000..105aba7 Binary files /dev/null and b/Screenshots/screen-20260328-083803-1774683464338.mp4 differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3d13489..e582c23 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.upnp.fakeCall" minSdk = 24 targetSdk = 36 - versionCode = 2 - versionName = "2.0" + versionCode = 25 + versionName = "2.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/release/app-release.apk b/app/release/app-release-offline.apk similarity index 57% rename from app/release/app-release.apk rename to app/release/app-release-offline.apk index 1101198..e70b82c 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release-offline.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 16d4627..42a91bf 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 959bc5e..a39702c 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 7ca704c..6969a60 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 2, - "versionName": "2.0", + "versionCode": 25, + "versionName": "2.5", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 76405d2..bcfb3c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,9 +4,11 @@ + + @@ -125,6 +127,10 @@ android:name=".FakeCallAlarmReceiver" android:exported="false" /> + + diff --git a/app/src/main/java/com/upnp/fakeCall/AlarmModeAlarmReceiver.kt b/app/src/main/java/com/upnp/fakeCall/AlarmModeAlarmReceiver.kt new file mode 100644 index 0000000..386a582 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/AlarmModeAlarmReceiver.kt @@ -0,0 +1,192 @@ +package com.upnp.fakeCall + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class AlarmModeAlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + if (intent == null) return + val alarmId = intent.getLongExtra(EXTRA_ALARM_ID, 0L) + if (alarmId == 0L) return + + val callerNumber = intent.getStringExtra(EXTRA_CALLER_NUMBER).orEmpty().trim() + if (callerNumber.isBlank()) return + val callerName = intent.getStringExtra(EXTRA_CALLER_NAME).orEmpty() + val providerName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(KEY_PROVIDER_NAME, context.getString(R.string.default_provider_name)) + .orEmpty() + + val messageMode = runCatching { + AlarmMessageMode.valueOf( + intent.getStringExtra(EXTRA_MESSAGE_MODE).orEmpty().ifBlank { AlarmMessageMode.APP_VOICE_TTS.name } + ) + }.getOrDefault(AlarmMessageMode.APP_VOICE_TTS) + val ttsMessage = intent.getStringExtra(EXTRA_TTS_MESSAGE).orEmpty() + val repeatTtsMessage = intent.getBooleanExtra(EXTRA_REPEAT_TTS_MESSAGE, false) + val customAudioUri = intent.getStringExtra(EXTRA_CUSTOM_AUDIO_URI).orEmpty() + val customAudioName = intent.getStringExtra(EXTRA_CUSTOM_AUDIO_NAME).orEmpty() + val snoozeEnabled = intent.getBooleanExtra(EXTRA_SNOOZE_ENABLED, false) + val snoozeMinutes = intent.getIntExtra(EXTRA_SNOOZE_MINUTES, 5).coerceIn(1, 30) + val speakerDefault = runCatching { + AlarmSpeakerDefault.valueOf( + intent.getStringExtra(EXTRA_SPEAKER_DEFAULT).orEmpty().ifBlank { AlarmSpeakerDefault.EARPIECE.name } + ) + }.getOrDefault(AlarmSpeakerDefault.EARPIECE) + + applyRuntimeOverrides( + context = context, + messageMode = messageMode, + ttsMessage = ttsMessage, + repeatTtsMessage = repeatTtsMessage, + customAudioUri = customAudioUri, + customAudioName = customAudioName, + speakerDefault = speakerDefault, + snoozeEnabled = snoozeEnabled, + snoozeMinutes = snoozeMinutes, + alarmId = alarmId, + callerName = callerName, + callerNumber = callerNumber, + providerName = providerName + ) + + val telecomHelper = TelecomHelper(context) + telecomHelper.registerOrUpdatePhoneAccount(providerName.ifBlank { context.getString(R.string.default_provider_name) }) + if (telecomHelper.isAccountEnabled()) { + telecomHelper.triggerIncomingCall( + callerName = callerName, + callerNumber = callerNumber, + source = IncomingCallSource.ALARM + ) + } + + val repeatDays = (intent.getIntArrayExtra(EXTRA_REPEAT_DAYS) ?: intArrayOf()) + .toSet() + .filter { day -> day in 1..7 } + .toSet() + if (repeatDays.isEmpty()) { + AlarmModeRepository.disable(context, alarmId) + AlarmModeRepository.updateNextTrigger(context, alarmId, 0L) + AlarmModeScheduler.cancel(context, alarmId) + return + } + + val alarm = AlarmModeRepository.find(context, alarmId)?.copy( + callerName = callerName, + callerNumber = callerNumber, + hour = intent.getIntExtra(EXTRA_HOUR, 8).coerceIn(0, 23), + minute = intent.getIntExtra(EXTRA_MINUTE, 0).coerceIn(0, 59), + repeatDays = repeatDays, + messageMode = messageMode, + ttsMessage = ttsMessage, + repeatTtsMessage = repeatTtsMessage, + customAudioUri = customAudioUri, + customAudioName = customAudioName, + snoozeEnabled = snoozeEnabled, + snoozeMinutes = snoozeMinutes, + speakerDefault = speakerDefault, + enabled = true + ) + if (alarm != null) { + val next = AlarmModeScheduler.schedule(context, alarm) + AlarmModeRepository.upsert(context, alarm.copy(nextTriggerAtMillis = next)) + } + } + + private fun applyRuntimeOverrides( + context: Context, + messageMode: AlarmMessageMode, + ttsMessage: String, + repeatTtsMessage: Boolean, + customAudioUri: String, + customAudioName: String, + speakerDefault: AlarmSpeakerDefault, + snoozeEnabled: Boolean, + snoozeMinutes: Int, + alarmId: Long, + callerName: String, + callerNumber: String, + providerName: String + ) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .apply { + when (messageMode) { + AlarmMessageMode.CUSTOM_AUDIO -> { + if (customAudioUri.isNotBlank()) { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, true) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_URI, customAudioUri) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_NAME, customAudioName) + } else { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_URI) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_NAME) + } + putString(KEY_RUNTIME_MESSAGE_MODE, RUNTIME_MESSAGE_MODE_CUSTOM) + remove(KEY_RUNTIME_TTS_MESSAGE) + remove(KEY_RUNTIME_REPEAT_TTS_MESSAGE) + } + AlarmMessageMode.APP_VOICE_TTS -> { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_URI) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_NAME) + putString(KEY_RUNTIME_MESSAGE_MODE, RUNTIME_MESSAGE_MODE_TTS) + putBoolean(KEY_RUNTIME_REPEAT_TTS_MESSAGE, repeatTtsMessage) + putString( + KEY_RUNTIME_TTS_MESSAGE, + ttsMessage.ifBlank { + if (callerName.isNotBlank()) { + context.getString(R.string.alarm_tts_default_message_with_name, callerName) + } else { + context.getString(R.string.alarm_tts_default_message) + } + } + ) + } + } + putString(KEY_RUNTIME_SPEAKER_DEFAULT, speakerDefault.name) + putBoolean(KEY_RUNTIME_SNOOZE_ENABLED, snoozeEnabled) + putInt(KEY_RUNTIME_SNOOZE_MINUTES, snoozeMinutes) + putLong(KEY_RUNTIME_SNOOZE_ALARM_ID, alarmId) + putString(KEY_RUNTIME_SNOOZE_CALLER_NAME, callerName) + putString(KEY_RUNTIME_SNOOZE_CALLER_NUMBER, callerNumber) + putString(KEY_RUNTIME_SNOOZE_PROVIDER_NAME, providerName) + } + .apply() + } + + companion object { + const val EXTRA_ALARM_ID = "extra_alarm_id" + const val EXTRA_CALLER_NAME = "extra_alarm_caller_name" + const val EXTRA_CALLER_NUMBER = "extra_alarm_caller_number" + const val EXTRA_HOUR = "extra_alarm_hour" + const val EXTRA_MINUTE = "extra_alarm_minute" + const val EXTRA_REPEAT_DAYS = "extra_alarm_repeat_days" + const val EXTRA_MESSAGE_MODE = "extra_alarm_message_mode" + const val EXTRA_TTS_MESSAGE = "extra_alarm_tts_message" + const val EXTRA_REPEAT_TTS_MESSAGE = "extra_alarm_repeat_tts_message" + const val EXTRA_CUSTOM_AUDIO_URI = "extra_alarm_custom_audio_uri" + const val EXTRA_CUSTOM_AUDIO_NAME = "extra_alarm_custom_audio_name" + const val EXTRA_SNOOZE_ENABLED = "extra_alarm_snooze_enabled" + const val EXTRA_SNOOZE_MINUTES = "extra_alarm_snooze_minutes" + const val EXTRA_SPEAKER_DEFAULT = "extra_alarm_speaker_default" + + private const val PREFS_NAME = "fake_call_prefs" + private const val KEY_PROVIDER_NAME = "provider_name" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED = "runtime_audio_override_enabled" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_URI = "runtime_audio_override_uri" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_NAME = "runtime_audio_override_name" + private const val KEY_RUNTIME_MESSAGE_MODE = "runtime_message_mode" + private const val KEY_RUNTIME_TTS_MESSAGE = "runtime_tts_message" + private const val KEY_RUNTIME_REPEAT_TTS_MESSAGE = "runtime_repeat_tts_message" + private const val KEY_RUNTIME_SPEAKER_DEFAULT = "runtime_speaker_default" + private const val KEY_RUNTIME_SNOOZE_ENABLED = "runtime_snooze_enabled" + private const val KEY_RUNTIME_SNOOZE_MINUTES = "runtime_snooze_minutes" + private const val KEY_RUNTIME_SNOOZE_ALARM_ID = "runtime_snooze_alarm_id" + private const val KEY_RUNTIME_SNOOZE_CALLER_NAME = "runtime_snooze_caller_name" + private const val KEY_RUNTIME_SNOOZE_CALLER_NUMBER = "runtime_snooze_caller_number" + private const val KEY_RUNTIME_SNOOZE_PROVIDER_NAME = "runtime_snooze_provider_name" + private const val RUNTIME_MESSAGE_MODE_CUSTOM = "custom_audio" + private const val RUNTIME_MESSAGE_MODE_TTS = "tts" + } +} diff --git a/app/src/main/java/com/upnp/fakeCall/AlarmModeModels.kt b/app/src/main/java/com/upnp/fakeCall/AlarmModeModels.kt new file mode 100644 index 0000000..5c11020 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/AlarmModeModels.kt @@ -0,0 +1,46 @@ +package com.upnp.fakeCall + +enum class AlarmMessageMode { + APP_VOICE_TTS, + CUSTOM_AUDIO +} + +enum class AlarmSpeakerDefault { + EARPIECE, + SPEAKER +} + +data class AlarmModeItem( + val id: Long, + val callerName: String, + val callerNumber: String, + val hour: Int, + val minute: Int, + val repeatDays: Set = emptySet(), + val messageMode: AlarmMessageMode = AlarmMessageMode.APP_VOICE_TTS, + val ttsMessage: String = "", + val repeatTtsMessage: Boolean = false, + val customAudioUri: String = "", + val customAudioName: String = "", + val snoozeEnabled: Boolean = false, + val snoozeMinutes: Int = 5, + val speakerDefault: AlarmSpeakerDefault = AlarmSpeakerDefault.EARPIECE, + val enabled: Boolean = true, + val nextTriggerAtMillis: Long = 0L +) + +data class AlarmModeDraft( + val callerName: String, + val callerNumber: String, + val hour: Int, + val minute: Int, + val repeatDays: Set, + val messageMode: AlarmMessageMode, + val ttsMessage: String, + val repeatTtsMessage: Boolean, + val customAudioUri: String, + val customAudioName: String, + val snoozeEnabled: Boolean, + val snoozeMinutes: Int, + val speakerDefault: AlarmSpeakerDefault +) diff --git a/app/src/main/java/com/upnp/fakeCall/AlarmModeRepository.kt b/app/src/main/java/com/upnp/fakeCall/AlarmModeRepository.kt new file mode 100644 index 0000000..fb16660 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/AlarmModeRepository.kt @@ -0,0 +1,142 @@ +package com.upnp.fakeCall + +import android.content.Context +import org.json.JSONArray +import org.json.JSONObject + +object AlarmModeRepository { + private const val PREFS_NAME = "fake_call_prefs" + private const val KEY_ALARM_MODE_ITEMS = "alarm_mode_items" + + fun load(context: Context): List { + val raw = prefs(context).getString(KEY_ALARM_MODE_ITEMS, "").orEmpty() + if (raw.isBlank()) return emptyList() + return runCatching { + val array = JSONArray(raw) + buildList { + for (index in 0 until array.length()) { + val obj = array.optJSONObject(index) ?: continue + parseItem(obj)?.let(::add) + } + } + }.getOrDefault(emptyList()) + } + + fun find(context: Context, alarmId: Long): AlarmModeItem? { + return load(context).firstOrNull { it.id == alarmId } + } + + fun upsert(context: Context, item: AlarmModeItem): List { + val existing = load(context) + val updated = buildList { + var replaced = false + existing.forEach { current -> + if (current.id == item.id) { + add(item) + replaced = true + } else { + add(current) + } + } + if (!replaced) add(item) + }.sortedBy { it.hour * 60 + it.minute } + save(context, updated) + return updated + } + + fun updateEnabled(context: Context, alarmId: Long, enabled: Boolean): List { + val updated = load(context).map { item -> + if (item.id == alarmId) item.copy(enabled = enabled) else item + } + save(context, updated) + return updated + } + + fun updateNextTrigger(context: Context, alarmId: Long, nextTriggerAtMillis: Long): List { + val updated = load(context).map { item -> + if (item.id == alarmId) item.copy(nextTriggerAtMillis = nextTriggerAtMillis) else item + } + save(context, updated) + return updated + } + + fun disable(context: Context, alarmId: Long): List { + return updateEnabled(context, alarmId, false) + } + + fun delete(context: Context, alarmId: Long): List { + val updated = load(context).filterNot { it.id == alarmId } + save(context, updated) + return updated + } + + fun replaceAll(context: Context, items: List) { + save(context, items.sortedBy { it.hour * 60 + it.minute }) + } + + private fun save(context: Context, items: List) { + val array = JSONArray() + items.forEach { item -> + array.put( + JSONObject().apply { + put("id", item.id) + put("callerName", item.callerName) + put("callerNumber", item.callerNumber) + put("hour", item.hour) + put("minute", item.minute) + put("repeatDays", JSONArray(item.repeatDays.sorted())) + put("messageMode", item.messageMode.name) + put("ttsMessage", item.ttsMessage) + put("repeatTtsMessage", item.repeatTtsMessage) + put("customAudioUri", item.customAudioUri) + put("customAudioName", item.customAudioName) + put("snoozeEnabled", item.snoozeEnabled) + put("snoozeMinutes", item.snoozeMinutes) + put("speakerDefault", item.speakerDefault.name) + put("enabled", item.enabled) + put("nextTriggerAtMillis", item.nextTriggerAtMillis) + } + ) + } + prefs(context).edit().putString(KEY_ALARM_MODE_ITEMS, array.toString()).apply() + } + + private fun parseItem(obj: JSONObject): AlarmModeItem? { + val id = obj.optLong("id", 0L) + val callerNumber = obj.optString("callerNumber").orEmpty().trim() + if (id == 0L || callerNumber.isBlank()) return null + val daysArray = obj.optJSONArray("repeatDays") + val repeatDays = buildSet { + for (index in 0 until (daysArray?.length() ?: 0)) { + val day = daysArray?.optInt(index) ?: continue + if (day in 1..7) add(day) + } + } + val messageMode = runCatching { + AlarmMessageMode.valueOf(obj.optString("messageMode", AlarmMessageMode.APP_VOICE_TTS.name)) + }.getOrDefault(AlarmMessageMode.APP_VOICE_TTS) + val speakerDefault = runCatching { + AlarmSpeakerDefault.valueOf(obj.optString("speakerDefault", AlarmSpeakerDefault.EARPIECE.name)) + }.getOrDefault(AlarmSpeakerDefault.EARPIECE) + return AlarmModeItem( + id = id, + callerName = obj.optString("callerName").orEmpty(), + callerNumber = callerNumber, + hour = obj.optInt("hour", 8).coerceIn(0, 23), + minute = obj.optInt("minute", 0).coerceIn(0, 59), + repeatDays = repeatDays, + messageMode = messageMode, + ttsMessage = obj.optString("ttsMessage").orEmpty(), + repeatTtsMessage = obj.optBoolean("repeatTtsMessage", false), + customAudioUri = obj.optString("customAudioUri").orEmpty(), + customAudioName = obj.optString("customAudioName").orEmpty(), + snoozeEnabled = obj.optBoolean("snoozeEnabled", false), + snoozeMinutes = obj.optInt("snoozeMinutes", 5).coerceIn(1, 30), + speakerDefault = speakerDefault, + enabled = obj.optBoolean("enabled", true), + nextTriggerAtMillis = obj.optLong("nextTriggerAtMillis", 0L) + ) + } + + private fun prefs(context: Context) = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) +} diff --git a/app/src/main/java/com/upnp/fakeCall/AlarmModeScheduler.kt b/app/src/main/java/com/upnp/fakeCall/AlarmModeScheduler.kt new file mode 100644 index 0000000..c1d7838 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/AlarmModeScheduler.kt @@ -0,0 +1,139 @@ +package com.upnp.fakeCall + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import java.time.DayOfWeek +import java.time.ZonedDateTime + +object AlarmModeScheduler { + fun canScheduleExact(context: Context): Boolean { + val alarmManager = context.getSystemService(AlarmManager::class.java) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + alarmManager.canScheduleExactAlarms() + } else { + true + } + } + + fun schedule(context: Context, alarm: AlarmModeItem): Long { + val triggerAtMillis = computeNextTriggerAtMillis(alarm) + if (triggerAtMillis <= 0L) return 0L + if (!scheduleAt(context, alarm, triggerAtMillis)) return 0L + return triggerAtMillis + } + + fun scheduleSnooze( + context: Context, + alarm: AlarmModeItem, + triggerAtMillis: Long + ): Boolean { + return scheduleAt(context, alarm, triggerAtMillis) + } + + fun cancel(context: Context, alarmId: Long) { + val alarmManager = context.getSystemService(AlarmManager::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCodeFor(alarmId), + Intent(context, AlarmModeAlarmReceiver::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + } + + fun computeNextTriggerAtMillis( + alarm: AlarmModeItem, + now: ZonedDateTime = ZonedDateTime.now() + ): Long { + val hour = alarm.hour.coerceIn(0, 23) + val minute = alarm.minute.coerceIn(0, 59) + val hasRepeat = alarm.repeatDays.isNotEmpty() + var candidate = now + .withHour(hour) + .withMinute(minute) + .withSecond(0) + .withNano(0) + + if (!hasRepeat) { + if (!candidate.isAfter(now)) { + candidate = candidate.plusDays(1) + } + return candidate.toInstant().toEpochMilli() + } + + if (!candidate.isAfter(now)) { + candidate = candidate.plusDays(1) + } + repeat(8) { + val day = candidate.dayOfWeek.value + if (alarm.repeatDays.contains(day)) { + return candidate.toInstant().toEpochMilli() + } + candidate = candidate.plusDays(1) + } + return 0L + } + + fun dayLabel(context: Context, day: Int): String { + return when (day) { + DayOfWeek.MONDAY.value -> context.getString(R.string.weekday_mon) + DayOfWeek.TUESDAY.value -> context.getString(R.string.weekday_tue) + DayOfWeek.WEDNESDAY.value -> context.getString(R.string.weekday_wed) + DayOfWeek.THURSDAY.value -> context.getString(R.string.weekday_thu) + DayOfWeek.FRIDAY.value -> context.getString(R.string.weekday_fri) + DayOfWeek.SATURDAY.value -> context.getString(R.string.weekday_sat) + DayOfWeek.SUNDAY.value -> context.getString(R.string.weekday_sun) + else -> "" + } + } + + private fun scheduleAt( + context: Context, + alarm: AlarmModeItem, + triggerAtMillis: Long + ): Boolean { + val alarmManager = context.getSystemService(AlarmManager::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { + return false + } + val intent = Intent(context, AlarmModeAlarmReceiver::class.java).apply { + putExtra(AlarmModeAlarmReceiver.EXTRA_ALARM_ID, alarm.id) + putExtra(AlarmModeAlarmReceiver.EXTRA_CALLER_NAME, alarm.callerName) + putExtra(AlarmModeAlarmReceiver.EXTRA_CALLER_NUMBER, alarm.callerNumber) + putExtra(AlarmModeAlarmReceiver.EXTRA_HOUR, alarm.hour) + putExtra(AlarmModeAlarmReceiver.EXTRA_MINUTE, alarm.minute) + putExtra(AlarmModeAlarmReceiver.EXTRA_REPEAT_DAYS, alarm.repeatDays.toIntArray()) + putExtra(AlarmModeAlarmReceiver.EXTRA_MESSAGE_MODE, alarm.messageMode.name) + putExtra(AlarmModeAlarmReceiver.EXTRA_TTS_MESSAGE, alarm.ttsMessage) + putExtra(AlarmModeAlarmReceiver.EXTRA_REPEAT_TTS_MESSAGE, alarm.repeatTtsMessage) + putExtra(AlarmModeAlarmReceiver.EXTRA_CUSTOM_AUDIO_URI, alarm.customAudioUri) + putExtra(AlarmModeAlarmReceiver.EXTRA_CUSTOM_AUDIO_NAME, alarm.customAudioName) + putExtra(AlarmModeAlarmReceiver.EXTRA_SNOOZE_ENABLED, alarm.snoozeEnabled) + putExtra(AlarmModeAlarmReceiver.EXTRA_SNOOZE_MINUTES, alarm.snoozeMinutes) + putExtra(AlarmModeAlarmReceiver.EXTRA_SPEAKER_DEFAULT, alarm.speakerDefault.name) + } + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCodeFor(alarm.id), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent) + } else { + @Suppress("DEPRECATION") + alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent) + } + return true + } + + private fun requestCodeFor(alarmId: Long): Int { + val positive = if (alarmId < 0) -alarmId else alarmId + val bounded = positive % 1_000_000_000L + return (40_000 + bounded).toInt() + } +} diff --git a/app/src/main/java/com/upnp/fakeCall/BatterySetupNavigator.kt b/app/src/main/java/com/upnp/fakeCall/BatterySetupNavigator.kt new file mode 100644 index 0000000..6771602 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/BatterySetupNavigator.kt @@ -0,0 +1,157 @@ +package com.upnp.fakeCall + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings + +enum class RomFamily { + HYPER_OS_XIAOMI, + OXYGEN_OS_ONEPLUS, + COLOR_OS_OPPO_REALME, + ONE_UI_SAMSUNG, + GENERIC +} + +object BatterySetupNavigator { + + fun detectRomFamily(): RomFamily { + val manufacturer = Build.MANUFACTURER.orEmpty().lowercase() + val display = Build.DISPLAY.orEmpty().lowercase() + val fingerprint = Build.FINGERPRINT.orEmpty().lowercase() + val summary = "$manufacturer $display $fingerprint" + + return when { + manufacturer in setOf("xiaomi", "redmi", "poco") || summary.contains("hyperos") -> RomFamily.HYPER_OS_XIAOMI + manufacturer == "oneplus" || summary.contains("oxygen") -> RomFamily.OXYGEN_OS_ONEPLUS + manufacturer in setOf("oppo", "realme") || summary.contains("coloros") || summary.contains("realmeui") -> RomFamily.COLOR_OS_OPPO_REALME + manufacturer == "samsung" || summary.contains("one ui") -> RomFamily.ONE_UI_SAMSUNG + else -> RomFamily.GENERIC + } + } + + fun isBatteryOptimizationDisabled(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true + val powerManager = context.getSystemService(PowerManager::class.java) ?: return false + return powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + fun openSystemBatteryOptimization(context: Context): Boolean { + val intents = mutableListOf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!isBatteryOptimizationDisabled(context)) { + intents += Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:${context.packageName}") + ) + } + intents += Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + } + intents += Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:${context.packageName}") + ) + return launchFirstResolvable(context, intents) + } + + fun openOemBackgroundSettings(context: Context): Boolean { + val packageName = context.packageName + val appLabel = context.applicationInfo.loadLabel(context.packageManager).toString() + val intents = mutableListOf() + + when (detectRomFamily()) { + RomFamily.HYPER_OS_XIAOMI -> { + intents += Intent().setComponent( + ComponentName( + "com.miui.securitycenter", + "com.miui.permcenter.autostart.AutoStartManagementActivity" + ) + ) + intents += Intent().setComponent( + ComponentName( + "com.miui.securitycenter", + "com.miui.powerkeeper.ui.HiddenAppsConfigActivity" + ) + ).putExtra("package_name", packageName) + .putExtra("package_label", appLabel) + } + + RomFamily.OXYGEN_OS_ONEPLUS -> { + intents += Intent().setComponent( + ComponentName( + "com.oneplus.security", + "com.oneplus.security.chainlaunch.view.ChainLaunchAppListActivity" + ) + ) + intents += Intent("com.android.settings.action.BACKGROUND_OPTIMIZE") + .setPackage("com.oneplus.security") + } + + RomFamily.COLOR_OS_OPPO_REALME -> { + intents += Intent().setComponent( + ComponentName( + "com.coloros.safecenter", + "com.coloros.safecenter.permission.startup.StartupAppListActivity" + ) + ) + intents += Intent().setComponent( + ComponentName( + "com.oppo.safe", + "com.oppo.safe.permission.startup.StartupAppListActivity" + ) + ) + intents += Intent().setComponent( + ComponentName( + "com.coloros.oppoguardelf", + "com.coloros.powermanager.fuelgaue.PowerUsageModelActivity" + ) + ) + intents += Intent().setComponent( + ComponentName( + "com.oplus.battery", + "com.oplus.powermanager.fuelgaue.PowerUsageModelActivity" + ) + ) + } + + RomFamily.ONE_UI_SAMSUNG -> { + intents += Intent().setComponent( + ComponentName( + "com.samsung.android.lool", + "com.samsung.android.sm.ui.battery.BatteryActivity" + ) + ) + intents += Intent().setComponent( + ComponentName( + "com.samsung.android.lool", + "com.samsung.android.sm.battery.ui.usage.CheckableAppListActivity" + ) + ) + } + + RomFamily.GENERIC -> Unit + } + + intents += Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:${context.packageName}") + ) + return launchFirstResolvable(context, intents) + } + + private fun launchFirstResolvable(context: Context, intents: List): Boolean { + val packageManager = context.packageManager + intents.forEach { intent -> + val prepared = intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (prepared.resolveActivity(packageManager) != null) { + runCatching { context.startActivity(prepared) } + .onSuccess { return true } + } + } + return false + } +} + diff --git a/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmReceiver.kt b/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmReceiver.kt index 63680ea..36661dd 100644 --- a/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmReceiver.kt +++ b/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmReceiver.kt @@ -6,6 +6,22 @@ import android.content.Intent class FakeCallAlarmReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { + val runtimeAudioUri = intent?.getStringExtra(EXTRA_RUNTIME_AUDIO_URI).orEmpty() + val runtimeAudioName = intent?.getStringExtra(EXTRA_RUNTIME_AUDIO_NAME).orEmpty() + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .apply { + if (runtimeAudioUri.isBlank()) { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_URI) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_NAME) + } else { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, true) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_URI, runtimeAudioUri) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_NAME, runtimeAudioName) + } + } + .apply() context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .remove(KEY_TIMER_ENDS_AT) @@ -32,5 +48,10 @@ class FakeCallAlarmReceiver : BroadcastReceiver() { const val EXTRA_CALLER_NAME = "extra_caller_name" const val EXTRA_CALLER_NUMBER = "extra_caller_number" const val EXTRA_PROVIDER_NAME = "extra_provider_name" + const val EXTRA_RUNTIME_AUDIO_URI = "extra_runtime_audio_uri" + const val EXTRA_RUNTIME_AUDIO_NAME = "extra_runtime_audio_name" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED = "runtime_audio_override_enabled" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_URI = "runtime_audio_override_uri" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_NAME = "runtime_audio_override_name" } } diff --git a/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmScheduler.kt b/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmScheduler.kt index f7223d8..6b47455 100644 --- a/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmScheduler.kt +++ b/app/src/main/java/com/upnp/fakeCall/FakeCallAlarmScheduler.kt @@ -22,7 +22,9 @@ object FakeCallAlarmScheduler { triggerAtMillis: Long, callerName: String, callerNumber: String, - providerName: String + providerName: String, + runtimeAudioUri: String? = null, + runtimeAudioName: String? = null ): Boolean { val alarmManager = context.getSystemService(AlarmManager::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { @@ -33,6 +35,13 @@ object FakeCallAlarmScheduler { putExtra(FakeCallAlarmReceiver.EXTRA_CALLER_NAME, callerName) putExtra(FakeCallAlarmReceiver.EXTRA_CALLER_NUMBER, callerNumber) putExtra(FakeCallAlarmReceiver.EXTRA_PROVIDER_NAME, providerName) + if (!runtimeAudioUri.isNullOrBlank()) { + putExtra(FakeCallAlarmReceiver.EXTRA_RUNTIME_AUDIO_URI, runtimeAudioUri) + putExtra(FakeCallAlarmReceiver.EXTRA_RUNTIME_AUDIO_NAME, runtimeAudioName.orEmpty()) + } else { + removeExtra(FakeCallAlarmReceiver.EXTRA_RUNTIME_AUDIO_URI) + removeExtra(FakeCallAlarmReceiver.EXTRA_RUNTIME_AUDIO_NAME) + } } val pendingIntent = PendingIntent.getBroadcast( context, diff --git a/app/src/main/java/com/upnp/fakeCall/FakeCallConnectionService.kt b/app/src/main/java/com/upnp/fakeCall/FakeCallConnectionService.kt index 5648a4c..d3d505b 100644 --- a/app/src/main/java/com/upnp/fakeCall/FakeCallConnectionService.kt +++ b/app/src/main/java/com/upnp/fakeCall/FakeCallConnectionService.kt @@ -17,11 +17,18 @@ class FakeCallConnectionService : ConnectionService() { ?: extras?.getString(TelecomHelper.EXTRA_FAKE_CALLER_NUMBER) ?: getString(R.string.notification_unknown_caller) val name = extras?.getString(TelecomHelper.EXTRA_FAKE_CALLER_NAME).orEmpty() + val source = IncomingCallSource.fromStorage( + extras?.getString(TelecomHelper.EXTRA_FAKE_CALL_SOURCE) + ) + val defaultRingTimeoutSeconds = if (source == IncomingCallSource.ALARM) 0 else 45 + val ringTimeoutSeconds = extras?.getInt(TelecomHelper.EXTRA_RING_TIMEOUT_SECONDS, defaultRingTimeoutSeconds) + ?: defaultRingTimeoutSeconds return FakeConnection( context = this, callerName = name, - callerNumber = number + callerNumber = number, + ringTimeoutSeconds = ringTimeoutSeconds ) } } diff --git a/app/src/main/java/com/upnp/fakeCall/FakeCallViewModel.kt b/app/src/main/java/com/upnp/fakeCall/FakeCallViewModel.kt index d3dfe46..681e875 100644 --- a/app/src/main/java/com/upnp/fakeCall/FakeCallViewModel.kt +++ b/app/src/main/java/com/upnp/fakeCall/FakeCallViewModel.kt @@ -1,15 +1,24 @@ package com.upnp.fakeCall +import android.Manifest import android.app.Application +import android.content.ContentUris import android.os.Build import com.upnp.fakeCall.R import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri +import android.provider.ContactsContract import android.provider.DocumentsContract import android.provider.Settings +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.telecom.TelecomManager +import android.util.Base64 import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.upnp.fakeCall.ivr.IvrConfig @@ -27,8 +36,11 @@ import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import java.io.ByteArrayOutputStream import java.util.Locale import java.util.UUID +import org.json.JSONArray +import org.json.JSONObject enum class ScheduleKind { PRESET, @@ -36,6 +48,19 @@ enum class ScheduleKind { CUSTOM_EXACT } +enum class CallerInputMode { + MANUAL, + CONTACT +} + +data class CallContact( + val id: Long, + val displayName: String, + val phoneNumber: String, + val photoUri: String = "", + val avatarBase64: String = "" +) + data class CustomPreset( val kind: ScheduleKind, val minutes: Int = 0, @@ -44,11 +69,29 @@ data class CustomPreset( val minute: Int = 0 ) +data class SimProviderOption( + val subscriptionId: Int, + val displayName: String, + val carrierName: String, + val slotIndex: Int +) + +enum class AnswerAudioMode { + SILENT, + AUDIO_FILE, + CUSTOM_IVR, + MP3_IVR +} + data class FakeCallUiState( val isOnboardingComplete: Boolean = false, val providerName: String = "", val callerName: String = "", val callerNumber: String = "", + val callerInputMode: CallerInputMode = CallerInputMode.MANUAL, + val selectedContact: CallContact? = null, + val pinnedContacts: List = emptyList(), + val recentContacts: List = emptyList(), val selectedDelaySeconds: Int = 10, val scheduleKind: ScheduleKind = ScheduleKind.PRESET, val customCountdownMinutes: Int = 2, @@ -57,6 +100,7 @@ data class FakeCallUiState( val customExactMinute: Int = 45, val customPresets: List = emptyList(), val ivrConfig: IvrConfig? = null, + val answerAudioMode: AnswerAudioMode = AnswerAudioMode.SILENT, val selectedAudioUri: String = "", val selectedAudioName: String = "", val hasRequiredPermissions: Boolean = false, @@ -64,13 +108,20 @@ data class FakeCallUiState( val isTimerRunning: Boolean = false, val timerEndsAtMillis: Long = 0L, val statusMessage: String = "", - val isRecordingEnabled: Boolean = true, + val isRecordingEnabled: Boolean = false, val recordingsFolderName: String = "", val quickTriggerCallerName: String = "", val quickTriggerCallerNumber: String = "", val quickTriggerDelaySeconds: Int = QuickTriggerManager.DEFAULT_DELAY_SECONDS, val quickTriggerPresetName: String = "", val quickTriggerPresets: List = emptyList(), + val quickTriggerDefaultPresetSlot: Int? = null, + val callRingTimeoutSeconds: Int = 45, + val alarmRingTimeoutSeconds: Int = 0, + val alarmModeItems: List = emptyList(), + val isMp3IvrModeEnabled: Boolean = false, + val mp3IvrFolderUri: String = "", + val mp3IvrFolderName: String = "", val startupUpdate: ReleaseInfo? = null ) @@ -85,6 +136,12 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application private val updateChecker = UpdateChecker() private val quickTriggerDefaults = QuickTriggerManager.loadDefaults(application) private val quickTriggerPresets = QuickTriggerManager.loadPresets(application) + private val initialAlarmModeItems = AlarmModeRepository.load(application) + private val initialPinnedContacts = parseContactList(prefs.getString(KEY_PINNED_CONTACTS, "").orEmpty()) + private val initialRecentContacts = pruneRecentContacts( + recentContacts = parseContactList(prefs.getString(KEY_RECENT_CONTACTS, "").orEmpty()), + pinnedContacts = initialPinnedContacts + ) private val _uiState = MutableStateFlow( FakeCallUiState( @@ -92,6 +149,14 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application providerName = prefs.getString(KEY_PROVIDER_NAME, application.getString(R.string.default_provider_name)).orEmpty(), callerName = prefs.getString(KEY_CALLER_NAME, "").orEmpty(), callerNumber = prefs.getString(KEY_CALLER_NUMBER, "").orEmpty(), + callerInputMode = runCatching { + CallerInputMode.valueOf( + prefs.getString(KEY_CALLER_INPUT_MODE, CallerInputMode.MANUAL.name).orEmpty() + ) + }.getOrDefault(CallerInputMode.MANUAL), + selectedContact = parseContact(prefs.getString(KEY_SELECTED_CONTACT, "").orEmpty()), + pinnedContacts = initialPinnedContacts, + recentContacts = initialRecentContacts, selectedDelaySeconds = prefs.getInt(KEY_DELAY_SECONDS, 10), scheduleKind = runCatching { ScheduleKind.valueOf( @@ -104,21 +169,35 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application customExactMinute = prefs.getInt(KEY_CUSTOM_EXACT_MINUTE, 45), customPresets = parseCustomPresets(prefs.getString(KEY_CUSTOM_PRESETS, "").orEmpty()), ivrConfig = ivrStore.load(application), + answerAudioMode = loadAnswerAudioMode(application), selectedAudioUri = prefs.getString(KEY_AUDIO_URI, "").orEmpty(), selectedAudioName = prefs.getString(KEY_AUDIO_NAME, application.getString(R.string.default_audio_name)).orEmpty(), timerEndsAtMillis = prefs.getLong(KEY_TIMER_ENDS_AT, 0L), - isRecordingEnabled = prefs.getBoolean(KEY_RECORDING_ENABLED, true), + isRecordingEnabled = prefs.getBoolean(KEY_RECORDING_ENABLED, DEFAULT_RECORDING_ENABLED), recordingsFolderName = prefs.getString(KEY_RECORDINGS_FOLDER_NAME, application.getString(R.string.default_recordings_folder)).orEmpty(), quickTriggerCallerName = quickTriggerDefaults.callerName, quickTriggerCallerNumber = quickTriggerDefaults.callerNumber, quickTriggerDelaySeconds = quickTriggerDefaults.delaySeconds, quickTriggerPresetName = prefs.getString(KEY_QUICK_TRIGGER_PRESET_NAME, "").orEmpty(), - quickTriggerPresets = quickTriggerPresets + quickTriggerPresets = quickTriggerPresets, + quickTriggerDefaultPresetSlot = QuickTriggerManager.loadDefaultPresetSlot(application), + callRingTimeoutSeconds = prefs.getInt(KEY_CALL_RING_TIMEOUT_SECONDS, DEFAULT_CALL_RING_TIMEOUT_SECONDS) + .coerceAtLeast(0), + alarmRingTimeoutSeconds = prefs.getInt(KEY_ALARM_RING_TIMEOUT_SECONDS, DEFAULT_ALARM_RING_TIMEOUT_SECONDS) + .coerceAtLeast(0), + alarmModeItems = initialAlarmModeItems, + isMp3IvrModeEnabled = prefs.getBoolean(KEY_MP3_IVR_MODE_ENABLED, false), + mp3IvrFolderUri = prefs.getString(KEY_MP3_IVR_FOLDER_URI, "").orEmpty(), + mp3IvrFolderName = prefs.getString( + KEY_MP3_IVR_FOLDER_NAME, + application.getString(R.string.settings_mp3_ivr_no_folder_selected) + ).orEmpty() ) ) val uiState: StateFlow = _uiState.asStateFlow() val delayOptionsSeconds: List = listOf(0, 10, 30, 60, 120, 300) + val ringTimeoutOptionsSeconds: List = listOf(0, 15, 30, 45, 60, 90, 120, 180) init { viewModelScope.launch { @@ -127,6 +206,7 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application if (uiState.value.hasRequiredPermissions) { refreshProviderStatus() } + syncAlarmModeState() delay(1_000L) } } @@ -177,11 +257,198 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application saveQuickTriggerDefaults(uiState.value.copy(quickTriggerDelaySeconds = delaySeconds)) } + fun onCallRingTimeoutChange(timeoutSeconds: Int) { + val normalized = timeoutSeconds.coerceAtLeast(0) + prefs.edit().putInt(KEY_CALL_RING_TIMEOUT_SECONDS, normalized).apply() + _uiState.update { it.copy(callRingTimeoutSeconds = normalized) } + } + + fun onAlarmRingTimeoutChange(timeoutSeconds: Int) { + val normalized = timeoutSeconds.coerceAtLeast(0) + prefs.edit().putInt(KEY_ALARM_RING_TIMEOUT_SECONDS, normalized).apply() + _uiState.update { it.copy(alarmRingTimeoutSeconds = normalized) } + } + fun onQuickTriggerPresetNameChange(value: String) { prefs.edit().putString(KEY_QUICK_TRIGGER_PRESET_NAME, value).apply() _uiState.update { it.copy(quickTriggerPresetName = value) } } + fun newAlarmModeDraft(): AlarmModeDraft { + val state = uiState.value + val now = ZonedDateTime.now().plusMinutes(5) + return AlarmModeDraft( + callerName = state.callerName, + callerNumber = state.callerNumber, + hour = now.hour, + minute = now.minute, + repeatDays = emptySet(), + messageMode = if (state.selectedAudioUri.isBlank()) { + AlarmMessageMode.APP_VOICE_TTS + } else { + AlarmMessageMode.CUSTOM_AUDIO + }, + ttsMessage = str(R.string.alarm_tts_default_message), + repeatTtsMessage = false, + customAudioUri = state.selectedAudioUri, + customAudioName = state.selectedAudioName, + snoozeEnabled = true, + snoozeMinutes = 5, + speakerDefault = AlarmSpeakerDefault.EARPIECE + ) + } + + fun draftForAlarm(alarmId: Long): AlarmModeDraft? { + val alarm = AlarmModeRepository.find(getApplication(), alarmId) ?: return null + return AlarmModeDraft( + callerName = alarm.callerName, + callerNumber = alarm.callerNumber, + hour = alarm.hour, + minute = alarm.minute, + repeatDays = alarm.repeatDays, + messageMode = alarm.messageMode, + ttsMessage = alarm.ttsMessage.ifBlank { str(R.string.alarm_tts_default_message) }, + repeatTtsMessage = alarm.repeatTtsMessage, + customAudioUri = alarm.customAudioUri, + customAudioName = alarm.customAudioName, + snoozeEnabled = alarm.snoozeEnabled, + snoozeMinutes = alarm.snoozeMinutes, + speakerDefault = alarm.speakerDefault + ) + } + + fun saveAlarmMode(draft: AlarmModeDraft): Boolean { + val alarmId = System.currentTimeMillis() + val item = createAlarmModeItem(alarmId, draft) ?: return false + return persistAlarmModeItem(item, isUpdate = false) + } + + fun updateAlarmMode(alarmId: Long, draft: AlarmModeDraft): Boolean { + val existing = AlarmModeRepository.find(getApplication(), alarmId) ?: return false + val item = createAlarmModeItem(alarmId, draft, enabled = existing.enabled) ?: return false + return persistAlarmModeItem(item, isUpdate = true) + } + + fun deleteAlarmMode(alarmId: Long) { + AlarmModeScheduler.cancel(getApplication(), alarmId) + val updated = AlarmModeRepository.delete(getApplication(), alarmId) + _uiState.update { + it.copy( + alarmModeItems = updated, + statusMessage = str(R.string.status_alarm_deleted) + ) + } + } + + private fun createAlarmModeItem( + alarmId: Long, + draft: AlarmModeDraft, + enabled: Boolean = true + ): AlarmModeItem? { + if (!uiState.value.hasRequiredPermissions) { + _uiState.update { it.copy(statusMessage = str(R.string.status_grant_permissions_scheduling)) } + return null + } + if (!uiState.value.isProviderEnabled) { + _uiState.update { it.copy(statusMessage = str(R.string.status_enable_calling_accounts)) } + return null + } + val number = draft.callerNumber.trim() + if (number.isBlank()) { + _uiState.update { it.copy(statusMessage = str(R.string.status_enter_caller_number_scheduling)) } + return null + } + val callerName = draft.callerName.trim() + val normalizedDraft = draft.copy( + callerName = callerName, + callerNumber = number, + hour = draft.hour.coerceIn(0, 23), + minute = draft.minute.coerceIn(0, 59), + snoozeMinutes = draft.snoozeMinutes.coerceIn(1, 30), + ttsMessage = draft.ttsMessage.trim() + ) + return AlarmModeItem( + id = alarmId, + callerName = normalizedDraft.callerName, + callerNumber = normalizedDraft.callerNumber, + hour = normalizedDraft.hour, + minute = normalizedDraft.minute, + repeatDays = normalizedDraft.repeatDays.filter { it in 1..7 }.toSet(), + messageMode = normalizedDraft.messageMode, + ttsMessage = normalizedDraft.ttsMessage, + repeatTtsMessage = normalizedDraft.repeatTtsMessage, + customAudioUri = normalizedDraft.customAudioUri, + customAudioName = normalizedDraft.customAudioName, + snoozeEnabled = normalizedDraft.snoozeEnabled, + snoozeMinutes = normalizedDraft.snoozeMinutes, + speakerDefault = normalizedDraft.speakerDefault, + enabled = enabled + ) + } + + private fun persistAlarmModeItem(item: AlarmModeItem, isUpdate: Boolean): Boolean { + if (!AlarmModeScheduler.canScheduleExact(getApplication())) { + _uiState.update { it.copy(statusMessage = str(R.string.status_enable_exact_alarms)) } + return false + } + + AlarmModeScheduler.cancel(getApplication(), item.id) + val triggerAtMillis = AlarmModeScheduler.schedule(getApplication(), item) + if (triggerAtMillis <= 0L) { + _uiState.update { it.copy(statusMessage = str(R.string.status_alarm_schedule_failed)) } + return false + } + val persisted = item.copy(nextTriggerAtMillis = triggerAtMillis) + val updated = AlarmModeRepository.upsert(getApplication(), persisted) + _uiState.update { + it.copy( + alarmModeItems = updated, + statusMessage = str( + if (isUpdate) R.string.status_alarm_updated_for else R.string.status_alarm_saved_for, + formatAlarmClockTime(persisted.hour, persisted.minute), + formatAlarmRepeatDays(getApplication(), persisted.repeatDays) + ) + ) + } + return true + } + + fun onAlarmModeEnabledChanged(alarmId: Long, enabled: Boolean) { + val current = AlarmModeRepository.find(getApplication(), alarmId) ?: return + if (!enabled) { + AlarmModeScheduler.cancel(getApplication(), alarmId) + val updated = AlarmModeRepository.upsert( + getApplication(), + current.copy(enabled = false, nextTriggerAtMillis = 0L) + ) + _uiState.update { it.copy(alarmModeItems = updated) } + return + } + if (!uiState.value.hasRequiredPermissions) { + _uiState.update { it.copy(statusMessage = str(R.string.status_grant_permissions_scheduling)) } + return + } + if (!uiState.value.isProviderEnabled) { + _uiState.update { it.copy(statusMessage = str(R.string.status_enable_calling_accounts)) } + return + } + if (!AlarmModeScheduler.canScheduleExact(getApplication())) { + _uiState.update { it.copy(statusMessage = str(R.string.status_enable_exact_alarms)) } + return + } + val enabledItem = current.copy(enabled = true) + val triggerAtMillis = AlarmModeScheduler.schedule(getApplication(), enabledItem) + if (triggerAtMillis <= 0L) { + _uiState.update { it.copy(statusMessage = str(R.string.status_alarm_schedule_failed)) } + return + } + val updated = AlarmModeRepository.upsert( + getApplication(), + enabledItem.copy(nextTriggerAtMillis = triggerAtMillis) + ) + _uiState.update { it.copy(alarmModeItems = updated) } + } + fun saveQuickTriggerPreset() { val customName = uiState.value.quickTriggerPresetName.trim() val result = QuickTriggerManager.saveCurrentDefaultsAsPreset( @@ -227,6 +494,62 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application refreshQuickTriggerPresets(status) } + fun setQuickTriggerDefaultPreset(slot: Int) { + val updated = QuickTriggerManager.setDefaultPresetSlot(getApplication(), slot) + val status = if (updated) { + str(R.string.status_quick_trigger_default_preset_set, slot) + } else { + str(R.string.status_preset_not_found) + } + refreshQuickTriggerPresets(status) + } + + fun onQuickTriggerPresetUseCustomAudioChange(slot: Int, enabled: Boolean) { + val updated = QuickTriggerManager.updatePresetAudioMode(getApplication(), slot, enabled) + val status = if (updated) { + if (enabled) { + str(R.string.status_quick_trigger_preset_audio_enabled) + } else { + str(R.string.status_quick_trigger_preset_audio_disabled) + } + } else { + str(R.string.status_preset_not_found) + } + refreshQuickTriggerPresets(status) + } + + fun onQuickTriggerPresetAudioSelected(slot: Int, uri: Uri?) { + if (uri == null) return + val app = getApplication() + val resolver = app.contentResolver + runCatching { + resolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val label = resolveDisplayName(uri) + val updated = QuickTriggerManager.updatePresetAudio( + context = app, + slot = slot, + audioUri = uri.toString(), + audioName = label + ) + val status = if (updated) { + str(R.string.status_quick_trigger_preset_audio_selected, label) + } else { + str(R.string.status_preset_not_found) + } + refreshQuickTriggerPresets(status) + } + + fun clearQuickTriggerPresetAudio(slot: Int) { + val cleared = QuickTriggerManager.clearPresetAudio(getApplication(), slot) + val status = if (cleared) { + str(R.string.status_quick_trigger_preset_audio_cleared) + } else { + str(R.string.status_preset_not_found) + } + refreshQuickTriggerPresets(status) + } + fun onCallerNameChange(value: String) { _uiState.update { it.copy(callerName = value) } } @@ -235,6 +558,106 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application _uiState.update { it.copy(callerNumber = value) } } + fun onCallerInputModeChange(mode: CallerInputMode) { + prefs.edit().putString(KEY_CALLER_INPUT_MODE, mode.name).apply() + _uiState.update { + it.copy( + callerInputMode = mode, + statusMessage = if (mode == CallerInputMode.CONTACT && it.selectedContact == null) { + str(R.string.status_select_contact_scheduling) + } else { + it.statusMessage + } + ) + } + } + + fun onContactPicked(uri: Uri?) { + if (uri == null) return + val contact = resolveContactFromUri(uri) ?: run { + _uiState.update { it.copy(statusMessage = str(R.string.status_contact_pick_failed)) } + return + } + selectContact(contact) + } + + fun selectContact(contact: CallContact) { + val state = uiState.value + val updatedPinned = state.pinnedContacts.map { + if (sameContact(it, contact)) contact else it + } + val updatedRecentBase = buildList { + var replaced = false + state.recentContacts.forEach { existing -> + if (sameContact(existing, contact)) { + add(contact) + replaced = true + } else { + add(existing) + } + } + if (!replaced) { + add(contact) + } + }.takeLast(MAX_RECENT_CONTACTS) + val updatedRecent = pruneRecentContacts( + recentContacts = updatedRecentBase, + pinnedContacts = updatedPinned + ) + + persistContactState( + selectedContact = contact, + pinned = updatedPinned, + recent = updatedRecent + ) + _uiState.update { + it.copy( + callerInputMode = CallerInputMode.CONTACT, + selectedContact = contact, + pinnedContacts = updatedPinned, + recentContacts = updatedRecent, + callerName = contact.displayName, + callerNumber = contact.phoneNumber + ) + } + } + + fun togglePinnedContact(contact: CallContact) { + val state = uiState.value + val isPinned = state.pinnedContacts.any { sameContact(it, contact) } + val updatedPinned = if (isPinned) { + state.pinnedContacts.filterNot { sameContact(it, contact) } + } else { + buildList { + add(contact) + addAll(state.pinnedContacts.filterNot { sameContact(it, contact) }) + }.take(MAX_PINNED_CONTACTS) + } + val updatedRecent = if (isPinned) { + pruneRecentContacts( + recentContacts = state.recentContacts, + pinnedContacts = updatedPinned + ) + } else { + val pinnedIndexInRecent = state.recentContacts.indexOfLast { sameContact(it, contact) } + val trimmedRecent = if (pinnedIndexInRecent >= 0) { + state.recentContacts.drop(pinnedIndexInRecent + 1) + } else { + state.recentContacts + } + pruneRecentContacts( + recentContacts = trimmedRecent, + pinnedContacts = updatedPinned + ) + } + persistContactState( + selectedContact = state.selectedContact, + pinned = updatedPinned, + recent = updatedRecent + ) + _uiState.update { it.copy(pinnedContacts = updatedPinned, recentContacts = updatedRecent) } + } + fun onDelaySelected(delaySeconds: Int) { _uiState.update { it.copy( @@ -362,25 +785,36 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application prefs.edit() .putString(KEY_AUDIO_URI, uri.toString()) .putString(KEY_AUDIO_NAME, displayName) + .putString(KEY_ANSWER_AUDIO_MODE, AnswerAudioMode.AUDIO_FILE.name) + .putBoolean(KEY_MP3_IVR_MODE_ENABLED, false) .apply() _uiState.update { it.copy( + answerAudioMode = AnswerAudioMode.AUDIO_FILE, selectedAudioUri = uri.toString(), selectedAudioName = displayName, + isMp3IvrModeEnabled = false, statusMessage = str(R.string.status_audio_selected, displayName) ) } } fun clearAudioSelection() { + val nextMode = if (uiState.value.answerAudioMode == AnswerAudioMode.AUDIO_FILE) { + AnswerAudioMode.SILENT + } else { + uiState.value.answerAudioMode + } prefs.edit() .remove(KEY_AUDIO_URI) .putString(KEY_AUDIO_NAME, str(R.string.default_audio_name)) + .putString(KEY_ANSWER_AUDIO_MODE, nextMode.name) .apply() _uiState.update { it.copy( + answerAudioMode = nextMode, selectedAudioUri = "", selectedAudioName = str(R.string.default_audio_name), statusMessage = str(R.string.status_audio_disabled) @@ -388,6 +822,38 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } } + fun onAnswerAudioModeChange(mode: AnswerAudioMode) { + val state = uiState.value + if (mode == AnswerAudioMode.AUDIO_FILE && state.selectedAudioUri.isBlank()) { + _uiState.update { it.copy(statusMessage = str(R.string.status_select_audio_file_first)) } + return + } + if (mode == AnswerAudioMode.MP3_IVR && state.mp3IvrFolderUri.isBlank()) { + _uiState.update { it.copy(statusMessage = str(R.string.status_select_mp3_ivr_folder)) } + return + } + + prefs.edit() + .putString(KEY_ANSWER_AUDIO_MODE, mode.name) + .putBoolean(KEY_MP3_IVR_MODE_ENABLED, mode == AnswerAudioMode.MP3_IVR) + .apply() + + _uiState.update { + it.copy( + answerAudioMode = mode, + isMp3IvrModeEnabled = mode == AnswerAudioMode.MP3_IVR, + statusMessage = str( + when (mode) { + AnswerAudioMode.SILENT -> R.string.status_answer_audio_silent + AnswerAudioMode.AUDIO_FILE -> R.string.status_answer_audio_file + AnswerAudioMode.CUSTOM_IVR -> R.string.status_answer_audio_custom_ivr + AnswerAudioMode.MP3_IVR -> R.string.status_answer_audio_mp3_ivr + } + ) + ) + } + } + fun onRecordingEnabledChange(enabled: Boolean) { prefs.edit().putBoolean(KEY_RECORDING_ENABLED, enabled).apply() @@ -561,6 +1027,56 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } } + fun onMp3IvrModeEnabledChange(enabled: Boolean) { + onAnswerAudioModeChange(if (enabled) AnswerAudioMode.MP3_IVR else AnswerAudioMode.SILENT) + } + + fun onMp3IvrFolderSelected(uri: Uri?) { + if (uri == null) return + val resolver = getApplication().contentResolver + runCatching { + resolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val folderName = readableTreeLabel(uri) + prefs.edit() + .putString(KEY_MP3_IVR_FOLDER_URI, uri.toString()) + .putString(KEY_MP3_IVR_FOLDER_NAME, folderName) + .putString(KEY_ANSWER_AUDIO_MODE, AnswerAudioMode.MP3_IVR.name) + .putBoolean(KEY_MP3_IVR_MODE_ENABLED, true) + .apply() + _uiState.update { + it.copy( + answerAudioMode = AnswerAudioMode.MP3_IVR, + isMp3IvrModeEnabled = true, + mp3IvrFolderUri = uri.toString(), + mp3IvrFolderName = folderName, + statusMessage = str(R.string.status_answer_audio_mp3_ivr) + ) + } + } + + fun clearMp3IvrFolderSelection() { + val nextMode = if (uiState.value.answerAudioMode == AnswerAudioMode.MP3_IVR) { + AnswerAudioMode.SILENT + } else { + uiState.value.answerAudioMode + } + prefs.edit() + .remove(KEY_MP3_IVR_FOLDER_URI) + .putString(KEY_MP3_IVR_FOLDER_NAME, str(R.string.settings_mp3_ivr_no_folder_selected)) + .putString(KEY_ANSWER_AUDIO_MODE, nextMode.name) + .putBoolean(KEY_MP3_IVR_MODE_ENABLED, false) + .apply() + _uiState.update { + it.copy( + answerAudioMode = nextMode, + isMp3IvrModeEnabled = false, + mp3IvrFolderUri = "", + mp3IvrFolderName = str(R.string.settings_mp3_ivr_no_folder_selected) + ) + } + } + fun saveProvider() { if (!uiState.value.hasRequiredPermissions) { _uiState.update { it.copy(statusMessage = str(R.string.status_grant_permissions_first)) } @@ -586,6 +1102,69 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application refreshProviderStatus() } + fun loadSimProviderOptions(): List { + val context = getApplication() + val hasPermission = context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED || + context.checkSelfPermission(Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED + if (!hasPermission) return emptyList() + + val subscriptionManager = context.getSystemService(SubscriptionManager::class.java) ?: return emptyList() + val telephonyManager = context.getSystemService(TelephonyManager::class.java) + return runCatching { + subscriptionManager.activeSubscriptionInfoList.orEmpty() + .mapNotNull { info -> + val simOperatorName = runCatching { + telephonyManager + ?.createForSubscriptionId(info.subscriptionId) + ?.simOperatorName + .orEmpty() + .trim() + }.getOrDefault("") + val displayName = info.displayName?.toString().orEmpty().trim() + val carrierName = info.carrierName?.toString().orEmpty().trim() + val providerName = listOf( + simOperatorName, + displayName, + carrierName + ).firstOrNull { candidate -> + candidate.isStableSimProviderName() + }.orEmpty() + if (providerName.isBlank()) return@mapNotNull null + SimProviderOption( + subscriptionId = info.subscriptionId, + displayName = providerName, + carrierName = carrierName.takeIf { it.isStableSimProviderName() }.orEmpty(), + slotIndex = info.simSlotIndex + ) + } + .distinctBy { option -> option.subscriptionId } + }.getOrDefault(emptyList()) + } + + fun applySimProviderName(option: SimProviderOption) { + val providerName = option.displayName.trim() + .ifBlank { option.carrierName.trim() } + .ifBlank { str(R.string.default_provider_name) } + val registered = if (uiState.value.hasRequiredPermissions) { + telecomHelper.registerOrUpdatePhoneAccount(providerName) + } else { + false + } + + prefs.edit().putString(KEY_PROVIDER_NAME, providerName).apply() + _uiState.update { + it.copy( + providerName = providerName, + statusMessage = when { + !uiState.value.hasRequiredPermissions -> str(R.string.status_grant_permissions_first) + registered -> str(R.string.status_provider_saved) + else -> str(R.string.status_provider_register_failed) + } + ) + } + refreshProviderStatus() + } + fun openCallingAccountsIntent(): Intent { return Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } @@ -631,14 +1210,23 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application return } - val number = state.callerNumber.trim() + val (resolvedName, resolvedNumber) = resolveCaller(state) + val number = resolvedNumber.trim() if (number.isBlank()) { - _uiState.update { it.copy(statusMessage = str(R.string.status_enter_caller_number_scheduling)) } + _uiState.update { + it.copy( + statusMessage = if (state.callerInputMode == CallerInputMode.CONTACT) { + str(R.string.status_select_contact_scheduling) + } else { + str(R.string.status_enter_caller_number_scheduling) + } + ) + } return } prefs.edit() - .putString(KEY_CALLER_NAME, state.callerName) + .putString(KEY_CALLER_NAME, resolvedName) .putString(KEY_CALLER_NUMBER, number) .apply() @@ -661,7 +1249,7 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application val telecomHelper = TelecomHelper(getApplication()) telecomHelper.registerOrUpdatePhoneAccount(state.providerName) val triggered = if (telecomHelper.isAccountEnabled()) { - telecomHelper.triggerIncomingCall(state.callerName, number) + telecomHelper.triggerIncomingCall(resolvedName, number) } else { false } @@ -687,7 +1275,7 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application val scheduled = FakeCallAlarmScheduler.scheduleExact( context = getApplication(), triggerAtMillis = triggerAtMillis, - callerName = state.callerName, + callerName = resolvedName, callerNumber = number, providerName = state.providerName ) @@ -754,6 +1342,37 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } } + private fun syncAlarmModeState() { + val now = System.currentTimeMillis() + var changed = false + val updated = AlarmModeRepository.load(getApplication()).map { alarm -> + if (!alarm.enabled) { + if (alarm.nextTriggerAtMillis != 0L) { + changed = true + alarm.copy(nextTriggerAtMillis = 0L) + } else { + alarm + } + } else if (alarm.nextTriggerAtMillis <= now + 1_000L) { + val next = AlarmModeScheduler.computeNextTriggerAtMillis(alarm) + if (next > 0L && next != alarm.nextTriggerAtMillis) { + changed = true + alarm.copy(nextTriggerAtMillis = next) + } else { + alarm + } + } else { + alarm + } + } + if (changed) { + AlarmModeRepository.replaceAll(getApplication(), updated) + } + if (uiState.value.alarmModeItems != updated) { + _uiState.update { it.copy(alarmModeItems = updated) } + } + } + private fun ensurePhoneAccountRegistered() { telecomHelper.registerOrUpdatePhoneAccount(uiState.value.providerName) } @@ -783,12 +1402,16 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } private fun saveQuickTriggerDefaults(state: FakeCallUiState) { + val existingDefaults = QuickTriggerManager.loadDefaults(getApplication()) QuickTriggerManager.saveDefaults( context = getApplication(), defaults = QuickTriggerDefaults( callerName = state.quickTriggerCallerName, callerNumber = state.quickTriggerCallerNumber, - delaySeconds = state.quickTriggerDelaySeconds + delaySeconds = state.quickTriggerDelaySeconds, + useCustomAudioOverride = existingDefaults.useCustomAudioOverride, + customAudioUri = existingDefaults.customAudioUri, + customAudioName = existingDefaults.customAudioName ) ) _uiState.update { @@ -801,9 +1424,11 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } private fun refreshQuickTriggerPresets(statusMessage: String) { + val application = getApplication() _uiState.update { it.copy( - quickTriggerPresets = QuickTriggerManager.loadPresets(getApplication()), + quickTriggerPresets = QuickTriggerManager.loadPresets(application), + quickTriggerDefaultPresetSlot = QuickTriggerManager.loadDefaultPresetSlot(application), statusMessage = statusMessage ) } @@ -815,6 +1440,52 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application return minutes * 60 + seconds } + private fun loadAnswerAudioMode(context: Context): AnswerAudioMode { + val selectedAudioUri = prefs.getString(KEY_AUDIO_URI, "").orEmpty() + val mp3FolderUri = prefs.getString(KEY_MP3_IVR_FOLDER_URI, "").orEmpty() + val storedMode = prefs.getString(KEY_ANSWER_AUDIO_MODE, "").orEmpty() + val parsedMode = runCatching { AnswerAudioMode.valueOf(storedMode) }.getOrNull() + val legacyMode = if (parsedMode == null) { + val ivrConfig = ivrStore.load(context) + val rootNode = ivrConfig?.nodes?.get(ivrConfig.rootId) + when { + prefs.getBoolean(KEY_MP3_IVR_MODE_ENABLED, false) && mp3FolderUri.isNotBlank() -> AnswerAudioMode.MP3_IVR + rootNode?.audioUri?.isNotBlank() == true -> AnswerAudioMode.CUSTOM_IVR + selectedAudioUri.isNotBlank() -> AnswerAudioMode.AUDIO_FILE + ivrConfig?.nodes?.isNotEmpty() == true -> AnswerAudioMode.CUSTOM_IVR + else -> AnswerAudioMode.SILENT + } + } else { + parsedMode + } + + val normalized = when (legacyMode) { + AnswerAudioMode.AUDIO_FILE -> if (selectedAudioUri.isNotBlank()) legacyMode else AnswerAudioMode.SILENT + AnswerAudioMode.MP3_IVR -> if (mp3FolderUri.isNotBlank()) legacyMode else AnswerAudioMode.SILENT + else -> legacyMode + } + + prefs.edit() + .putString(KEY_ANSWER_AUDIO_MODE, normalized.name) + .putBoolean(KEY_MP3_IVR_MODE_ENABLED, normalized == AnswerAudioMode.MP3_IVR) + .apply() + + return normalized + } + + private fun resolveCaller(state: FakeCallUiState): Pair { + return if (state.callerInputMode == CallerInputMode.CONTACT) { + val contact = state.selectedContact + if (contact != null) { + contact.displayName to contact.phoneNumber + } else { + "" to "" + } + } else { + state.callerName to state.callerNumber + } + } + private fun computeNextExactTriggerMillis(hour: Int, minute: Int): Long { val now = ZonedDateTime.now() var target = now.withHour(hour).withMinute(minute).withSecond(0).withNano(0) @@ -847,6 +1518,25 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } } + private fun formatAlarmClockTime(hour: Int, minute: Int): String { + val locale = getApplication().resources.configuration.locales[0] ?: Locale.getDefault() + val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) + return ZonedDateTime.now() + .withHour(hour.coerceIn(0, 23)) + .withMinute(minute.coerceIn(0, 59)) + .withSecond(0) + .withNano(0) + .toLocalTime() + .format(formatter) + } + + private fun formatAlarmRepeatDays(context: Context, days: Set): String { + if (days.isEmpty()) return str(R.string.alarm_repeat_once) + return days.sorted().joinToString(", ") { day -> + AlarmModeScheduler.dayLabel(context, day) + } + } + private fun parseCustomPresets(raw: String): List { if (raw.isBlank()) return emptyList() return raw.split("|").mapNotNull { token -> @@ -923,11 +1613,268 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application } } + private fun resolveContactFromUri(uri: Uri): CallContact? { + val resolver = getApplication().contentResolver + val phoneProjection = arrayOf( + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI, + ContactsContract.CommonDataKinds.Phone.PHOTO_URI + ) + + runCatching { + resolver.query(uri, phoneProjection, null, null, null)?.use { cursor -> + if (!cursor.moveToFirst()) return@use null + val idIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID) + val lookupIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY) + val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME) + val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + val thumbIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI) + val photoIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_URI) + + val contactId = if (idIndex >= 0) cursor.getLong(idIndex) else 0L + val lookupKey = if (lookupIndex >= 0) cursor.getString(lookupIndex).orEmpty() else "" + val number = if (numberIndex >= 0) cursor.getString(numberIndex).orEmpty().trim() else "" + if (number.isBlank()) return@use null + val name = if (nameIndex >= 0) cursor.getString(nameIndex).orEmpty() else "" + val thumbnailUri = if (thumbIndex >= 0) cursor.getString(thumbIndex).orEmpty() else "" + val photoUri = if (photoIndex >= 0) cursor.getString(photoIndex).orEmpty() else "" + val resolvedPhotoUri = thumbnailUri.ifBlank { photoUri } + val avatarBase64 = encodeContactAvatarBase64( + contactId = contactId, + lookupKey = lookupKey, + photoUri = resolvedPhotoUri + ) + + return CallContact( + id = contactId, + displayName = name.ifBlank { number }, + phoneNumber = number, + photoUri = resolvedPhotoUri, + avatarBase64 = avatarBase64 + ) + } + } + + val contactProjection = arrayOf( + ContactsContract.Contacts._ID, + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.Contacts.PHOTO_URI + ) + var id = 0L + var name = "" + var photoUri = "" + runCatching { + resolver.query(uri, contactProjection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val idIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID) + val nameIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME) + val photoIndex = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI) + if (idIndex >= 0) id = cursor.getLong(idIndex) + if (nameIndex >= 0) name = cursor.getString(nameIndex).orEmpty() + if (photoIndex >= 0) photoUri = cursor.getString(photoIndex).orEmpty() + } + } + } + if (id <= 0L) return null + val number = resolvePrimaryNumberForContact(id).trim() + if (number.isBlank()) return null + return CallContact( + id = id, + displayName = name.ifBlank { number }, + phoneNumber = number, + photoUri = photoUri, + avatarBase64 = encodeContactAvatarBase64(contactId = id, lookupKey = "", photoUri = photoUri) + ) + } + + private fun encodeContactAvatarBase64( + contactId: Long, + lookupKey: String, + photoUri: String + ): String { + val resolver = getApplication().contentResolver + + fun decodeFromUri(uriString: String): Bitmap? { + if (uriString.isBlank()) return null + return runCatching { + resolver.openInputStream(Uri.parse(uriString))?.use(BitmapFactory::decodeStream) + }.getOrNull() + } + + val directPhoto = decodeFromUri(photoUri) + val lookupPhoto = if (directPhoto == null && contactId > 0L) { + val lookupUri = if (lookupKey.isNotBlank()) { + ContactsContract.Contacts.getLookupUri(contactId, lookupKey) + } else { + ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId) + } + runCatching { + ContactsContract.Contacts.openContactPhotoInputStream(resolver, lookupUri, true) + ?.use(BitmapFactory::decodeStream) + }.getOrNull() + } else { + null + } + + val bitmap = directPhoto ?: lookupPhoto ?: return "" + return bitmapToBase64(bitmap) + } + + private fun bitmapToBase64(bitmap: Bitmap): String { + val scaled = Bitmap.createScaledBitmap(bitmap, 128, 128, true) + val bytes = ByteArrayOutputStream() + scaled.compress(Bitmap.CompressFormat.PNG, 100, bytes) + if (scaled !== bitmap) { + scaled.recycle() + } + return Base64.encodeToString(bytes.toByteArray(), Base64.NO_WRAP) + } + + private fun resolvePrimaryNumberForContact(contactId: Long): String { + val resolver = getApplication().contentResolver + val projection = arrayOf( + ContactsContract.CommonDataKinds.Phone.NUMBER + ) + val selection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID}=?" + val args = arrayOf(contactId.toString()) + return runCatching { + resolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + projection, + selection, + args, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val index = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + if (index >= 0) cursor.getString(index).orEmpty() else "" + } else { + "" + } + }.orEmpty() + }.getOrDefault("") + } + + private fun String.isStableSimProviderName(): Boolean { + val value = trim() + if (value.isBlank()) return false + val compact = value + .lowercase(Locale.ROOT) + .filter { it.isLetterOrDigit() } + return compact !in UNSTABLE_SIM_PROVIDER_NAMES + } + + private fun persistContactState( + selectedContact: CallContact?, + pinned: List, + recent: List + ) { + prefs.edit() + .putString(KEY_SELECTED_CONTACT, serializeContact(selectedContact)) + .putString(KEY_PINNED_CONTACTS, serializeContactList(pinned)) + .putString(KEY_RECENT_CONTACTS, serializeContactList(recent)) + .apply() + } + + private fun pruneRecentContacts( + recentContacts: List, + pinnedContacts: List + ): List { + val limit = if (pinnedContacts.isNotEmpty()) 1 else 3 + val deduped = buildList { + recentContacts.forEach { contact -> + if (none { existing -> sameContact(existing, contact) }) { + add(contact) + } + } + } + return deduped + .filterNot { recent -> pinnedContacts.any { sameContact(it, recent) } } + .takeLast(limit) + } + + private fun sameContact(a: CallContact, b: CallContact): Boolean { + return if (a.id > 0 && b.id > 0) a.id == b.id else a.phoneNumber == b.phoneNumber + } + + private fun serializeContact(contact: CallContact?): String { + if (contact == null) return "" + return JSONObject().apply { + put("id", contact.id) + put("name", contact.displayName) + put("number", contact.phoneNumber) + put("photoUri", contact.photoUri) + put("avatarBase64", contact.avatarBase64) + }.toString() + } + + private fun parseContact(raw: String): CallContact? { + if (raw.isBlank()) return null + return runCatching { + val obj = JSONObject(raw) + val number = obj.optString("number").orEmpty() + if (number.isBlank()) return@runCatching null + CallContact( + id = obj.optLong("id", 0L), + displayName = obj.optString("name").orEmpty().ifBlank { number }, + phoneNumber = number, + photoUri = obj.optString("photoUri").orEmpty(), + avatarBase64 = obj.optString("avatarBase64").orEmpty() + ) + }.getOrNull() + } + + private fun serializeContactList(contacts: List): String { + return JSONArray().apply { + contacts.forEach { contact -> + put( + JSONObject().apply { + put("id", contact.id) + put("name", contact.displayName) + put("number", contact.phoneNumber) + put("photoUri", contact.photoUri) + put("avatarBase64", contact.avatarBase64) + } + ) + } + }.toString() + } + + private fun parseContactList(raw: String): List { + if (raw.isBlank()) return emptyList() + return runCatching { + val array = JSONArray(raw) + buildList { + for (index in 0 until array.length()) { + val obj = array.optJSONObject(index) ?: continue + val number = obj.optString("number").orEmpty().trim() + if (number.isBlank()) continue + add( + CallContact( + id = obj.optLong("id", 0L), + displayName = obj.optString("name").orEmpty().ifBlank { number }, + phoneNumber = number, + photoUri = obj.optString("photoUri").orEmpty(), + avatarBase64 = obj.optString("avatarBase64").orEmpty() + ) + ) + } + } + }.getOrDefault(emptyList()) + } + companion object { private const val PREFS_NAME = "fake_call_prefs" private const val KEY_PROVIDER_NAME = "provider_name" private const val KEY_CALLER_NAME = "caller_name" private const val KEY_CALLER_NUMBER = "caller_number" + private const val KEY_CALLER_INPUT_MODE = "caller_input_mode" + private const val KEY_SELECTED_CONTACT = "selected_contact" + private const val KEY_PINNED_CONTACTS = "pinned_contacts" + private const val KEY_RECENT_CONTACTS = "recent_contacts" private const val KEY_DELAY_SECONDS = "delay_seconds" private const val KEY_SCHEDULE_KIND = "schedule_kind" private const val KEY_CUSTOM_COUNTDOWN_MINUTES = "custom_countdown_minutes" @@ -937,16 +1884,42 @@ class FakeCallViewModel(application: Application) : AndroidViewModel(application private const val KEY_CUSTOM_PRESETS = "custom_presets" private const val KEY_TIMER_ENDS_AT = "timer_ends_at" private const val KEY_ACTIVE_PRESET_SLOT = "quick_trigger_active_preset_slot" + private const val KEY_ANSWER_AUDIO_MODE = "answer_audio_mode" private const val KEY_AUDIO_URI = "audio_uri" private const val KEY_AUDIO_NAME = "audio_name" private const val KEY_RECORDING_ENABLED = "recording_enabled" private const val KEY_RECORDINGS_TREE_URI = "recordings_tree_uri" private const val KEY_RECORDINGS_FOLDER_NAME = "recordings_folder_name" + private const val KEY_MP3_IVR_MODE_ENABLED = "mp3_ivr_mode_enabled" + private const val KEY_MP3_IVR_FOLDER_URI = "mp3_ivr_folder_uri" + private const val KEY_MP3_IVR_FOLDER_NAME = "mp3_ivr_folder_name" private const val KEY_ONBOARDING_COMPLETE = "onboarding_complete" private const val KEY_QUICK_TRIGGER_PRESET_NAME = "quick_trigger_preset_name" + private const val KEY_CALL_RING_TIMEOUT_SECONDS = "call_ring_timeout_seconds" + private const val KEY_ALARM_RING_TIMEOUT_SECONDS = "alarm_ring_timeout_seconds" + private const val DEFAULT_CALL_RING_TIMEOUT_SECONDS = 45 + private const val DEFAULT_ALARM_RING_TIMEOUT_SECONDS = 0 + private const val DEFAULT_RECORDING_ENABLED = false + private const val MAX_RECENT_CONTACTS = 12 + private const val MAX_PINNED_CONTACTS = 8 + private val UNSTABLE_SIM_PROVIDER_NAMES = setOf( + "wifi", + "wificalling", + "wlan", + "wlancalling", + "vowifi" + ) fun formatDelay(context: Context, seconds: Int): String { return DelayFormatter.formatLong(context, seconds) } + + fun formatRingTimeout(context: Context, seconds: Int): String { + val safeSeconds = seconds.coerceAtLeast(0) + if (safeSeconds == 0) { + return context.getString(R.string.settings_ring_timeout_unlimited) + } + return DelayFormatter.formatLong(context, safeSeconds) + } } } diff --git a/app/src/main/java/com/upnp/fakeCall/FakeConnection.kt b/app/src/main/java/com/upnp/fakeCall/FakeConnection.kt index b4f2ab0..f6c0ce7 100644 --- a/app/src/main/java/com/upnp/fakeCall/FakeConnection.kt +++ b/app/src/main/java/com/upnp/fakeCall/FakeConnection.kt @@ -12,6 +12,8 @@ import android.media.MediaRecorder import android.net.Uri import android.os.Build import android.os.Environment +import android.os.Handler +import android.os.Looper import android.provider.DocumentsContract import android.provider.MediaStore import android.telecom.CallAudioState @@ -19,6 +21,8 @@ import android.telecom.Connection import android.telecom.DisconnectCause import android.telecom.PhoneAccount import android.telecom.TelecomManager +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener import android.util.Log import androidx.core.content.ContextCompat import com.upnp.fakeCall.ivr.IvrConfigStore @@ -31,7 +35,8 @@ import java.util.Locale class FakeConnection( private val context: Context, private val callerName: String, - private val callerNumber: String + private val callerNumber: String, + private val ringTimeoutSeconds: Int ) : Connection() { private var mediaPlayer: MediaPlayer? = null @@ -43,6 +48,19 @@ class FakeConnection( private val ivrStore = IvrConfigStore() private var ivrStateMachine: IvrStateMachine? = null private var ivrAudioAttributes: AudioAttributes? = null + private var ttsEngine: TextToSpeech? = null + private var pendingTtsRequest: TtsRequest? = null + private var repeatingTtsMessage: String? = null + private val folderNavStack = mutableListOf() + private val runtimeOverrides: RuntimeOverrides = consumeRuntimeOverrides() + private val ringTimeoutHandler = Handler(Looper.getMainLooper()) + private val ringTimeoutRunnable = Runnable { + if (!wasAnswered) { + disconnectWithCause(DisconnectCause.MISSED) + } + } + private var wasAnswered = false + private var snoozeTriggered = false init { val displayName = callerName.ifBlank { callerNumber } @@ -52,33 +70,41 @@ class FakeConnection( setAudioModeIsVoip(true) setInitializing() setRinging() + scheduleRingTimeoutIfNeeded() } override fun onAnswer() { + cancelRingTimeout() + wasAnswered = true setActive() runCatching { audioManager.mode = AudioManager.MODE_IN_COMMUNICATION } runCatching { - // Start on earpiece, but allow system UI to switch routes afterward. - setAudioRoute(CallAudioState.ROUTE_EARPIECE) - audioManager.isSpeakerphoneOn = false - runCatching { audioManager.stopBluetoothSco() } - runCatching { audioManager.isBluetoothScoOn = false } + val defaultRoute = if (runtimeOverrides.speakerDefault == AlarmSpeakerDefault.SPEAKER) { + CallAudioState.ROUTE_SPEAKER + } else { + CallAudioState.ROUTE_EARPIECE + } + setAudioRoute(defaultRoute) + applyAudioRoute(defaultRoute) } - startVoicePlayback() maybeStartMicRecording() + startVoicePlayback() } override fun onReject() { + cancelRingTimeout() disconnectWithCause(DisconnectCause.REJECTED) } override fun onDisconnect() { + cancelRingTimeout() disconnectWithCause(DisconnectCause.LOCAL) } override fun onAbort() { + cancelRingTimeout() disconnectWithCause(DisconnectCause.CANCELED) } @@ -123,6 +149,12 @@ class FakeConnection( override fun onPlayDtmfTone(c: Char) { super.onPlayDtmfTone(c) + if (c == '1' && runtimeOverrides.snoozeEnabled) { + triggerSnooze() + disconnectWithCause(DisconnectCause.LOCAL) + return + } + if (handleFolderModeDtmf(c)) return val machine = ivrStateMachine ?: return val next = machine.handleDtmf(c) ?: return val uriString = next.audioUri @@ -136,7 +168,13 @@ class FakeConnection( } private fun disconnectWithCause(code: Int) { + cancelRingTimeout() + if (!wasAnswered && runtimeOverrides.snoozeEnabled) { + triggerSnooze() + } stopAndReleasePlayer() + shutdownTts() + folderNavStack.clear() stopAndReleaseRecording() runCatching { audioManager.mode = AudioManager.MODE_NORMAL @@ -145,6 +183,17 @@ class FakeConnection( destroy() } + private fun scheduleRingTimeoutIfNeeded() { + val timeoutMillis = ringTimeoutSeconds.coerceAtLeast(0) * 1_000L + if (timeoutMillis <= 0L) return + ringTimeoutHandler.removeCallbacks(ringTimeoutRunnable) + ringTimeoutHandler.postDelayed(ringTimeoutRunnable, timeoutMillis) + } + + private fun cancelRingTimeout() { + ringTimeoutHandler.removeCallbacks(ringTimeoutRunnable) + } + private fun startVoicePlayback() { stopAndReleasePlayer() @@ -152,7 +201,55 @@ class FakeConnection( val audioAttributes = buildVoiceAudioAttributes() ivrAudioAttributes = audioAttributes + if (runtimeOverrides.messageMode == RuntimeMessageMode.TTS) { + val message = runtimeOverrides.ttsMessage.ifBlank { + if (callerName.isNotBlank()) { + context.getString(R.string.alarm_tts_default_message_with_name, callerName) + } else { + context.getString(R.string.alarm_tts_default_message) + } + } + speakPrompt(message, repeat = runtimeOverrides.repeatTtsMessage) + return + } + if (runtimeOverrides.customAudioUri.isNotBlank()) { + val runtimeUri = runCatching { Uri.parse(runtimeOverrides.customAudioUri) }.getOrNull() + if (runtimeUri == null) { + Log.i(TAG, "Runtime audio override was invalid; skipping call playback.") + return + } + val started = startPlayerFromUri(runtimeUri, audioAttributes) + if (!started) { + Log.e(TAG, "Failed to start runtime call playback for uri=$runtimeUri") + } + return + } + when (loadAnswerAudioMode()) { + AnswerAudioMode.SILENT -> { + Log.i(TAG, "Answer audio mode is silent; skipping call playback.") + } + AnswerAudioMode.AUDIO_FILE -> { + val selectedUri = loadSelectedAudioUri() + if (selectedUri == null) { + Log.i(TAG, "Answer audio mode is audio file, but no audio file is selected.") + return + } + val started = startPlayerFromUri(selectedUri, audioAttributes) + if (!started) { + Log.e(TAG, "Failed to start call playback for uri=$selectedUri") + } + } + AnswerAudioMode.CUSTOM_IVR -> { + startCustomIvrMode(audioAttributes) + } + AnswerAudioMode.MP3_IVR -> { + startFolderMode() + } + } + } + + private fun startCustomIvrMode(audioAttributes: AudioAttributes) { val ivrConfig = ivrStore.load(context) ivrStateMachine = ivrConfig?.let { IvrStateMachine(it) } @@ -162,21 +259,310 @@ class FakeConnection( ?.takeIf { it.isNotBlank() } ?.let { runCatching { Uri.parse(it) }.getOrNull() } - if (ivrAudio != null) { - val started = startPlayerFromUri(ivrAudio, audioAttributes) - if (started) return + if (ivrAudio == null) { + Log.i(TAG, "Custom IVR mode is selected, but the root node has no audio.") + return + } + + val started = startPlayerFromUri(ivrAudio, audioAttributes) + if (!started) { + Log.e(TAG, "Failed to start custom IVR root audio for uri=$ivrAudio") } + } + + private fun startFolderMode() { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - val selectedUri = loadSelectedAudioUri() - if (selectedUri == null) { - Log.i(TAG, "No audio file selected; skipping call playback.") + ivrStateMachine = null + val rootUri = prefs.getString(KEY_MP3_IVR_FOLDER_URI, "") + .orEmpty() + .takeIf { it.isNotBlank() } + ?.let { runCatching { Uri.parse(it) }.getOrNull() } + if (rootUri == null) { + folderNavStack.clear() + speakFolderPrompt(context.getString(R.string.status_select_mp3_ivr_folder)) return } - val started = startPlayerFromUri(selectedUri, audioAttributes) + val rootName = prefs.getString( + KEY_MP3_IVR_FOLDER_NAME, + context.getString(R.string.settings_mp3_ivr_no_folder_selected) + ).orEmpty().ifBlank { context.getString(R.string.settings_mp3_ivr_no_folder_selected) } + + val entries = listFolderEntries(rootUri) + folderNavStack.clear() + folderNavStack.add( + FolderNavState( + folderUri = rootUri, + folderName = rootName, + entries = entries + ) + ) + if (entries.isEmpty()) { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_empty_folder)) + return + } + + speakCurrentFolderMenu() + } + + private fun handleFolderModeDtmf(digit: Char): Boolean { + val current = folderNavStack.lastOrNull() ?: return false + when (digit) { + '#' -> { + val hasNext = (current.pageIndex + 1) * FOLDER_PAGE_SIZE < current.entries.size + if (hasNext) { + current.pageIndex += 1 + } else { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_no_next_page)) + } + speakCurrentFolderMenu() + return true + } + '*' -> { + if (current.pageIndex > 0) { + current.pageIndex -= 1 + } else { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_no_previous_page)) + } + speakCurrentFolderMenu() + return true + } + '0' -> { + if (folderNavStack.size > 1) { + folderNavStack.removeLast() + speakCurrentFolderMenu() + } else { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_root_folder)) + } + return true + } + in '1'..'9' -> { + val item = currentPageEntries(current).getOrNull(digit - '1') + if (item == null) { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_invalid_selection)) + return true + } + if (item.isDirectory) { + openFolderEntry(item) + } else { + playFolderAudioEntry(item) + } + return true + } + else -> return true + } + } + + private fun openFolderEntry(entry: FolderNavEntry) { + val nestedEntries = listFolderEntries(entry.uri) + folderNavStack.add( + FolderNavState( + folderUri = entry.uri, + folderName = entry.displayName, + entries = nestedEntries + ) + ) + if (nestedEntries.isEmpty()) { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_empty_folder)) + } + speakCurrentFolderMenu() + } + + private fun playFolderAudioEntry(entry: FolderNavEntry) { + val attrs = ivrAudioAttributes ?: buildVoiceAudioAttributes() + runCatching { ttsEngine?.stop() } + stopAndReleasePlayer() + val started = startPlayerFromUri( + uri = entry.uri, + audioAttributes = attrs, + loop = false, + onCompletion = { speakCurrentFolderMenu() } + ) if (!started) { - Log.e(TAG, "Failed to start call playback for uri=$selectedUri") + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_audio_failed)) + } + } + + private fun speakCurrentFolderMenu() { + val state = folderNavStack.lastOrNull() ?: return + val entries = currentPageEntries(state) + if (entries.isEmpty()) { + speakFolderPrompt(context.getString(R.string.tts_mp3_ivr_empty_folder)) + return + } + + val promptParts = mutableListOf() + promptParts += context.getString(R.string.tts_mp3_ivr_menu_intro, state.folderName) + val totalPages = ((state.entries.size - 1) / FOLDER_PAGE_SIZE) + 1 + if (totalPages > 1) { + promptParts += context.getString( + R.string.tts_mp3_ivr_page_announcement, + state.pageIndex + 1, + totalPages + ) + } + + entries.forEachIndexed { index, entry -> + val key = index + 1 + promptParts += if (entry.isDirectory) { + context.getString(R.string.tts_mp3_ivr_menu_item_folder, key, entry.displayName) + } else { + context.getString(R.string.tts_mp3_ivr_menu_item_audio, key, entry.displayName) + } + } + + if ((state.pageIndex + 1) * FOLDER_PAGE_SIZE < state.entries.size) { + promptParts += context.getString(R.string.tts_mp3_ivr_next_page_hint) } + if (state.pageIndex > 0) { + promptParts += context.getString(R.string.tts_mp3_ivr_previous_page_hint) + } + if (folderNavStack.size > 1) { + promptParts += context.getString(R.string.tts_mp3_ivr_back_folder_hint) + } + + speakFolderPrompt(promptParts.joinToString(" ")) + } + + private fun currentPageEntries(state: FolderNavState): List { + val from = state.pageIndex * FOLDER_PAGE_SIZE + if (from >= state.entries.size) return emptyList() + return state.entries.drop(from).take(FOLDER_PAGE_SIZE) + } + + private fun listFolderEntries(folderUri: Uri): List { + val resolver = context.contentResolver + val folderId = runCatching { DocumentsContract.getDocumentId(folderUri) }.getOrElse { + DocumentsContract.getTreeDocumentId(folderUri) + } + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, folderId) + + val entries = mutableListOf() + val projection = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + ) + + runCatching { + resolver.query(childrenUri, projection, null, null, null)?.use { cursor -> + val idCol = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val nameCol = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val mimeCol = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) + while (cursor.moveToNext()) { + val docId = if (idCol >= 0) cursor.getString(idCol) else null + val name = if (nameCol >= 0) cursor.getString(nameCol) else null + val mime = if (mimeCol >= 0) cursor.getString(mimeCol) else null + if (docId.isNullOrBlank()) continue + val isDirectory = mime == DocumentsContract.Document.MIME_TYPE_DIR + val isAudio = !isDirectory && looksLikeAudio(mime, name) + if (!isDirectory && !isAudio) continue + + val childUri = DocumentsContract.buildDocumentUriUsingTree(folderUri, docId) + entries += FolderNavEntry( + uri = childUri, + displayName = name.orEmpty().ifBlank { context.getString(R.string.label_unknown) }, + isDirectory = isDirectory + ) + } + } + } + + return entries.sortedWith( + compareByDescending { it.isDirectory } + .thenBy { it.displayName.lowercase(Locale.getDefault()) } + ) + } + + private fun looksLikeAudio(mimeType: String?, displayName: String?): Boolean { + if (mimeType?.startsWith("audio/") == true) return true + val lowerName = displayName.orEmpty().lowercase(Locale.getDefault()) + return lowerName.endsWith(".mp3") || + lowerName.endsWith(".wav") || + lowerName.endsWith(".m4a") || + lowerName.endsWith(".aac") || + lowerName.endsWith(".ogg") || + lowerName.endsWith(".flac") + } + + private fun speakFolderPrompt(message: String) { + speakPrompt(message, repeat = false) + } + + private fun speakPrompt(message: String, repeat: Boolean) { + if (message.isBlank()) return + val existing = ttsEngine + if (existing != null) { + speakWithTts(existing, message, repeat) + return + } + pendingTtsRequest = TtsRequest(message = message, repeat = repeat) + initializeTtsIfNeeded() + } + + private fun speakWithTts(tts: TextToSpeech, message: String, repeat: Boolean) { + repeatingTtsMessage = if (repeat) message else null + runCatching { + tts.stop() + tts.speak( + message, + TextToSpeech.QUEUE_FLUSH, + null, + if (repeat) TTS_REPEAT_UTTERANCE_ID else "tts_${System.currentTimeMillis()}" + ) + } + } + + private fun initializeTtsIfNeeded() { + if (ttsEngine != null) return + var newEngine: TextToSpeech? = null + newEngine = TextToSpeech(context.applicationContext) { status -> + if (status != TextToSpeech.SUCCESS) { + runCatching { newEngine?.shutdown() } + return@TextToSpeech + } + ttsEngine = newEngine + runCatching { + newEngine?.language = Locale.getDefault() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + runCatching { + newEngine?.setAudioAttributes(buildVoiceAudioAttributes()) + } + } + newEngine?.setOnUtteranceProgressListener(object : UtteranceProgressListener() { + override fun onStart(utteranceId: String?) = Unit + + override fun onDone(utteranceId: String?) { + if (utteranceId != TTS_REPEAT_UTTERANCE_ID) return + val message = repeatingTtsMessage ?: return + ringTimeoutHandler.postDelayed({ + val activeEngine = ttsEngine ?: return@postDelayed + if (activeEngine === newEngine && repeatingTtsMessage == message) { + speakWithTts(activeEngine, message, repeat = true) + } + }, TTS_REPEAT_DELAY_MILLIS) + } + + @Deprecated("Deprecated in Java") + override fun onError(utteranceId: String?) = Unit + }) + pendingTtsRequest?.let { request -> + speakWithTts(newEngine!!, request.message, request.repeat) + } + pendingTtsRequest = null + } + } + + private fun shutdownTts() { + pendingTtsRequest = null + repeatingTtsMessage = null + ttsEngine?.let { tts -> + runCatching { tts.stop() } + runCatching { tts.shutdown() } + } + ttsEngine = null } private fun maybeStartMicRecording() { @@ -200,7 +586,6 @@ class FakeConnection( } recorder.apply { - // MIC is more stable for continuous capture during self-managed calls. setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) @@ -222,17 +607,25 @@ class FakeConnection( } } - private fun startPlayerFromUri(uri: Uri, audioAttributes: AudioAttributes): Boolean { + private fun startPlayerFromUri( + uri: Uri, + audioAttributes: AudioAttributes, + loop: Boolean = true, + onCompletion: (() -> Unit)? = null + ): Boolean { return runCatching { val player = MediaPlayer().apply { setAudioAttributes(audioAttributes) setDataSource(context, uri) - isLooping = true + isLooping = loop setOnErrorListener { _, what, extra -> Log.e(TAG, "MediaPlayer error what=$what extra=$extra for uri=$uri") stopAndReleasePlayer() true } + if (onCompletion != null) { + setOnCompletionListener { onCompletion() } + } prepare() start() } @@ -272,9 +665,14 @@ class FakeConnection( val selectedTreeUri = loadRecordingsTreeUri() if (selectedTreeUri != null) { val fileUri = runCatching { + val treeDocumentId = DocumentsContract.getTreeDocumentId(selectedTreeUri) + val parentDocumentUri = DocumentsContract.buildDocumentUriUsingTree( + selectedTreeUri, + treeDocumentId + ) DocumentsContract.createDocument( resolver, - selectedTreeUri, + parentDocumentUri, "audio/mp4", filename ) @@ -311,7 +709,7 @@ class FakeConnection( private fun isRecordingEnabled(): Boolean { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - return prefs.getBoolean(KEY_RECORDING_ENABLED, true) + return prefs.getBoolean(KEY_RECORDING_ENABLED, DEFAULT_RECORDING_ENABLED) } private fun hasRecordAudioPermission(): Boolean { @@ -340,6 +738,129 @@ class FakeConnection( return runCatching { Uri.parse(value) }.getOrNull() } + private fun loadAnswerAudioMode(): AnswerAudioMode { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val stored = prefs.getString(KEY_ANSWER_AUDIO_MODE, "").orEmpty() + val selectedAudioUri = prefs.getString(KEY_AUDIO_URI, "").orEmpty() + val mp3FolderUri = prefs.getString(KEY_MP3_IVR_FOLDER_URI, "").orEmpty() + val parsed = runCatching { AnswerAudioMode.valueOf(stored) }.getOrNull() + val legacyMode = if (parsed == null) { + when { + prefs.getBoolean(KEY_MP3_IVR_MODE_ENABLED, false) && mp3FolderUri.isNotBlank() -> AnswerAudioMode.MP3_IVR + selectedAudioUri.isNotBlank() -> AnswerAudioMode.AUDIO_FILE + else -> AnswerAudioMode.SILENT + } + } else { + parsed + } + return when (legacyMode) { + AnswerAudioMode.AUDIO_FILE -> if (selectedAudioUri.isNotBlank()) legacyMode else AnswerAudioMode.SILENT + AnswerAudioMode.MP3_IVR -> if (mp3FolderUri.isNotBlank()) legacyMode else AnswerAudioMode.SILENT + else -> legacyMode + } + } + + private fun consumeRuntimeOverrides(): RuntimeOverrides { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val hasRuntimeAudioOverride = prefs.getBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + val customAudioUri = if (hasRuntimeAudioOverride) { + prefs.getString(KEY_RUNTIME_AUDIO_OVERRIDE_URI, "").orEmpty() + } else { + "" + } + val messageMode = when (prefs.getString(KEY_RUNTIME_MESSAGE_MODE, "").orEmpty()) { + RUNTIME_MESSAGE_MODE_TTS -> RuntimeMessageMode.TTS + RUNTIME_MESSAGE_MODE_CUSTOM -> RuntimeMessageMode.CUSTOM_AUDIO + else -> RuntimeMessageMode.DEFAULT + } + val ttsMessage = prefs.getString(KEY_RUNTIME_TTS_MESSAGE, "").orEmpty() + val repeatTtsMessage = prefs.getBoolean(KEY_RUNTIME_REPEAT_TTS_MESSAGE, false) + val speakerDefault = runCatching { + AlarmSpeakerDefault.valueOf( + prefs.getString(KEY_RUNTIME_SPEAKER_DEFAULT, AlarmSpeakerDefault.EARPIECE.name).orEmpty() + ) + }.getOrDefault(AlarmSpeakerDefault.EARPIECE) + val snoozeEnabled = prefs.getBoolean(KEY_RUNTIME_SNOOZE_ENABLED, false) + val snoozeMinutes = prefs.getInt(KEY_RUNTIME_SNOOZE_MINUTES, 5).coerceIn(1, 30) + val snoozeAlarmId = prefs.getLong(KEY_RUNTIME_SNOOZE_ALARM_ID, 0L) + val snoozeCallerName = prefs.getString(KEY_RUNTIME_SNOOZE_CALLER_NAME, callerName).orEmpty() + val snoozeCallerNumber = prefs.getString(KEY_RUNTIME_SNOOZE_CALLER_NUMBER, callerNumber).orEmpty() + val snoozeProviderName = prefs.getString( + KEY_RUNTIME_SNOOZE_PROVIDER_NAME, + context.getString(R.string.default_provider_name) + ).orEmpty() + + prefs.edit() + .putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + .remove(KEY_RUNTIME_AUDIO_OVERRIDE_URI) + .remove(KEY_RUNTIME_AUDIO_OVERRIDE_NAME) + .remove(KEY_RUNTIME_MESSAGE_MODE) + .remove(KEY_RUNTIME_TTS_MESSAGE) + .remove(KEY_RUNTIME_REPEAT_TTS_MESSAGE) + .remove(KEY_RUNTIME_SPEAKER_DEFAULT) + .remove(KEY_RUNTIME_SNOOZE_ENABLED) + .remove(KEY_RUNTIME_SNOOZE_MINUTES) + .remove(KEY_RUNTIME_SNOOZE_ALARM_ID) + .remove(KEY_RUNTIME_SNOOZE_CALLER_NAME) + .remove(KEY_RUNTIME_SNOOZE_CALLER_NUMBER) + .remove(KEY_RUNTIME_SNOOZE_PROVIDER_NAME) + .apply() + + return RuntimeOverrides( + messageMode = messageMode, + customAudioUri = customAudioUri, + ttsMessage = ttsMessage, + repeatTtsMessage = repeatTtsMessage, + speakerDefault = speakerDefault, + snoozeEnabled = snoozeEnabled, + snoozeMinutes = snoozeMinutes, + snoozeAlarmId = snoozeAlarmId, + snoozeCallerName = snoozeCallerName, + snoozeCallerNumber = snoozeCallerNumber, + snoozeProviderName = snoozeProviderName + ) + } + + private fun triggerSnooze() { + if (snoozeTriggered) return + if (!runtimeOverrides.snoozeEnabled) return + val number = runtimeOverrides.snoozeCallerNumber.trim() + if (number.isBlank()) return + val baseAlarm = if (runtimeOverrides.snoozeAlarmId != 0L) { + AlarmModeRepository.find(context, runtimeOverrides.snoozeAlarmId) + } else { + null + } + val snoozeId = System.currentTimeMillis() + val alarm = baseAlarm ?: AlarmModeItem( + id = snoozeId, + callerName = runtimeOverrides.snoozeCallerName, + callerNumber = number, + hour = 0, + minute = 0, + repeatDays = emptySet(), + messageMode = if (runtimeOverrides.messageMode == RuntimeMessageMode.TTS) { + AlarmMessageMode.APP_VOICE_TTS + } else { + AlarmMessageMode.CUSTOM_AUDIO + }, + ttsMessage = runtimeOverrides.ttsMessage, + repeatTtsMessage = runtimeOverrides.repeatTtsMessage, + customAudioUri = runtimeOverrides.customAudioUri, + customAudioName = "", + snoozeEnabled = runtimeOverrides.snoozeEnabled, + snoozeMinutes = runtimeOverrides.snoozeMinutes, + speakerDefault = runtimeOverrides.speakerDefault, + enabled = true + ) + val snoozeAlarm = alarm.copy(id = snoozeId, repeatDays = emptySet(), enabled = true) + val triggerAtMillis = System.currentTimeMillis() + runtimeOverrides.snoozeMinutes * 60_000L + val scheduled = AlarmModeScheduler.scheduleSnooze(context, snoozeAlarm, triggerAtMillis) + if (scheduled) { + snoozeTriggered = true + } + } + private fun loadRecordingsTreeUri(): Uri? { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val value = prefs.getString(KEY_RECORDINGS_TREE_URI, "").orEmpty() @@ -399,9 +920,32 @@ class FakeConnection( companion object { private const val TAG = "FakeConnection" private const val PREFS_NAME = "fake_call_prefs" + private const val KEY_ANSWER_AUDIO_MODE = "answer_audio_mode" private const val KEY_AUDIO_URI = "audio_uri" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED = "runtime_audio_override_enabled" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_URI = "runtime_audio_override_uri" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_NAME = "runtime_audio_override_name" + private const val KEY_RUNTIME_MESSAGE_MODE = "runtime_message_mode" + private const val KEY_RUNTIME_TTS_MESSAGE = "runtime_tts_message" + private const val KEY_RUNTIME_REPEAT_TTS_MESSAGE = "runtime_repeat_tts_message" + private const val KEY_RUNTIME_SPEAKER_DEFAULT = "runtime_speaker_default" + private const val KEY_RUNTIME_SNOOZE_ENABLED = "runtime_snooze_enabled" + private const val KEY_RUNTIME_SNOOZE_MINUTES = "runtime_snooze_minutes" + private const val KEY_RUNTIME_SNOOZE_ALARM_ID = "runtime_snooze_alarm_id" + private const val KEY_RUNTIME_SNOOZE_CALLER_NAME = "runtime_snooze_caller_name" + private const val KEY_RUNTIME_SNOOZE_CALLER_NUMBER = "runtime_snooze_caller_number" + private const val KEY_RUNTIME_SNOOZE_PROVIDER_NAME = "runtime_snooze_provider_name" + private const val RUNTIME_MESSAGE_MODE_CUSTOM = "custom_audio" + private const val RUNTIME_MESSAGE_MODE_TTS = "tts" private const val KEY_RECORDING_ENABLED = "recording_enabled" private const val KEY_RECORDINGS_TREE_URI = "recordings_tree_uri" + private const val DEFAULT_RECORDING_ENABLED = false + private const val KEY_MP3_IVR_MODE_ENABLED = "mp3_ivr_mode_enabled" + private const val KEY_MP3_IVR_FOLDER_URI = "mp3_ivr_folder_uri" + private const val KEY_MP3_IVR_FOLDER_NAME = "mp3_ivr_folder_name" + private const val FOLDER_PAGE_SIZE = 9 + private const val TTS_REPEAT_UTTERANCE_ID = "alarm_tts_repeat" + private const val TTS_REPEAT_DELAY_MILLIS = 750L } private fun finalizeRecordingDestination(stoppedCleanly: Boolean) { @@ -465,3 +1009,41 @@ private data class RecordingDestination( val uri: Uri?, val file: File? ) + +private data class TtsRequest( + val message: String, + val repeat: Boolean +) + +private data class FolderNavEntry( + val uri: Uri, + val displayName: String, + val isDirectory: Boolean +) + +private data class FolderNavState( + val folderUri: Uri, + val folderName: String, + val entries: List, + var pageIndex: Int = 0 +) + +private enum class RuntimeMessageMode { + DEFAULT, + CUSTOM_AUDIO, + TTS +} + +private data class RuntimeOverrides( + val messageMode: RuntimeMessageMode = RuntimeMessageMode.DEFAULT, + val customAudioUri: String = "", + val ttsMessage: String = "", + val repeatTtsMessage: Boolean = false, + val speakerDefault: AlarmSpeakerDefault = AlarmSpeakerDefault.EARPIECE, + val snoozeEnabled: Boolean = false, + val snoozeMinutes: Int = 5, + val snoozeAlarmId: Long = 0L, + val snoozeCallerName: String = "", + val snoozeCallerNumber: String = "", + val snoozeProviderName: String = "" +) diff --git a/app/src/main/java/com/upnp/fakeCall/QuickTriggerAccessibilityService.kt b/app/src/main/java/com/upnp/fakeCall/QuickTriggerAccessibilityService.kt index 1a0e3e8..de223dc 100644 --- a/app/src/main/java/com/upnp/fakeCall/QuickTriggerAccessibilityService.kt +++ b/app/src/main/java/com/upnp/fakeCall/QuickTriggerAccessibilityService.kt @@ -36,15 +36,13 @@ class QuickTriggerAccessibilityService : AccessibilityService() { private fun scheduleQuickTrigger() { val defaults = QuickTriggerManager.loadDefaults(this) - val result = QuickTriggerManager.executeFromInputs( - context = this, - callerName = defaults.callerName, - callerNumber = defaults.callerNumber, - delaySeconds = defaults.delaySeconds - ) + val defaultPresetSlot = QuickTriggerManager.loadDefaultPresetSlot(this) + val defaultPreset = defaultPresetSlot?.let { QuickTriggerManager.getPresetBySlot(this, it) } + val result = QuickTriggerManager.executeFromDefaults(this) + val displayDelay = defaultPreset?.delaySeconds ?: defaults.delaySeconds val message = when (result) { QuickTriggerExecution.IMMEDIATE -> getString(R.string.toast_triggering_now) - QuickTriggerExecution.SCHEDULED -> getString(R.string.toast_scheduled_in, defaults.delaySeconds) + QuickTriggerExecution.SCHEDULED -> getString(R.string.toast_scheduled_in, displayDelay) QuickTriggerExecution.FAILED -> getString(R.string.toast_call_scheduling_failed) } Toast.makeText(this, message, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/upnp/fakeCall/QuickTriggerManager.kt b/app/src/main/java/com/upnp/fakeCall/QuickTriggerManager.kt index 5bafdd2..1db4404 100644 --- a/app/src/main/java/com/upnp/fakeCall/QuickTriggerManager.kt +++ b/app/src/main/java/com/upnp/fakeCall/QuickTriggerManager.kt @@ -14,14 +14,20 @@ import org.json.JSONObject data class QuickTriggerDefaults( val callerName: String = "", val callerNumber: String = "", - val delaySeconds: Int = QuickTriggerManager.DEFAULT_DELAY_SECONDS + val delaySeconds: Int = QuickTriggerManager.DEFAULT_DELAY_SECONDS, + val useCustomAudioOverride: Boolean = false, + val customAudioUri: String = "", + val customAudioName: String = "" ) data class TriggerScheduleRequest( val callerName: String, val callerNumber: String, val delaySeconds: Int, - val providerName: String + val providerName: String, + val usePresetAudioOverride: Boolean = false, + val presetAudioUri: String = "", + val presetAudioName: String = "" ) data class QuickTriggerPreset( @@ -29,7 +35,10 @@ data class QuickTriggerPreset( val title: String, val callerName: String, val callerNumber: String, - val delaySeconds: Int + val delaySeconds: Int, + val useCustomAudio: Boolean = false, + val customAudioUri: String = "", + val customAudioName: String = "" ) enum class QuickTriggerExecution { @@ -54,8 +63,15 @@ object QuickTriggerManager { private const val KEY_QUICK_TRIGGER_CALLER_NAME = "quick_trigger_caller_name" private const val KEY_QUICK_TRIGGER_CALLER_NUMBER = "quick_trigger_caller_number" private const val KEY_QUICK_TRIGGER_DELAY_SECONDS = "quick_trigger_delay_seconds" + private const val KEY_QUICK_TRIGGER_USE_CUSTOM_AUDIO = "quick_trigger_use_custom_audio" + private const val KEY_QUICK_TRIGGER_CUSTOM_AUDIO_URI = "quick_trigger_custom_audio_uri" + private const val KEY_QUICK_TRIGGER_CUSTOM_AUDIO_NAME = "quick_trigger_custom_audio_name" private const val KEY_QUICK_TRIGGER_PRESETS = "quick_trigger_presets_v1" private const val KEY_ACTIVE_PRESET_SLOT = "quick_trigger_active_preset_slot" + private const val KEY_DEFAULT_PRESET_SLOT = "quick_trigger_default_preset_slot" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED = "runtime_audio_override_enabled" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_URI = "runtime_audio_override_uri" + private const val KEY_RUNTIME_AUDIO_OVERRIDE_NAME = "runtime_audio_override_name" private const val SHORTCUT_ID_PREFIX = "quick_trigger_preset_" const val DEFAULT_DELAY_SECONDS = 10 const val MAX_PRESETS = 5 @@ -72,10 +88,16 @@ object QuickTriggerManager { KEY_QUICK_TRIGGER_DELAY_SECONDS, prefs.getInt(KEY_DELAY_SECONDS, DEFAULT_DELAY_SECONDS) ).coerceAtLeast(0) + val useCustomAudioOverride = prefs.getBoolean(KEY_QUICK_TRIGGER_USE_CUSTOM_AUDIO, false) + val customAudioUri = prefs.getString(KEY_QUICK_TRIGGER_CUSTOM_AUDIO_URI, "").orEmpty() + val customAudioName = prefs.getString(KEY_QUICK_TRIGGER_CUSTOM_AUDIO_NAME, "").orEmpty() return QuickTriggerDefaults( callerName = callerName, callerNumber = callerNumber, - delaySeconds = delaySeconds + delaySeconds = delaySeconds, + useCustomAudioOverride = useCustomAudioOverride, + customAudioUri = customAudioUri, + customAudioName = customAudioName ) } @@ -85,6 +107,9 @@ object QuickTriggerManager { .putString(KEY_QUICK_TRIGGER_CALLER_NAME, defaults.callerName) .putString(KEY_QUICK_TRIGGER_CALLER_NUMBER, defaults.callerNumber) .putInt(KEY_QUICK_TRIGGER_DELAY_SECONDS, defaults.delaySeconds.coerceAtLeast(0)) + .putBoolean(KEY_QUICK_TRIGGER_USE_CUSTOM_AUDIO, defaults.useCustomAudioOverride) + .putString(KEY_QUICK_TRIGGER_CUSTOM_AUDIO_URI, defaults.customAudioUri) + .putString(KEY_QUICK_TRIGGER_CUSTOM_AUDIO_NAME, defaults.customAudioName) .apply() } @@ -107,10 +132,16 @@ object QuickTriggerManager { title = title, callerName = defaults.callerName, callerNumber = defaults.callerNumber, - delaySeconds = defaults.delaySeconds + delaySeconds = defaults.delaySeconds, + useCustomAudio = defaults.useCustomAudioOverride, + customAudioUri = defaults.customAudioUri, + customAudioName = defaults.customAudioName ) current.add(preset) savePresets(context, current) + if (current.size == 1) { + saveDefaultPresetSlot(context, 1) + } updateLauncherShortcuts(context) requestTileRefresh(context) return QuickTriggerPresetSaveResult.SAVED @@ -133,7 +164,10 @@ object QuickTriggerManager { title = item.optString("title").orEmpty().ifBlank { callerNumber }.take(30), callerName = item.optString("callerName").orEmpty(), callerNumber = callerNumber, - delaySeconds = item.optInt("delaySeconds", DEFAULT_DELAY_SECONDS).coerceAtLeast(0) + delaySeconds = item.optInt("delaySeconds", DEFAULT_DELAY_SECONDS).coerceAtLeast(0), + useCustomAudio = item.optBoolean("useCustomAudio", false), + customAudioUri = item.optString("customAudioUri").orEmpty(), + customAudioName = item.optString("customAudioName").orEmpty() ) ) } @@ -153,6 +187,12 @@ object QuickTriggerManager { activeSlot == slot -> saveActivePresetSlot(context, null) activeSlot > slot -> saveActivePresetSlot(context, activeSlot - 1) } + val defaultSlot = loadDefaultPresetSlot(context, current) + when { + defaultSlot == null -> Unit + defaultSlot == slot -> saveDefaultPresetSlot(context, null) + defaultSlot > slot -> saveDefaultPresetSlot(context, defaultSlot - 1) + } updateLauncherShortcuts(context) requestTileRefresh(context) return true @@ -165,7 +205,10 @@ object QuickTriggerManager { defaults = QuickTriggerDefaults( callerName = preset.callerName, callerNumber = preset.callerNumber, - delaySeconds = preset.delaySeconds + delaySeconds = preset.delaySeconds, + useCustomAudioOverride = preset.useCustomAudio, + customAudioUri = preset.customAudioUri, + customAudioName = preset.customAudioName ) ) return true @@ -177,6 +220,44 @@ object QuickTriggerManager { return presets.getOrNull(index) } + fun setDefaultPresetSlot(context: Context, slot: Int?): Boolean { + if (slot == null) { + saveDefaultPresetSlot(context, null) + return true + } + val presets = loadPresets(context) + val index = slot - 1 + if (index !in presets.indices) return false + saveDefaultPresetSlot(context, slot) + return true + } + + fun loadDefaultPresetSlot(context: Context): Int? { + return loadDefaultPresetSlot(context, loadPresets(context)) + } + + fun updatePresetAudioMode(context: Context, slot: Int, enabled: Boolean): Boolean { + return updatePreset(context, slot) { preset -> + preset.copy(useCustomAudio = enabled) + } + } + + fun updatePresetAudio(context: Context, slot: Int, audioUri: String, audioName: String): Boolean { + return updatePreset(context, slot) { preset -> + preset.copy( + useCustomAudio = true, + customAudioUri = audioUri, + customAudioName = audioName + ) + } + } + + fun clearPresetAudio(context: Context, slot: Int): Boolean { + return updatePreset(context, slot) { preset -> + preset.copy(customAudioUri = "", customAudioName = "") + } + } + fun loadActivePresetSlot(context: Context): Int? { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val slot = prefs.getInt(KEY_ACTIVE_PRESET_SLOT, -1) @@ -187,11 +268,20 @@ object QuickTriggerManager { fun executePreset(context: Context, slot: Int): QuickTriggerExecution { val preset = getPresetBySlot(context, slot) ?: return QuickTriggerExecution.FAILED - return executeFromInputs( + val request = resolveRequest( context = context, callerName = preset.callerName, callerNumber = preset.callerNumber, - delaySeconds = preset.delaySeconds, + delaySeconds = preset.delaySeconds + )?.copy( + usePresetAudioOverride = preset.useCustomAudio, + presetAudioUri = preset.customAudioUri, + presetAudioName = preset.customAudioName + ) + ?: return QuickTriggerExecution.FAILED + return execute( + context = context, + request = request, presetSlot = slot ) } @@ -209,6 +299,10 @@ object QuickTriggerManager { } fun executeFromDefaults(context: Context): QuickTriggerExecution { + val defaultSlot = loadDefaultPresetSlot(context) + if (defaultSlot != null) { + return executePreset(context, defaultSlot) + } val defaults = loadDefaults(context) return executeFromInputs( context = context, @@ -239,7 +333,10 @@ object QuickTriggerManager { callerName = resolvedName, callerNumber = resolvedNumber, delaySeconds = resolvedDelay.coerceAtLeast(0), - providerName = providerName + providerName = providerName, + usePresetAudioOverride = defaults.useCustomAudioOverride, + presetAudioUri = defaults.customAudioUri, + presetAudioName = defaults.customAudioName ) } @@ -277,9 +374,13 @@ object QuickTriggerManager { ): QuickTriggerExecution { val now = System.currentTimeMillis() val triggerAtMillis = now + request.delaySeconds * 1_000L + val runtimeAudioUri = request.presetAudioUri.takeIf { + request.usePresetAudioOverride && it.isNotBlank() + } + val runtimeAudioName = request.presetAudioName.takeIf { runtimeAudioUri != null } return if (request.delaySeconds == 0 || triggerAtMillis < now + 100L) { - if (triggerImmediately(context, request)) { + if (triggerImmediately(context, request, runtimeAudioUri, runtimeAudioName)) { saveActivePresetSlot(context, null) requestTileRefresh(context) QuickTriggerExecution.IMMEDIATE @@ -287,7 +388,7 @@ object QuickTriggerManager { QuickTriggerExecution.FAILED } } else { - if (scheduleAlarm(context, request, triggerAtMillis)) { + if (scheduleAlarm(context, request, triggerAtMillis, runtimeAudioUri, runtimeAudioName)) { saveActivePresetSlot(context, presetSlot) requestTileRefresh(context) QuickTriggerExecution.SCHEDULED @@ -300,15 +401,20 @@ object QuickTriggerManager { private fun scheduleAlarm( context: Context, request: TriggerScheduleRequest, - triggerAtMillis: Long + triggerAtMillis: Long, + runtimeAudioUri: String?, + runtimeAudioName: String? ): Boolean { FakeCallAlarmScheduler.cancel(context) + configureRuntimeAudioOverride(context, null, null) val scheduled = FakeCallAlarmScheduler.scheduleExact( context = context, triggerAtMillis = triggerAtMillis, callerName = request.callerName, callerNumber = request.callerNumber, - providerName = request.providerName + providerName = request.providerName, + runtimeAudioUri = runtimeAudioUri, + runtimeAudioName = runtimeAudioName ) if (scheduled) { @@ -323,8 +429,14 @@ object QuickTriggerManager { return scheduled } - private fun triggerImmediately(context: Context, request: TriggerScheduleRequest): Boolean { + private fun triggerImmediately( + context: Context, + request: TriggerScheduleRequest, + runtimeAudioUri: String?, + runtimeAudioName: String? + ): Boolean { FakeCallAlarmScheduler.cancel(context) + configureRuntimeAudioOverride(context, runtimeAudioUri, runtimeAudioName) val telecomHelper = TelecomHelper(context) telecomHelper.registerOrUpdatePhoneAccount(request.providerName) val triggered = if (telecomHelper.isAccountEnabled()) { @@ -340,6 +452,8 @@ object QuickTriggerManager { .putString(KEY_CALLER_NUMBER, request.callerNumber) .remove(KEY_TIMER_ENDS_AT) .apply() + } else { + configureRuntimeAudioOverride(context, null, null) } return triggered @@ -355,6 +469,9 @@ object QuickTriggerManager { .put("callerName", preset.callerName) .put("callerNumber", preset.callerNumber) .put("delaySeconds", preset.delaySeconds) + .put("useCustomAudio", preset.useCustomAudio) + .put("customAudioUri", preset.customAudioUri) + .put("customAudioName", preset.customAudioName) ) } context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -370,6 +487,13 @@ object QuickTriggerManager { .apply() } + private fun saveDefaultPresetSlot(context: Context, slot: Int?) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(KEY_DEFAULT_PRESET_SLOT, slot ?: -1) + .apply() + } + private fun requestTileRefresh(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return val services = listOf( @@ -387,4 +511,50 @@ object QuickTriggerManager { private fun formatDelay(context: Context, seconds: Int): String { return DelayFormatter.formatShort(context, seconds) } + + private fun updatePreset( + context: Context, + slot: Int, + updater: (QuickTriggerPreset) -> QuickTriggerPreset + ): Boolean { + val index = slot - 1 + val current = loadPresets(context).toMutableList() + if (index !in current.indices) return false + current[index] = updater(current[index]) + savePresets(context, current) + updateLauncherShortcuts(context) + requestTileRefresh(context) + return true + } + + private fun configureRuntimeAudioOverride( + context: Context, + runtimeAudioUri: String?, + runtimeAudioName: String? + ) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit().apply { + if (runtimeAudioUri.isNullOrBlank()) { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, false) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_URI) + remove(KEY_RUNTIME_AUDIO_OVERRIDE_NAME) + } else { + putBoolean(KEY_RUNTIME_AUDIO_OVERRIDE_ENABLED, true) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_URI, runtimeAudioUri) + putString(KEY_RUNTIME_AUDIO_OVERRIDE_NAME, runtimeAudioName.orEmpty()) + } + }.apply() + } + + private fun loadDefaultPresetSlot(context: Context, presets: List): Int? { + if (presets.isEmpty()) return null + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val stored = prefs.getInt(KEY_DEFAULT_PRESET_SLOT, -1) + val storedValid = stored in 1..presets.size + return when { + storedValid -> stored + presets.size == 1 -> 1 + else -> null + } + } } diff --git a/app/src/main/java/com/upnp/fakeCall/TelecomHelper.kt b/app/src/main/java/com/upnp/fakeCall/TelecomHelper.kt index 087e520..839e578 100644 --- a/app/src/main/java/com/upnp/fakeCall/TelecomHelper.kt +++ b/app/src/main/java/com/upnp/fakeCall/TelecomHelper.kt @@ -38,12 +38,23 @@ class TelecomHelper(private val context: Context) { }.getOrDefault(false) } - fun triggerIncomingCall(callerName: String, callerNumber: String): Boolean { + fun triggerIncomingCall( + callerName: String, + callerNumber: String, + source: IncomingCallSource = IncomingCallSource.CALL + ): Boolean { return runCatching { val normalizedNumber = callerNumber.trim() + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val timeoutSeconds = when (source) { + IncomingCallSource.ALARM -> prefs.getInt(KEY_ALARM_RING_TIMEOUT_SECONDS, DEFAULT_ALARM_RING_TIMEOUT_SECONDS) + IncomingCallSource.CALL -> prefs.getInt(KEY_CALL_RING_TIMEOUT_SECONDS, DEFAULT_CALL_RING_TIMEOUT_SECONDS) + }.coerceAtLeast(0) val incomingExtras = Bundle().apply { putString(EXTRA_FAKE_CALLER_NAME, callerName.trim()) putString(EXTRA_FAKE_CALLER_NUMBER, normalizedNumber) + putString(EXTRA_FAKE_CALL_SOURCE, source.storageValue) + putInt(EXTRA_RING_TIMEOUT_SECONDS, timeoutSeconds) } val extras = Bundle().apply { @@ -60,8 +71,26 @@ class TelecomHelper(private val context: Context) { } companion object { + private const val PREFS_NAME = "fake_call_prefs" + private const val KEY_CALL_RING_TIMEOUT_SECONDS = "call_ring_timeout_seconds" + private const val KEY_ALARM_RING_TIMEOUT_SECONDS = "alarm_ring_timeout_seconds" + private const val DEFAULT_CALL_RING_TIMEOUT_SECONDS = 45 + private const val DEFAULT_ALARM_RING_TIMEOUT_SECONDS = 0 const val ACCOUNT_ID = "fake_call_provider_account" const val EXTRA_FAKE_CALLER_NAME = "extra_fake_caller_name" const val EXTRA_FAKE_CALLER_NUMBER = "extra_fake_caller_number" + const val EXTRA_FAKE_CALL_SOURCE = "extra_fake_call_source" + const val EXTRA_RING_TIMEOUT_SECONDS = "extra_ring_timeout_seconds" + } +} + +enum class IncomingCallSource(val storageValue: String) { + CALL("call"), + ALARM("alarm"); + + companion object { + fun fromStorage(value: String?): IncomingCallSource { + return values().firstOrNull { it.storageValue == value } ?: CALL + } } } diff --git a/app/src/main/java/com/upnp/fakeCall/ui/FakeCallApp.kt b/app/src/main/java/com/upnp/fakeCall/ui/FakeCallApp.kt index 6bf25be..ebff96b 100644 --- a/app/src/main/java/com/upnp/fakeCall/ui/FakeCallApp.kt +++ b/app/src/main/java/com/upnp/fakeCall/ui/FakeCallApp.kt @@ -9,6 +9,8 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween @@ -16,52 +18,97 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.clickable import androidx.compose.ui.res.stringResource import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Alarm import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Phone import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.NavType +import androidx.navigation.navArgument import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.upnp.fakeCall.FakeCallViewModel import com.upnp.fakeCall.R import com.upnp.fakeCall.ReleaseInfo +import com.upnp.fakeCall.ui.screens.AlarmCreateScreen +import com.upnp.fakeCall.ui.screens.AlarmOverviewScreen import com.upnp.fakeCall.ui.screens.DashboardScreen import com.upnp.fakeCall.ui.screens.OnboardingScreen import com.upnp.fakeCall.ui.screens.SettingsScreen private const val ROUTE_ONBOARDING = "onboarding" private const val ROUTE_DASHBOARD = "dashboard" +private const val ROUTE_ALARM = "alarm" +private const val ROUTE_ALARM_CREATE = "alarm_create" +private const val ROUTE_ALARM_EDIT = "alarm_edit/{alarmId}" private const val ROUTE_SETTINGS = "settings" +private const val ARG_ALARM_ID = "alarmId" + +private fun alarmEditRoute(alarmId: Long): String = "alarm_edit/$alarmId" + +private fun modeRouteIndex(route: String?): Int? { + return when (route) { + ROUTE_DASHBOARD -> 0 + ROUTE_ALARM -> 1 + else -> null + } +} + +private fun modeSlideDirection(fromRoute: String?, toRoute: String?): AnimatedContentTransitionScope.SlideDirection? { + val fromIndex = modeRouteIndex(fromRoute) ?: return null + val toIndex = modeRouteIndex(toRoute) ?: return null + if (fromIndex == toIndex) return null + return if (toIndex > fromIndex) { + AnimatedContentTransitionScope.SlideDirection.Left + } else { + AnimatedContentTransitionScope.SlideDirection.Right + } +} private val RequiredPermissions = arrayOf( Manifest.permission.READ_PHONE_STATE, @@ -76,6 +123,8 @@ fun FakeCallApp( ) { val navController = rememberNavController() val state by viewModel.uiState.collectAsStateWithLifecycle() + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route val context = navController.context val slideSpec = tween( @@ -115,28 +164,76 @@ fun FakeCallApp( }, modifier = Modifier.fillMaxSize(), enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = slideSpec - ) + fadeIn(animationSpec = fadeSpec) + val direction = modeSlideDirection(initialState.destination.route, targetState.destination.route) + when { + initialState.destination.route == targetState.destination.route -> EnterTransition.None + direction != null -> { + slideIntoContainer( + towards = direction, + animationSpec = tween(durationMillis = 360, easing = FastOutSlowInEasing) + ) + fadeIn(animationSpec = tween(200, easing = FastOutSlowInEasing)) + } + else -> { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = slideSpec + ) + fadeIn(animationSpec = fadeSpec) + } + } }, exitTransition = { - slideOutOfContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = slideSpec - ) + fadeOut(animationSpec = fadeSpec) + val direction = modeSlideDirection(initialState.destination.route, targetState.destination.route) + when { + initialState.destination.route == targetState.destination.route -> ExitTransition.None + direction != null -> { + slideOutOfContainer( + towards = direction, + animationSpec = tween(durationMillis = 360, easing = FastOutSlowInEasing) + ) + fadeOut(animationSpec = tween(180, easing = FastOutSlowInEasing)) + } + else -> { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = slideSpec + ) + fadeOut(animationSpec = fadeSpec) + } + } }, popEnterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = slideSpec - ) + fadeIn(animationSpec = fadeSpec) + val direction = modeSlideDirection(initialState.destination.route, targetState.destination.route) + when { + initialState.destination.route == targetState.destination.route -> EnterTransition.None + direction != null -> { + slideIntoContainer( + towards = direction, + animationSpec = tween(durationMillis = 360, easing = FastOutSlowInEasing) + ) + fadeIn(animationSpec = tween(200, easing = FastOutSlowInEasing)) + } + else -> { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = slideSpec + ) + fadeIn(animationSpec = fadeSpec) + } + } }, popExitTransition = { - slideOutOfContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = slideSpec - ) + fadeOut(animationSpec = fadeSpec) + val direction = modeSlideDirection(initialState.destination.route, targetState.destination.route) + when { + initialState.destination.route == targetState.destination.route -> ExitTransition.None + direction != null -> { + slideOutOfContainer( + towards = direction, + animationSpec = tween(durationMillis = 360, easing = FastOutSlowInEasing) + ) + fadeOut(animationSpec = tween(180, easing = FastOutSlowInEasing)) + } + else -> { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = slideSpec + ) + fadeOut(animationSpec = fadeSpec) + } + } } ) { composable(route = ROUTE_ONBOARDING) { @@ -154,19 +251,100 @@ fun FakeCallApp( composable(route = ROUTE_DASHBOARD) { DashboardScreen( viewModel = viewModel, - onOpenSettings = { navController.navigate(ROUTE_SETTINGS) } + onOpenSettings = { navController.navigate(ROUTE_SETTINGS) }, + bottomFloatingInset = 76.dp, + modeNavigationBar = null + ) + } + + composable(route = ROUTE_ALARM) { + AlarmOverviewScreen( + viewModel = viewModel, + onOpenSettings = { navController.navigate(ROUTE_SETTINGS) }, + onOpenCreateAlarm = { navController.navigate(ROUTE_ALARM_CREATE) }, + onEditAlarm = { alarmId -> + navController.navigate(alarmEditRoute(alarmId)) + } + ) + } + + composable(route = ROUTE_ALARM_CREATE) { + AlarmCreateScreen( + viewModel = viewModel, + onBack = { + if (!navController.popBackStack()) { + navController.navigate(ROUTE_ALARM) { launchSingleTop = true } + } + } + ) + } + + composable( + route = ROUTE_ALARM_EDIT, + arguments = listOf(navArgument(ARG_ALARM_ID) { type = NavType.LongType }) + ) { entry -> + val alarmId = entry.arguments?.getLong(ARG_ALARM_ID) ?: return@composable + AlarmCreateScreen( + viewModel = viewModel, + editAlarmId = alarmId, + onBack = { + if (!navController.popBackStack()) { + navController.navigate(ROUTE_ALARM) { launchSingleTop = true } + } + } ) } composable(route = ROUTE_SETTINGS) { SettingsScreen( viewModel = viewModel, - onBack = { navController.popBackStack() }, + onBack = { + if (!navController.popBackStack()) { + navController.navigate(ROUTE_DASHBOARD) { + popUpTo(ROUTE_SETTINGS) { inclusive = true } + launchSingleTop = true + } + } + }, onRequestPermissions = { permissionLauncher.launch(RequiredPermissions) } ) } } + val showModeBar = currentRoute == ROUTE_DASHBOARD || currentRoute == ROUTE_ALARM + AnimatedVisibility( + visible = showModeBar, + enter = fadeIn(animationSpec = tween(160, easing = FastOutSlowInEasing)), + exit = fadeOut(animationSpec = tween(120, easing = FastOutSlowInEasing)), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + HomeModeNavigationBar( + selectedRoute = if (currentRoute == ROUTE_ALARM) ROUTE_ALARM else ROUTE_DASHBOARD, + onSelectDashboard = { + if (currentRoute != ROUTE_DASHBOARD) { + navController.navigate(ROUTE_DASHBOARD) { + popUpTo(ROUTE_DASHBOARD) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + }, + onSelectAlarm = { + if (currentRoute != ROUTE_ALARM) { + navController.navigate(ROUTE_ALARM) { + popUpTo(ROUTE_DASHBOARD) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + } + ) + } + val startupUpdate = state.startupUpdate AnimatedVisibility( visible = startupUpdate != null, @@ -217,10 +395,10 @@ private fun UpdateBanner( modifier = Modifier.size(34.dp) ) { Box(contentAlignment = Alignment.Center) { - Text( - text = stringResource(R.string.label_version_prefix), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onTertiaryContainer + Icon( + imageVector = Icons.Outlined.Download, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiaryContainer ) } } @@ -230,7 +408,13 @@ private fun UpdateBanner( color = MaterialTheme.colorScheme.onTertiaryContainer, modifier = Modifier.weight(1f) ) - TextButton(onClick = onDownload) { + FilledTonalButton( + onClick = onDownload, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.onTertiaryContainer, + contentColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { Text(stringResource(R.string.action_download)) } IconButton(onClick = onDismiss) { @@ -244,6 +428,117 @@ private fun UpdateBanner( } } +@Composable +private fun HomeModeNavigationBar( + selectedRoute: String, + onSelectDashboard: () -> Unit, + onSelectAlarm: () -> Unit +) { + val selectedIndex = if (selectedRoute == ROUTE_ALARM) 1 else 0 + + Box( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .padding(horizontal = 16.dp, vertical = 6.dp), + contentAlignment = Alignment.Center + ) { + Surface( + modifier = Modifier.widthIn(min = 236.dp, max = 312.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 2.dp + ) { + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp, vertical = 6.dp) + ) { + val selectorOffsetTarget = if (selectedIndex == 0) 0.dp else maxWidth / 2 + val selectorOffset by animateDpAsState( + targetValue = selectorOffsetTarget, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMediumLow + ), + label = "modeSelectorOffset" + ) + val selectorWidth = maxWidth / 2 + + Surface( + modifier = Modifier + .offset(x = selectorOffset) + .width(selectorWidth) + .height(46.dp), + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.primaryContainer + ) {} + + Row(modifier = Modifier.fillMaxWidth()) { + ModeSwitchItem( + modifier = Modifier.weight(1f), + label = stringResource(R.string.nav_mode_call), + icon = Icons.Outlined.Phone, + selected = selectedIndex == 0, + onClick = onSelectDashboard + ) + ModeSwitchItem( + modifier = Modifier.weight(1f), + label = stringResource(R.string.nav_mode_alarm), + icon = Icons.Outlined.Alarm, + selected = selectedIndex == 1, + onClick = onSelectAlarm + ) + } + } + } + } +} + +@Composable +private fun ModeSwitchItem( + modifier: Modifier, + label: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + selected: Boolean, + onClick: () -> Unit +) { + val contentColor by animateColorAsState( + targetValue = if (selected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "modeSwitchContent" + ) + + Row( + modifier = modifier + .height(46.dp) + .clip(RoundedCornerShape(22.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = contentColor + ) + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = contentColor, + modifier = Modifier.padding(start = 8.dp) + ) + } +} + private fun hasAllPermissions(context: Context): Boolean { return RequiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/com/upnp/fakeCall/ui/components/Components.kt b/app/src/main/java/com/upnp/fakeCall/ui/components/Components.kt index f683f60..2cba455 100644 --- a/app/src/main/java/com/upnp/fakeCall/ui/components/Components.kt +++ b/app/src/main/java/com/upnp/fakeCall/ui/components/Components.kt @@ -1,22 +1,31 @@ package com.upnp.fakeCall.ui.components +import android.graphics.BitmapFactory import android.media.MediaPlayer import android.net.Uri +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.background +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -28,6 +37,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -42,7 +52,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput @@ -53,6 +65,8 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -60,10 +74,15 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AccessTime import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.Stop +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material.icons.outlined.StarBorder import androidx.compose.material.icons.outlined.Tune import androidx.compose.material.icons.outlined.VolumeUp +import com.upnp.fakeCall.CallContact +import com.upnp.fakeCall.CallerInputMode import com.upnp.fakeCall.CustomPreset import com.upnp.fakeCall.FakeCallViewModel import com.upnp.fakeCall.R @@ -208,6 +227,7 @@ fun ExpressiveButton( modifier: Modifier = Modifier, leadingIcon: ImageVector? = null, enabled: Boolean = true, + maxLines: Int = 2, containerColor: Color = MaterialTheme.colorScheme.primary, contentColor: Color = MaterialTheme.colorScheme.onPrimary, shape: Shape = RoundedCornerShape(24.dp) @@ -239,7 +259,9 @@ fun ExpressiveButton( } Text( text = label, - style = MaterialTheme.typography.labelLarge + style = MaterialTheme.typography.labelLarge, + maxLines = maxLines, + textAlign = TextAlign.Center ) } } @@ -276,39 +298,326 @@ fun SectionCard( @Composable fun CallerInputCard( + callerInputMode: CallerInputMode, + onCallerInputModeChange: (CallerInputMode) -> Unit, callerName: String, callerNumber: String, onCallerNameChange: (String) -> Unit, onCallerNumberChange: (String) -> Unit, + selectedContact: CallContact?, + pinnedContacts: List, + recentContacts: List, + onPickContact: () -> Unit, + onSelectContact: (CallContact) -> Unit, + onTogglePinned: (CallContact) -> Unit, modifier: Modifier = Modifier ) { SectionCard( title = stringResource(R.string.caller_details_title), modifier = modifier, - shape = ExpressiveAsymmetricShape + shape = ExpressiveCardShape ) { - ExpressiveTextField( - value = callerName, - onValueChange = onCallerNameChange, - label = stringResource(R.string.label_caller_name), - modifier = Modifier.fillMaxWidth(), - imeAction = ImeAction.Next - ) - ExpressiveTextField( - value = callerNumber, - onValueChange = onCallerNumberChange, - label = stringResource(R.string.label_caller_number), - modifier = Modifier.fillMaxWidth(), - imeAction = ImeAction.Done - ) - Text( - text = stringResource(R.string.hint_shown_on_incoming_call), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + SegmentedButton( + selected = callerInputMode == CallerInputMode.MANUAL, + onClick = { onCallerInputModeChange(CallerInputMode.MANUAL) }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), + modifier = Modifier.bounceClick(), + label = { Text(stringResource(R.string.caller_mode_manual)) } + ) + SegmentedButton( + selected = callerInputMode == CallerInputMode.CONTACT, + onClick = { onCallerInputModeChange(CallerInputMode.CONTACT) }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), + modifier = Modifier.bounceClick(), + label = { Text(stringResource(R.string.caller_mode_contact)) } + ) + } + + if (callerInputMode == CallerInputMode.MANUAL) { + ExpressiveTextField( + value = callerName, + onValueChange = onCallerNameChange, + label = stringResource(R.string.label_caller_name), + modifier = Modifier.fillMaxWidth(), + imeAction = ImeAction.Next + ) + ExpressiveTextField( + value = callerNumber, + onValueChange = onCallerNumberChange, + label = stringResource(R.string.label_caller_number), + modifier = Modifier.fillMaxWidth(), + imeAction = ImeAction.Done + ) + Text( + text = stringResource(R.string.hint_shown_on_incoming_call), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + ExpressiveButton( + label = stringResource(R.string.action_select_contact), + onClick = onPickContact, + leadingIcon = Icons.Outlined.Person, + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + + val availableRecent = recentContacts + .filterNot { recent -> pinnedContacts.any { sameContact(it, recent) } } + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val minCardWidth = 108.dp + val spacing = 8.dp + val columns = ((maxWidth + spacing) / (minCardWidth + spacing)) + .toInt() + .coerceIn(1, 3) + val recentLimit = if (pinnedContacts.isNotEmpty()) 1 else 3 + val recentLimited = availableRecent.takeLast(recentLimit) + ContactSelectionGrid( + columns = columns, + selectedContact = selectedContact, + pinnedContacts = pinnedContacts, + recentContacts = recentLimited, + onSelect = onSelectContact, + onTogglePinned = onTogglePinned, + modifier = Modifier.animateContentSize(animationSpec = expressiveSpring()) + ) + } + } } } +@Composable +private fun ContactSelectionGrid( + columns: Int, + selectedContact: CallContact?, + pinnedContacts: List, + recentContacts: List, + onSelect: (CallContact) -> Unit, + onTogglePinned: (CallContact) -> Unit, + modifier: Modifier = Modifier +) { + if (selectedContact == null && pinnedContacts.isEmpty() && recentContacts.isEmpty()) { + return + } + + val selectedHandledInGroups = selectedContact?.let { selected -> + pinnedContacts.any { sameContact(it, selected) } || + recentContacts.any { sameContact(it, selected) } + } ?: true + val orphanSelectedContact = if (!selectedHandledInGroups) selectedContact else null + + val displayContacts = buildList { + addAll(pinnedContacts) + addAll(recentContacts) + if (orphanSelectedContact != null) add(orphanSelectedContact) + } + val rows = displayContacts.chunked(columns) + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + rows.forEach { rowContacts -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowContacts.forEach { contact -> + ContactChip( + contact = contact, + isSelected = selectedContact?.let { sameContact(it, contact) } == true, + isPinned = pinnedContacts.any { sameContact(it, contact) }, + modifier = Modifier.weight(1f), + onSelect = onSelect, + onTogglePinned = onTogglePinned + ) + } + repeat(columns - rowContacts.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun ContactChip( + contact: CallContact, + isSelected: Boolean, + isPinned: Boolean, + modifier: Modifier = Modifier, + onSelect: (CallContact) -> Unit, + onTogglePinned: (CallContact) -> Unit +) { + val containerColor by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.58f) + } else { + MaterialTheme.colorScheme.surfaceContainer + }, + animationSpec = expressiveSpring(), + label = "contactCardColor" + ) + val scale by animateFloatAsState( + targetValue = if (isSelected) 1f else 0.98f, + animationSpec = expressiveSpring(), + label = "contactCardScale" + ) + + Surface( + shape = RoundedCornerShape(22.dp), + color = containerColor, + border = if (isSelected) { + BorderStroke(1.5.dp, MaterialTheme.colorScheme.primary) + } else { + null + }, + tonalElevation = if (isSelected) 2.dp else 1.dp, + modifier = modifier + .height(162.dp) + .clip(RoundedCornerShape(22.dp)) + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .clickable { onSelect(contact) } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp, vertical = 8.dp) + ) { + IconButton( + onClick = { onTogglePinned(contact) }, + modifier = Modifier + .align(Alignment.TopEnd) + .size(22.dp) + ) { + androidx.compose.material3.Icon( + imageVector = if (isPinned) { + Icons.Outlined.Star + } else { + Icons.Outlined.StarBorder + }, + contentDescription = null, + tint = if (isPinned) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + val avatarSize = when { + maxWidth < 90.dp -> 42.dp + maxWidth < 104.dp -> 48.dp + else -> if (isSelected) 58.dp else 52.dp + } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + ContactAvatar( + name = contact.displayName, + photoUri = contact.photoUri, + avatarBase64 = contact.avatarBase64, + size = avatarSize + ) + Text( + text = contact.displayName, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (isSelected) { + Text( + text = stringResource(R.string.selected_short), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + maxLines = 1 + ) + } else { + Spacer(modifier = Modifier.height(18.dp)) + } + } + } + } + } +} + +@Composable +private fun ContactAvatar( + name: String, + photoUri: String, + avatarBase64: String, + size: Dp +) { + val context = LocalContext.current + val imageBitmap: ImageBitmap? = remember(avatarBase64, photoUri) { + val fromBase64 = runCatching { + if (avatarBase64.isBlank()) return@runCatching null + val bytes = android.util.Base64.decode(avatarBase64, android.util.Base64.DEFAULT) + BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.asImageBitmap() + }.getOrNull() + if (fromBase64 != null) { + fromBase64 + } else if (photoUri.isBlank()) { + null + } else { + runCatching { + val uri = Uri.parse(photoUri) + context.contentResolver.openInputStream(uri)?.use { stream -> + BitmapFactory.decodeStream(stream)?.asImageBitmap() + } + }.getOrNull() + } + } + + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.size(size) + ) { + if (imageBitmap != null) { + Image( + bitmap = imageBitmap, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } else { + Box(contentAlignment = Alignment.Center) { + Text( + text = initialsForName(name), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + } +} + +private fun initialsForName(name: String): String { + val parts = name.trim().split(" ").filter { it.isNotBlank() } + return when { + parts.isEmpty() -> "?" + parts.size == 1 -> parts.first().take(1).uppercase() + else -> (parts[0].take(1) + parts[1].take(1)).uppercase() + } +} + +private fun sameContact(a: CallContact, b: CallContact): Boolean { + return if (a.id > 0 && b.id > 0) a.id == b.id else a.phoneNumber == b.phoneNumber +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun TimingSelectionCard( @@ -447,10 +756,14 @@ fun TimingSelectionCard( @Composable fun AudioPreviewCard( + modeLabel: String, audioLabel: String, - audioUri: String, - onOpenSettings: () -> Unit, - onClearAudio: () -> Unit, + helperText: String, + previewUri: String, + primaryActionLabel: String, + onPrimaryAction: () -> Unit, + secondaryActionLabel: String? = null, + onSecondaryAction: (() -> Unit)? = null, modifier: Modifier = Modifier ) { val context = LocalContext.current @@ -466,7 +779,35 @@ fun AudioPreviewCard( isPlaying = false } - DisposableEffect(audioUri) { + val startPlayback: () -> Unit = { + if (previewUri.isNotBlank()) { + val uri = runCatching { Uri.parse(previewUri) }.getOrNull() + if (uri != null) { + stopPlayback() + val newPlayer = MediaPlayer() + val started = runCatching { + newPlayer.setDataSource(context, uri) + newPlayer.prepare() + newPlayer.setOnCompletionListener { + isPlaying = false + runCatching { it.release() } + if (player === it) { + player = null + } + } + newPlayer.start() + }.onFailure { + runCatching { newPlayer.release() } + }.isSuccess + if (started) { + player = newPlayer + isPlaying = true + } + } + } + } + + DisposableEffect(previewUri) { onDispose { stopPlayback() } @@ -489,67 +830,134 @@ fun AudioPreviewCard( tint = MaterialTheme.colorScheme.onTertiaryContainer ) Column(modifier = Modifier.weight(1f)) { + Surface( + shape = RoundedCornerShape(999.dp), + color = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + tonalElevation = 1.dp + ) { + Text( + text = modeLabel, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp) + ) + } Text( text = audioLabel, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp) ) Text( - text = stringResource(R.string.audio_preview_hint), + text = helperText, style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis ) } } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - ExpressiveButton( - label = if (isPlaying) stringResource(R.string.action_stop) else stringResource(R.string.action_play), - onClick = { - if (audioUri.isBlank()) return@ExpressiveButton - if (isPlaying) { - stopPlayback() - } else { - val uri = runCatching { Uri.parse(audioUri) }.getOrNull() ?: return@ExpressiveButton - val newPlayer = MediaPlayer().apply { - setDataSource(context, uri) - prepare() - start() - setOnCompletionListener { - isPlaying = false - runCatching { release() } - player = null - } - } - player = newPlayer - isPlaying = true + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val useVerticalButtons = maxWidth < 460.dp + val hasPreview = previewUri.isNotBlank() + val hasSecondary = secondaryActionLabel != null && onSecondaryAction != null + if (useVerticalButtons) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (hasPreview) { + ExpressiveButton( + label = if (isPlaying) stringResource(R.string.action_stop) else stringResource(R.string.action_play), + onClick = { + if (isPlaying) { + stopPlayback() + } else { + startPlayback() + } + }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = if (isPlaying) Icons.Outlined.Stop else Icons.Outlined.PlayArrow, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) } - }, - modifier = Modifier.weight(1f), - leadingIcon = if (isPlaying) Icons.Outlined.Stop else Icons.Outlined.PlayArrow, - enabled = audioUri.isNotBlank(), - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - ExpressiveButton( - label = stringResource(R.string.action_open_settings), - onClick = onOpenSettings, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ExpressiveButton( - label = stringResource(R.string.action_clear_audio), - onClick = { - stopPlayback() - onClearAudio() - }, - modifier = Modifier.weight(1f), - containerColor = MaterialTheme.colorScheme.surfaceContainer, - contentColor = MaterialTheme.colorScheme.onSurface - ) + ExpressiveButton( + label = primaryActionLabel, + onClick = { + stopPlayback() + onPrimaryAction() + }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = Icons.Outlined.Tune, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + if (hasSecondary) { + ExpressiveButton( + label = secondaryActionLabel.orEmpty(), + onClick = { + stopPlayback() + onSecondaryAction?.invoke() + }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = Icons.Outlined.Close, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface + ) + } + } + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (hasPreview) { + ExpressiveButton( + label = if (isPlaying) stringResource(R.string.action_stop) else stringResource(R.string.action_play), + onClick = { + if (isPlaying) { + stopPlayback() + } else { + startPlayback() + } + }, + modifier = Modifier.weight(1f), + leadingIcon = if (isPlaying) Icons.Outlined.Stop else Icons.Outlined.PlayArrow, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + ExpressiveButton( + label = primaryActionLabel, + onClick = { + stopPlayback() + onPrimaryAction() + }, + modifier = Modifier.weight(1f), + leadingIcon = Icons.Outlined.Tune, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + if (hasSecondary) { + ExpressiveButton( + label = secondaryActionLabel.orEmpty(), + onClick = { + stopPlayback() + onSecondaryAction?.invoke() + }, + modifier = Modifier.weight(1f), + leadingIcon = Icons.Outlined.Close, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface + ) + } + } + } } } } diff --git a/app/src/main/java/com/upnp/fakeCall/ui/screens/AlarmModeScreen.kt b/app/src/main/java/com/upnp/fakeCall/ui/screens/AlarmModeScreen.kt new file mode 100644 index 0000000..1a3ab69 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/ui/screens/AlarmModeScreen.kt @@ -0,0 +1,782 @@ +package com.upnp.fakeCall.ui.screens + +import android.content.Intent +import android.provider.MediaStore +import android.text.format.DateFormat +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Alarm +import androidx.compose.material.icons.outlined.AudioFile +import androidx.compose.material.icons.outlined.Call +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Mic +import androidx.compose.material.icons.outlined.Schedule +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Snooze +import androidx.compose.material.icons.outlined.VolumeUp +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.upnp.fakeCall.AlarmMessageMode +import com.upnp.fakeCall.AlarmModeDraft +import com.upnp.fakeCall.AlarmModeItem +import com.upnp.fakeCall.AlarmSpeakerDefault +import com.upnp.fakeCall.AlarmModeScheduler +import com.upnp.fakeCall.FakeCallViewModel +import com.upnp.fakeCall.R +import com.upnp.fakeCall.ui.components.AnimatedIcon +import com.upnp.fakeCall.ui.components.ExpressiveButton +import com.upnp.fakeCall.ui.components.ExpressiveTextField +import com.upnp.fakeCall.ui.components.SectionCard +import com.upnp.fakeCall.ui.components.bounceClick +import java.time.DayOfWeek +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +@Composable +fun AlarmOverviewScreen( + viewModel: FakeCallViewModel, + onOpenSettings: () -> Unit, + onOpenCreateAlarm: () -> Unit, + onEditAlarm: (Long) -> Unit +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + val is24Hour = DateFormat.is24HourFormat(context) + var alarmPendingDelete by remember { mutableStateOf(null) } + + Scaffold( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)) + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = stringResource(R.string.alarm_mode_title), + style = MaterialTheme.typography.displayMedium, + fontWeight = FontWeight.ExtraBold, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.alarm_mode_subtitle), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + AnimatedIcon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(R.string.cd_open_settings), + shape = RoundedCornerShape(16.dp), + backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh, + tint = MaterialTheme.colorScheme.onSurface, + onClick = onOpenSettings + ) + AnimatedIcon( + imageVector = Icons.Outlined.Add, + contentDescription = stringResource(R.string.alarm_add_new), + shape = RoundedCornerShape(16.dp), + backgroundColor = MaterialTheme.colorScheme.primaryContainer, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + onClick = onOpenCreateAlarm + ) + } + } + }, + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 108.dp, top = 10.dp) + ) { + if (state.alarmModeItems.isEmpty()) { + item { + Surface( + shape = RoundedCornerShape(32.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedIcon( + imageVector = Icons.Outlined.Alarm, + contentDescription = null, + shape = CircleShape, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + size = 60.dp + ) + Text( + text = stringResource(R.string.alarm_empty_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.alarm_empty_subtitle), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ExpressiveButton( + label = stringResource(R.string.alarm_add_new), + onClick = onOpenCreateAlarm, + leadingIcon = Icons.Outlined.Add + ) + } + } + } + } else { + items(state.alarmModeItems, key = { it.id }) { alarm -> + AlarmOverviewItem( + alarm = alarm, + is24Hour = is24Hour, + onEnabledChange = { enabled -> viewModel.onAlarmModeEnabledChanged(alarm.id, enabled) }, + onEdit = { onEditAlarm(alarm.id) }, + onDelete = { alarmPendingDelete = alarm } + ) + } + } + } + } + + val toDelete = alarmPendingDelete + if (toDelete != null) { + AlertDialog( + onDismissRequest = { alarmPendingDelete = null }, + title = { Text(stringResource(R.string.alarm_delete_title)) }, + text = { + Text( + stringResource( + R.string.alarm_delete_message, + formatAlarmTime(toDelete.hour, toDelete.minute, is24Hour) + ) + ) + }, + confirmButton = { + TextButton( + onClick = { + viewModel.deleteAlarmMode(toDelete.id) + alarmPendingDelete = null + } + ) { + Text(stringResource(R.string.action_delete)) + } + }, + dismissButton = { + TextButton(onClick = { alarmPendingDelete = null }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } +} + +@Composable +private fun AlarmOverviewItem( + alarm: AlarmModeItem, + is24Hour: Boolean, + onEnabledChange: (Boolean) -> Unit, + onEdit: () -> Unit, + onDelete: () -> Unit +) { + val context = LocalContext.current + val alpha by animateFloatAsState( + targetValue = if (alarm.enabled) 1f else 0.62f, + animationSpec = com.upnp.fakeCall.ui.components.expressiveSpring(), + label = "alarmCardAlpha" + ) + Surface( + shape = RoundedCornerShape(32.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 2.dp, + modifier = Modifier + .fillMaxWidth() + .bounceClick() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedIcon( + imageVector = Icons.Outlined.Alarm, + contentDescription = null, + shape = CircleShape, + backgroundColor = MaterialTheme.colorScheme.primaryContainer, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = formatAlarmTime(alarm.hour, alarm.minute, is24Hour), + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) + ) + Text( + text = listOf(alarm.callerName, alarm.callerNumber).filter { it.isNotBlank() }.joinToString(" • "), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha) + ) + Text( + text = alarmRepeatSummary(context, alarm), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha) + ) + AnimatedVisibility(visible = alarm.nextTriggerAtMillis > 0L && alarm.enabled) { + Text( + text = formatNextTrigger(alarm.nextTriggerAtMillis), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Switch( + checked = alarm.enabled, + onCheckedChange = onEnabledChange + ) + Row { + IconButton(onClick = onEdit, modifier = Modifier.bounceClick()) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = stringResource(R.string.cd_edit_alarm), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = onDelete, modifier = Modifier.bounceClick()) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.cd_delete_alarm), + tint = MaterialTheme.colorScheme.error + ) + } + } + } + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AlarmCreateScreen( + viewModel: FakeCallViewModel, + onBack: () -> Unit, + editAlarmId: Long? = null, + modeNavigationBar: (@Composable () -> Unit)? = null +) { + val context = LocalContext.current + val isEditMode = editAlarmId != null + val initialDraft = remember(editAlarmId) { + if (editAlarmId != null) { + viewModel.draftForAlarm(editAlarmId) ?: viewModel.newAlarmModeDraft() + } else { + viewModel.newAlarmModeDraft() + } + } + var draft by remember(editAlarmId) { mutableStateOf(initialDraft) } + var showTimePicker by remember { mutableStateOf(false) } + val canSaveAlarm = draft.callerNumber.trim().isNotBlank() + + val audioPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + if (uri != null) { + runCatching { + context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + draft = draft.copy( + customAudioUri = uri.toString(), + customAudioName = resolveAudioLabel(context, uri) + ) + } + } + + val recorderLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + val uri = result.data?.data ?: return@rememberLauncherForActivityResult + runCatching { + context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + draft = draft.copy( + customAudioUri = uri.toString(), + customAudioName = resolveAudioLabel(context, uri) + ) + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)) + .padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = if (isEditMode) { + stringResource(R.string.alarm_edit_title) + } else { + stringResource(R.string.alarm_create_title) + }, + style = MaterialTheme.typography.displaySmall, + fontWeight = FontWeight.Bold + ) + Text( + text = if (isEditMode) { + stringResource(R.string.alarm_edit_subtitle) + } else { + stringResource(R.string.alarm_create_subtitle) + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + AnimatedIcon( + imageVector = Icons.Outlined.Schedule, + contentDescription = null, + shape = RoundedCornerShape(16.dp), + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + }, + bottomBar = { + Column { + Surface( + tonalElevation = 6.dp, + color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.92f) + ) { + ExpressiveButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .navigationBarsPadding(), + label = stringResource(R.string.action_save_call), + leadingIcon = Icons.Outlined.Call, + enabled = canSaveAlarm, + onClick = { + val saved = if (editAlarmId != null) { + viewModel.updateAlarmMode(editAlarmId, draft) + } else { + viewModel.saveAlarmMode(draft) + } + if (saved) onBack() + } + ) + } + modeNavigationBar?.invoke() + } + } + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 120.dp, top = 10.dp) + ) { + item { + SectionCard(title = stringResource(R.string.alarm_section_caller)) { + ExpressiveTextField( + value = draft.callerName, + onValueChange = { draft = draft.copy(callerName = it) }, + label = stringResource(R.string.label_caller_name) + ) + ExpressiveTextField( + value = draft.callerNumber, + onValueChange = { draft = draft.copy(callerNumber = it) }, + label = stringResource(R.string.label_caller_number) + ) + if (!canSaveAlarm) { + Text( + text = stringResource(R.string.status_enter_caller_number_scheduling), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error + ) + } + } + } + + item { + SectionCard(title = stringResource(R.string.alarm_section_schedule)) { + Surface( + modifier = Modifier + .fillMaxWidth() + .bounceClick(onClick = { showTimePicker = true }), + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surfaceContainer + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AnimatedIcon( + imageVector = Icons.Outlined.Schedule, + contentDescription = null, + shape = CircleShape, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + Column { + Text( + text = formatAlarmTime(draft.hour, draft.minute, DateFormat.is24HourFormat(context)), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(R.string.alarm_exact_time_only), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + WeekdaySelector( + selectedDays = draft.repeatDays, + onToggle = { day -> + draft = draft.copy( + repeatDays = if (draft.repeatDays.contains(day)) { + draft.repeatDays - day + } else { + draft.repeatDays + day + } + ) + } + ) + } + } + + item { + SectionCard(title = stringResource(R.string.alarm_section_message)) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + FilterChip( + selected = draft.messageMode == AlarmMessageMode.APP_VOICE_TTS, + onClick = { draft = draft.copy(messageMode = AlarmMessageMode.APP_VOICE_TTS) }, + label = { Text(stringResource(R.string.alarm_message_mode_tts)) } + ) + FilterChip( + selected = draft.messageMode == AlarmMessageMode.CUSTOM_AUDIO, + onClick = { draft = draft.copy(messageMode = AlarmMessageMode.CUSTOM_AUDIO) }, + label = { Text(stringResource(R.string.alarm_message_mode_custom)) } + ) + } + if (draft.messageMode == AlarmMessageMode.APP_VOICE_TTS) { + ExpressiveTextField( + value = draft.ttsMessage, + onValueChange = { draft = draft.copy(ttsMessage = it) }, + label = stringResource(R.string.alarm_tts_message_label) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.alarm_tts_repeat_title), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(R.string.alarm_tts_repeat_subtitle), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = draft.repeatTtsMessage, + onCheckedChange = { draft = draft.copy(repeatTtsMessage = it) } + ) + } + } else { + Text( + text = if (draft.customAudioName.isBlank()) { + stringResource(R.string.alarm_custom_audio_none) + } else { + draft.customAudioName + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + ExpressiveButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.alarm_select_audio), + leadingIcon = Icons.Outlined.AudioFile, + onClick = { audioPickerLauncher.launch(arrayOf("audio/*")) } + ) + ExpressiveButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.alarm_record_audio), + leadingIcon = Icons.Outlined.Mic, + onClick = { + val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION) + recorderLauncher.launch(intent) + } + ) + } + } + } + } + + item { + SectionCard(title = stringResource(R.string.alarm_section_snooze)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.alarm_snooze_enable), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(R.string.alarm_snooze_hint), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = draft.snoozeEnabled, + onCheckedChange = { draft = draft.copy(snoozeEnabled = it) } + ) + } + AnimatedVisibility(visible = draft.snoozeEnabled) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + listOf(3, 5, 10).forEach { minutes -> + FilterChip( + selected = draft.snoozeMinutes == minutes, + onClick = { draft = draft.copy(snoozeMinutes = minutes) }, + label = { Text(stringResource(R.string.alarm_snooze_minutes, minutes)) } + ) + } + } + } + } + } + + item { + SectionCard(title = stringResource(R.string.alarm_section_speaker)) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + FilterChip( + selected = draft.speakerDefault == AlarmSpeakerDefault.EARPIECE, + onClick = { draft = draft.copy(speakerDefault = AlarmSpeakerDefault.EARPIECE) }, + label = { Text(stringResource(R.string.alarm_speaker_earpiece)) }, + leadingIcon = { Icon(Icons.Outlined.Call, contentDescription = null) } + ) + FilterChip( + selected = draft.speakerDefault == AlarmSpeakerDefault.SPEAKER, + onClick = { draft = draft.copy(speakerDefault = AlarmSpeakerDefault.SPEAKER) }, + label = { Text(stringResource(R.string.alarm_speaker_speaker)) }, + leadingIcon = { Icon(Icons.Outlined.VolumeUp, contentDescription = null) } + ) + } + } + } + } + } + + if (showTimePicker) { + val timePickerState = rememberTimePickerState( + initialHour = draft.hour, + initialMinute = draft.minute, + is24Hour = DateFormat.is24HourFormat(context) + ) + Dialog(onDismissRequest = { showTimePicker = false }) { + Surface( + shape = RoundedCornerShape(36.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.alarm_pick_time), + style = MaterialTheme.typography.displaySmall + ) + TimePicker(state = timePickerState) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ExpressiveButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_cancel), + onClick = { showTimePicker = false }, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ExpressiveButton( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_apply), + onClick = { + draft = draft.copy( + hour = timePickerState.hour, + minute = timePickerState.minute + ) + showTimePicker = false + } + ) + } + } + } + } + } +} + +@Composable +private fun WeekdaySelector( + selectedDays: Set, + onToggle: (Int) -> Unit +) { + val context = LocalContext.current + val days = listOf( + DayOfWeek.MONDAY.value, + DayOfWeek.TUESDAY.value, + DayOfWeek.WEDNESDAY.value, + DayOfWeek.THURSDAY.value, + DayOfWeek.FRIDAY.value, + DayOfWeek.SATURDAY.value, + DayOfWeek.SUNDAY.value + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(R.string.alarm_repeat_label), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + days.forEach { day -> + FilterChip( + selected = selectedDays.contains(day), + onClick = { onToggle(day) }, + label = { Text(AlarmModeScheduler.dayLabel(context, day)) } + ) + } + } + } +} + +private fun formatAlarmTime(hour: Int, minute: Int, is24Hour: Boolean): String { + val locale = Locale.getDefault() + val formatter = if (is24Hour) { + DateTimeFormatter.ofPattern(android.text.format.DateFormat.getBestDateTimePattern(locale, "Hm"), locale) + } else { + DateTimeFormatter.ofPattern(android.text.format.DateFormat.getBestDateTimePattern(locale, "hm"), locale) + } + return java.time.LocalTime.of(hour.coerceIn(0, 23), minute.coerceIn(0, 59)).format(formatter) +} + +private fun alarmRepeatSummary(context: android.content.Context, alarm: AlarmModeItem): String { + if (alarm.repeatDays.isEmpty()) return context.getString(R.string.alarm_repeat_once) + val locale = Locale.getDefault() + val labels = alarm.repeatDays.sorted().map { + DayOfWeek.of(it).getDisplayName(java.time.format.TextStyle.SHORT, locale) + } + return labels.joinToString(", ") +} + +private fun formatNextTrigger(triggerAtMillis: Long): String { + if (triggerAtMillis <= 0L) return "" + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + return Instant.ofEpochMilli(triggerAtMillis).atZone(ZoneId.systemDefault()).format(formatter) +} + +private fun resolveAudioLabel(context: android.content.Context, uri: android.net.Uri): String { + val resolver = context.contentResolver + val label = runCatching { + resolver.query(uri, arrayOf(android.provider.OpenableColumns.DISPLAY_NAME), null, null, null) + ?.use { cursor -> + val index = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) + if (index >= 0 && cursor.moveToFirst()) cursor.getString(index) else null + } + }.getOrNull() + return label ?: (uri.lastPathSegment ?: context.getString(R.string.default_selected_audio)) +} diff --git a/app/src/main/java/com/upnp/fakeCall/ui/screens/DashboardScreen.kt b/app/src/main/java/com/upnp/fakeCall/ui/screens/DashboardScreen.kt index 6f6da40..aef6b05 100644 --- a/app/src/main/java/com/upnp/fakeCall/ui/screens/DashboardScreen.kt +++ b/app/src/main/java/com/upnp/fakeCall/ui/screens/DashboardScreen.kt @@ -1,6 +1,12 @@ package com.upnp.fakeCall.ui.screens +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.provider.ContactsContract import android.text.format.DateFormat +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -35,6 +41,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AccessTime +import androidx.compose.material.icons.outlined.Mic import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.rounded.Phone import androidx.compose.material.icons.rounded.PhoneInTalk @@ -43,6 +50,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TimePicker import androidx.compose.material3.rememberTimePickerState @@ -69,8 +77,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.compose.ui.window.Dialog +import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.upnp.fakeCall.AnswerAudioMode import com.upnp.fakeCall.CustomPreset +import com.upnp.fakeCall.CallerInputMode import com.upnp.fakeCall.FakeCallUiState import com.upnp.fakeCall.FakeCallViewModel import com.upnp.fakeCall.R @@ -96,7 +107,9 @@ import kotlin.math.absoluteValue @Composable fun DashboardScreen( viewModel: FakeCallViewModel, - onOpenSettings: () -> Unit + onOpenSettings: () -> Unit, + bottomFloatingInset: androidx.compose.ui.unit.Dp = 0.dp, + modeNavigationBar: (@Composable () -> Unit)? = null ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current @@ -126,6 +139,24 @@ fun DashboardScreen( ) val canTrigger = state.hasRequiredPermissions && state.isProviderEnabled + val hasCallerNumber = when (state.callerInputMode) { + CallerInputMode.CONTACT -> state.selectedContact?.phoneNumber?.trim().isNullOrEmpty().not() + CallerInputMode.MANUAL -> state.callerNumber.trim().isNotBlank() + } + val canRunPrimaryAction = state.isTimerRunning || (canTrigger && hasCallerNumber) + val contactPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + viewModel.onContactPicked(result.data?.data) + } + val contactPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI) + contactPickerLauncher.launch(intent) + } + } val actionLabel = if (state.isTimerRunning) stringResource(R.string.action_cancel_call) else stringResource(R.string.action_schedule_call) val is24Hour = DateFormat.is24HourFormat(context) @@ -205,19 +236,24 @@ fun DashboardScreen( } }, bottomBar = { - BottomActionBar( - enabled = canTrigger || state.isTimerRunning, - label = actionLabel, - containerColor = actionContainerColor, - contentColor = actionContentColor, - isRinging = state.isTimerRunning, - onClick = { - if (canTrigger || state.isTimerRunning) { - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - viewModel.onTriggerOrCancelClicked() + Column { + BottomActionBar( + enabled = canRunPrimaryAction, + label = actionLabel, + containerColor = actionContainerColor, + contentColor = actionContentColor, + isRinging = state.isTimerRunning, + extraBottomPadding = bottomFloatingInset, + applyBottomInset = modeNavigationBar == null, + onClick = { + if (canRunPrimaryAction) { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + viewModel.onTriggerOrCancelClicked() + } } - } - ) + ) + modeNavigationBar?.invoke() + } } ) { innerPadding -> LazyColumn( @@ -241,27 +277,57 @@ fun DashboardScreen( ) } - item { - AnimatedVisibility( - visible = state.isTimerRunning, - enter = expandVertically(animationSpec = expressiveSpring()) + fadeIn(animationSpec = expressiveSpring()), - exit = shrinkVertically(animationSpec = expressiveSpring()) + fadeOut(animationSpec = expressiveSpring()) - ) { - ScheduledBanner( - runningLabel = runningScheduleLabel(state.timerEndsAtMillis) - ) - } - } - item { CallerInputCard( + callerInputMode = state.callerInputMode, + onCallerInputModeChange = viewModel::onCallerInputModeChange, callerName = state.callerName, callerNumber = state.callerNumber, onCallerNameChange = viewModel::onCallerNameChange, - onCallerNumberChange = viewModel::onCallerNumberChange + onCallerNumberChange = viewModel::onCallerNumberChange, + selectedContact = state.selectedContact, + pinnedContacts = state.pinnedContacts, + recentContacts = state.recentContacts, + onPickContact = { + val hasContactsPermission = ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + if (hasContactsPermission) { + val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI) + contactPickerLauncher.launch(intent) + } else { + contactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS) + } + }, + onSelectContact = viewModel::selectContact, + onTogglePinned = viewModel::togglePinnedContact ) } + item { + if (!state.isTimerRunning && !hasCallerNumber) { + Surface( + tonalElevation = 1.dp, + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.errorContainer + ) { + Text( + text = if (state.callerInputMode == CallerInputMode.CONTACT) { + stringResource(R.string.status_select_contact_scheduling) + } else { + stringResource(R.string.status_enter_caller_number_scheduling) + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) + } + } + } + item { val manualLabel = when (state.scheduleKind) { ScheduleKind.PRESET -> stringResource(R.string.schedule_kind_set_custom_time) @@ -348,12 +414,27 @@ fun DashboardScreen( } item { - AudioPreviewCard( - audioLabel = state.selectedAudioName.ifBlank { stringResource(R.string.default_audio_name) }, - audioUri = state.selectedAudioUri, - onOpenSettings = onOpenSettings, + val audioPreview = answerPlaybackPreview( + state = state, onClearAudio = viewModel::clearAudioSelection ) + AudioPreviewCard( + modeLabel = audioPreview.modeLabel, + audioLabel = audioPreview.audioLabel, + helperText = audioPreview.helperText, + previewUri = audioPreview.previewUri, + primaryActionLabel = audioPreview.primaryActionLabel, + onPrimaryAction = onOpenSettings, + secondaryActionLabel = audioPreview.secondaryActionLabel, + onSecondaryAction = audioPreview.secondaryAction + ) + } + + item { + RecordingQuickToggleCard( + enabled = state.isRecordingEnabled, + onEnabledChange = viewModel::onRecordingEnabledChange + ) } item { @@ -486,39 +567,60 @@ private fun ScheduleStateCard( } @Composable -private fun ScheduledBanner(runningLabel: String) { +private fun RecordingQuickToggleCard( + enabled: Boolean, + onEnabledChange: (Boolean) -> Unit +) { Surface( + modifier = Modifier.fillMaxWidth(), tonalElevation = 1.dp, - shape = RoundedCornerShape(32.dp), + shape = RoundedCornerShape(28.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(14.dp) ) { AnimatedIcon( - imageVector = Icons.Rounded.Phone, + imageVector = Icons.Outlined.Mic, contentDescription = null, shape = CircleShape, - backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), - tint = MaterialTheme.colorScheme.primary, - isRinging = true, - isActive = true + backgroundColor = if (enabled) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainer + }, + tint = if (enabled) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + isActive = enabled ) - Column { + Column(modifier = Modifier.weight(1f)) { Text( - text = stringResource(R.string.schedule_state_running), - style = MaterialTheme.typography.titleMedium + text = stringResource(R.string.dashboard_recording_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface ) Text( - text = runningLabel, + text = if (enabled) { + stringResource(R.string.dashboard_recording_enabled) + } else { + stringResource(R.string.dashboard_recording_disabled) + }, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } + Switch( + checked = enabled, + onCheckedChange = onEnabledChange + ) } } } @@ -530,13 +632,22 @@ private fun BottomActionBar( containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, isRinging: Boolean, + extraBottomPadding: androidx.compose.ui.unit.Dp, + applyBottomInset: Boolean, onClick: () -> Unit ) { Box( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .then( + if (applyBottomInset) { + Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + } else { + Modifier + } + ) .padding(horizontal = 16.dp, vertical = 12.dp) + .padding(bottom = extraBottomPadding) ) { Surface( modifier = Modifier @@ -974,6 +1085,92 @@ private data class PickerState( val itemHeightPx: Int ) +private data class AnswerPlaybackPreview( + val modeLabel: String, + val audioLabel: String, + val helperText: String, + val previewUri: String, + val primaryActionLabel: String, + val secondaryActionLabel: String? = null, + val secondaryAction: (() -> Unit)? = null +) + +@Composable +private fun answerPlaybackPreview( + state: FakeCallUiState, + onClearAudio: () -> Unit +): AnswerPlaybackPreview { + val selectedAudioLabel = state.selectedAudioName.ifBlank { + stringResource(R.string.audio_preview_selected_audio_fallback) + } + val rootNode = state.ivrConfig?.let { it.nodes[it.rootId] } + + return when (state.answerAudioMode) { + AnswerAudioMode.MP3_IVR -> { + val folderName = state.mp3IvrFolderName.ifBlank { + stringResource(R.string.settings_mp3_ivr_no_folder_selected) + } + val helperText = if (state.mp3IvrFolderUri.isBlank()) { + stringResource(R.string.audio_preview_mp3_ivr_missing_folder) + } else { + stringResource(R.string.audio_preview_mp3_ivr_helper) + } + AnswerPlaybackPreview( + modeLabel = stringResource(R.string.audio_preview_mode_mp3_ivr), + audioLabel = folderName, + helperText = helperText, + previewUri = "", + primaryActionLabel = stringResource(R.string.action_configure_ivr) + ) + } + + AnswerAudioMode.CUSTOM_IVR -> { + val rootLabel = rootNode?.audioLabel.orEmpty() + .ifBlank { rootNode?.title.orEmpty() } + .ifBlank { stringResource(R.string.default_ivr_node_title) } + .ifBlank { stringResource(R.string.audio_preview_silent_after_answer) } + val helperText = if (rootNode?.audioUri?.isNotBlank() == true) { + stringResource(R.string.audio_preview_custom_ivr_helper) + } else { + stringResource(R.string.audio_preview_custom_ivr_missing_root_helper) + } + AnswerPlaybackPreview( + modeLabel = stringResource(R.string.audio_preview_mode_custom_ivr), + audioLabel = rootLabel, + helperText = helperText, + previewUri = rootNode?.audioUri.orEmpty(), + primaryActionLabel = stringResource(R.string.action_configure_ivr) + ) + } + + AnswerAudioMode.AUDIO_FILE -> AnswerPlaybackPreview( + modeLabel = stringResource(R.string.audio_preview_mode_selected_audio), + audioLabel = if (state.selectedAudioUri.isBlank()) { + stringResource(R.string.audio_preview_silent_after_answer) + } else { + selectedAudioLabel + }, + helperText = if (state.selectedAudioUri.isBlank()) { + stringResource(R.string.audio_preview_selected_audio_missing_helper) + } else { + stringResource(R.string.audio_preview_selected_audio_helper) + }, + previewUri = state.selectedAudioUri, + primaryActionLabel = stringResource(R.string.action_change_audio), + secondaryActionLabel = stringResource(R.string.action_clear_audio), + secondaryAction = onClearAudio + ) + + AnswerAudioMode.SILENT -> AnswerPlaybackPreview( + modeLabel = stringResource(R.string.audio_preview_mode_silent), + audioLabel = stringResource(R.string.audio_preview_silent_after_answer), + helperText = stringResource(R.string.audio_preview_silent_helper), + previewUri = "", + primaryActionLabel = stringResource(R.string.action_choose_audio) + ) + } +} + @Composable private fun rememberPickerState(initialIndex: Int): PickerState { val listState = androidx.compose.foundation.lazy.rememberLazyListState(initialFirstVisibleItemIndex = initialIndex) diff --git a/app/src/main/java/com/upnp/fakeCall/ui/screens/OnboardingScreen.kt b/app/src/main/java/com/upnp/fakeCall/ui/screens/OnboardingScreen.kt index 20a9bbf..d572417 100644 --- a/app/src/main/java/com/upnp/fakeCall/ui/screens/OnboardingScreen.kt +++ b/app/src/main/java/com/upnp/fakeCall/ui/screens/OnboardingScreen.kt @@ -35,6 +35,9 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -47,7 +50,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.upnp.fakeCall.FakeCallViewModel +import com.upnp.fakeCall.BatterySetupNavigator +import com.upnp.fakeCall.RomFamily import com.upnp.fakeCall.R +import com.upnp.fakeCall.SimProviderOption import com.upnp.fakeCall.ui.components.AnimatedIcon import com.upnp.fakeCall.ui.components.ExpressiveButton import com.upnp.fakeCall.ui.components.ExpressiveCardShape @@ -66,7 +72,16 @@ fun OnboardingScreen( val permissionsReady = state.hasRequiredPermissions val callingAccountReady = state.isProviderEnabled val exactAlarmsReady = viewModel.canScheduleExactAlarms() + val batteryOptimizationReady = BatterySetupNavigator.isBatteryOptimizationDisabled(context) + val romFamily = BatterySetupNavigator.detectRomFamily() val canFinish = permissionsReady && callingAccountReady + var showSimProviderDialog by remember { mutableStateOf(false) } + var simProviderOptions by remember { mutableStateOf>(emptyList()) } + + fun finishSetup() { + viewModel.completeOnboarding() + onFinish() + } val heroScale by animateFloatAsState( targetValue = if (canFinish) 1.02f else 1f, @@ -160,6 +175,21 @@ fun OnboardingScreen( ) } + item { + BatteryOptimizationCard( + isReady = batteryOptimizationReady, + romFamily = romFamily, + onOpenSystemSettings = { + haptics.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress) + BatterySetupNavigator.openSystemBatteryOptimization(context) + }, + onOpenRomSettings = { + haptics.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress) + BatterySetupNavigator.openOemBackgroundSettings(context) + } + ) + } + item { Surface( color = MaterialTheme.colorScheme.surfaceContainerHigh, @@ -251,8 +281,13 @@ fun OnboardingScreen( ExpressiveButton( label = if (canFinish) stringResource(R.string.onboarding_finish_setup) else stringResource(R.string.onboarding_finish_setup_needs_permissions), onClick = { - viewModel.completeOnboarding() - onFinish() + val options = viewModel.loadSimProviderOptions() + if (options.isNotEmpty()) { + simProviderOptions = options + showSimProviderDialog = true + } else { + finishSetup() + } }, enabled = canFinish, containerColor = MaterialTheme.colorScheme.primary, @@ -268,6 +303,105 @@ fun OnboardingScreen( } } } + + if (showSimProviderDialog) { + SimProviderPickerDialog( + options = simProviderOptions, + onSelect = { option -> + viewModel.applySimProviderName(option) + showSimProviderDialog = false + finishSetup() + }, + onKeepCurrent = { + showSimProviderDialog = false + finishSetup() + }, + onDismiss = { showSimProviderDialog = false } + ) + } +} + +@Composable +private fun BatteryOptimizationCard( + isReady: Boolean, + romFamily: RomFamily, + onOpenSystemSettings: () -> Unit, + onOpenRomSettings: () -> Unit +) { + val statusColor = if (isReady) { + MaterialTheme.colorScheme.tertiaryContainer + } else { + MaterialTheme.colorScheme.errorContainer + } + + val romHint = when (romFamily) { + RomFamily.HYPER_OS_XIAOMI -> stringResource(R.string.permission_battery_rom_hint_hyperos) + RomFamily.OXYGEN_OS_ONEPLUS -> stringResource(R.string.permission_battery_rom_hint_oxygenos) + RomFamily.COLOR_OS_OPPO_REALME -> stringResource(R.string.permission_battery_rom_hint_coloros) + RomFamily.ONE_UI_SAMSUNG -> stringResource(R.string.permission_battery_rom_hint_oneui) + RomFamily.GENERIC -> stringResource(R.string.permission_battery_rom_hint_generic) + } + + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = ExpressiveCardShape, + tonalElevation = 2.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AnimatedIcon( + imageVector = Icons.Outlined.Settings, + contentDescription = null, + shape = androidx.compose.foundation.shape.CircleShape, + backgroundColor = MaterialTheme.colorScheme.surfaceContainer, + tint = MaterialTheme.colorScheme.primary + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.permission_battery_title), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(R.string.permission_battery_subtitle), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + AnimatedIcon( + imageVector = if (isReady) Icons.Outlined.CheckCircle else Icons.Outlined.WarningAmber, + contentDescription = null, + shape = androidx.compose.foundation.shape.CircleShape, + backgroundColor = statusColor, + tint = MaterialTheme.colorScheme.onSurface + ) + } + Text( + text = romHint, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ExpressiveButton( + label = stringResource(R.string.permission_battery_system_action), + onClick = onOpenSystemSettings, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ExpressiveButton( + label = stringResource(R.string.permission_battery_oem_action), + onClick = onOpenRomSettings, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } } @Composable diff --git a/app/src/main/java/com/upnp/fakeCall/ui/screens/SettingsScreen.kt b/app/src/main/java/com/upnp/fakeCall/ui/screens/SettingsScreen.kt index 701729f..cd015ae 100644 --- a/app/src/main/java/com/upnp/fakeCall/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/upnp/fakeCall/ui/screens/SettingsScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Alarm import androidx.compose.material.icons.outlined.AccessTime import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Close @@ -72,8 +73,11 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -97,11 +101,13 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.upnp.fakeCall.AnswerAudioMode import com.upnp.fakeCall.BuildConfig import com.upnp.fakeCall.FakeCallViewModel import com.upnp.fakeCall.QuickTriggerManager import com.upnp.fakeCall.ReleaseInfo import com.upnp.fakeCall.R +import com.upnp.fakeCall.SimProviderOption import com.upnp.fakeCall.UpdateCheckResult import com.upnp.fakeCall.ivr.IvrNode import com.upnp.fakeCall.ui.components.AnimatedIcon @@ -129,9 +135,14 @@ fun SettingsScreen( var showAddNodeDialog by rememberSaveable { mutableStateOf(false) } var mappingNodeId by rememberSaveable { mutableStateOf(null) } var pendingAudioNodeId by rememberSaveable { mutableStateOf(null) } + var pendingQuickPresetAudioSlot by rememberSaveable { mutableStateOf(null) } var isCheckingUpdates by rememberSaveable { mutableStateOf(false) } var quickTriggerDelayExpanded by rememberSaveable { mutableStateOf(false) } + var callRingTimeoutExpanded by rememberSaveable { mutableStateOf(false) } + var alarmRingTimeoutExpanded by rememberSaveable { mutableStateOf(false) } var updateDialogRelease by remember { mutableStateOf(null) } + var showSimProviderDialog by remember { mutableStateOf(false) } + var simProviderOptions by remember { mutableStateOf>(emptyList()) } var activeSubmenu by rememberSaveable { mutableStateOf(SettingsSubmenu.MAIN) } var backGestureProgress by remember { mutableStateOf(0f) } @@ -145,6 +156,11 @@ fun SettingsScreen( ) { uri -> viewModel.onRecordingFolderSelected(uri) } + val mp3IvrFolderLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocumentTree() + ) { uri -> + viewModel.onMp3IvrFolderSelected(uri) + } val ivrAudioPicker = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument() @@ -156,6 +172,16 @@ fun SettingsScreen( pendingAudioNodeId = null } + val quickPresetAudioPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + val slot = pendingQuickPresetAudioSlot + if (slot != null) { + viewModel.onQuickTriggerPresetAudioSelected(slot, uri) + } + pendingQuickPresetAudioSlot = null + } + val ivrExportLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("text/xml") ) { uri -> @@ -322,6 +348,18 @@ fun SettingsScreen( ) } + item { + PreferenceCard( + icon = Icons.Outlined.Phone, + title = stringResource(R.string.settings_use_sim_provider_title), + subtitle = stringResource(R.string.settings_use_sim_provider_subtitle), + onClick = { + simProviderOptions = viewModel.loadSimProviderOptions() + showSimProviderDialog = true + } + ) + } + item { PreferenceCard( icon = Icons.Outlined.Settings, @@ -342,22 +380,124 @@ fun SettingsScreen( item { PreferenceCard( icon = Icons.Outlined.MusicNote, - title = stringResource(R.string.settings_select_audio_title), - subtitle = stringResource( - R.string.settings_select_audio_subtitle, - state.selectedAudioName.ifBlank { stringResource(R.string.default_audio_name) } - ), - onClick = { audioPickerLauncher.launch(arrayOf("audio/*")) } - ) - } + title = stringResource(R.string.settings_answer_audio_title), + subtitle = answerAudioModeSummary(state), + onClick = null, + trailingContent = null + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + AnswerAudioModeOption( + title = stringResource(R.string.settings_answer_audio_mode_silent_title), + subtitle = stringResource(R.string.settings_answer_audio_mode_silent_subtitle), + icon = Icons.Outlined.VolumeOff, + selected = state.answerAudioMode == AnswerAudioMode.SILENT, + onClick = { viewModel.onAnswerAudioModeChange(AnswerAudioMode.SILENT) } + ) + AnswerAudioModeOption( + title = stringResource(R.string.settings_answer_audio_mode_file_title), + subtitle = if (state.selectedAudioUri.isBlank()) { + stringResource(R.string.settings_answer_audio_mode_file_missing) + } else { + stringResource(R.string.settings_answer_audio_mode_file_subtitle, state.selectedAudioName) + }, + icon = Icons.Outlined.MusicNote, + selected = state.answerAudioMode == AnswerAudioMode.AUDIO_FILE, + onClick = { + if (state.selectedAudioUri.isBlank()) { + audioPickerLauncher.launch(arrayOf("audio/*")) + } else { + viewModel.onAnswerAudioModeChange(AnswerAudioMode.AUDIO_FILE) + } + } + ) + AnswerAudioModeOption( + title = stringResource(R.string.settings_answer_audio_mode_custom_ivr_title), + subtitle = stringResource(R.string.settings_answer_audio_mode_custom_ivr_subtitle), + icon = Icons.Outlined.Settings, + selected = state.answerAudioMode == AnswerAudioMode.CUSTOM_IVR, + onClick = { viewModel.onAnswerAudioModeChange(AnswerAudioMode.CUSTOM_IVR) } + ) + AnswerAudioModeOption( + title = stringResource(R.string.settings_answer_audio_mode_mp3_ivr_title), + subtitle = if (state.mp3IvrFolderUri.isBlank()) { + stringResource(R.string.settings_answer_audio_mode_mp3_ivr_missing) + } else { + stringResource(R.string.settings_answer_audio_mode_mp3_ivr_subtitle, state.mp3IvrFolderName) + }, + icon = Icons.Outlined.Folder, + selected = state.answerAudioMode == AnswerAudioMode.MP3_IVR, + onClick = { + if (state.mp3IvrFolderUri.isBlank()) { + mp3IvrFolderLauncher.launch(null) + } else { + viewModel.onAnswerAudioModeChange(AnswerAudioMode.MP3_IVR) + } + } + ) - item { - PreferenceCard( - icon = Icons.Outlined.VolumeOff, - title = stringResource(R.string.settings_use_default_audio_title), - subtitle = stringResource(R.string.settings_use_default_audio_subtitle), - onClick = viewModel::clearAudioSelection - ) + when (state.answerAudioMode) { + AnswerAudioMode.SILENT -> { + Text( + text = stringResource(R.string.settings_answer_audio_silent_note), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + AnswerAudioMode.AUDIO_FILE -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { audioPickerLauncher.launch(arrayOf("audio/*")) }, + modifier = Modifier.bounceClick() + ) { + Text(stringResource(R.string.settings_answer_audio_choose_file)) + } + TextButton( + onClick = viewModel::clearAudioSelection, + enabled = state.selectedAudioUri.isNotBlank(), + modifier = Modifier.bounceClick(enabled = state.selectedAudioUri.isNotBlank()) + ) { + Text(stringResource(R.string.action_clear_audio)) + } + } + } + AnswerAudioMode.CUSTOM_IVR -> { + FilledTonalButton( + onClick = { activeSubmenu = SettingsSubmenu.MAILBOX }, + modifier = Modifier + .fillMaxWidth() + .bounceClick() + ) { + Text(stringResource(R.string.settings_answer_audio_configure_keymap)) + } + } + AnswerAudioMode.MP3_IVR -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { mp3IvrFolderLauncher.launch(null) }, + modifier = Modifier.bounceClick() + ) { + Text(stringResource(R.string.settings_answer_audio_choose_folder)) + } + TextButton( + onClick = viewModel::clearMp3IvrFolderSelection, + enabled = state.mp3IvrFolderUri.isNotBlank(), + modifier = Modifier.bounceClick(enabled = state.mp3IvrFolderUri.isNotBlank()) + ) { + Text(stringResource(R.string.action_clear_folder)) + } + } + } + } + } + } } item { @@ -383,6 +523,106 @@ fun SettingsScreen( ) } + item { + PreferenceCategoryHeader(stringResource(R.string.settings_category_call_behavior)) + } + + item { + PreferenceCard( + icon = Icons.Outlined.AccessTime, + title = stringResource(R.string.settings_call_ring_timeout_title), + subtitle = stringResource(R.string.settings_call_ring_timeout_subtitle), + onClick = null, + trailingContent = null + ) { + Box(modifier = Modifier.fillMaxWidth()) { + androidx.compose.material3.OutlinedTextField( + value = FakeCallViewModel.formatRingTimeout(context, state.callRingTimeoutSeconds), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.settings_call_ring_timeout_label)) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = callRingTimeoutExpanded) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp) + ) + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + callRingTimeoutExpanded = !callRingTimeoutExpanded + } + ) + DropdownMenu( + expanded = callRingTimeoutExpanded, + onDismissRequest = { callRingTimeoutExpanded = false } + ) { + viewModel.ringTimeoutOptionsSeconds.forEach { timeoutSeconds -> + DropdownMenuItem( + text = { Text(FakeCallViewModel.formatRingTimeout(context, timeoutSeconds)) }, + onClick = { + viewModel.onCallRingTimeoutChange(timeoutSeconds) + callRingTimeoutExpanded = false + } + ) + } + } + } + } + } + + item { + PreferenceCard( + icon = Icons.Outlined.Alarm, + title = stringResource(R.string.settings_alarm_ring_timeout_title), + subtitle = stringResource(R.string.settings_alarm_ring_timeout_subtitle), + onClick = null, + trailingContent = null + ) { + Box(modifier = Modifier.fillMaxWidth()) { + androidx.compose.material3.OutlinedTextField( + value = FakeCallViewModel.formatRingTimeout(context, state.alarmRingTimeoutSeconds), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.settings_alarm_ring_timeout_label)) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = alarmRingTimeoutExpanded) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp) + ) + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + alarmRingTimeoutExpanded = !alarmRingTimeoutExpanded + } + ) + DropdownMenu( + expanded = alarmRingTimeoutExpanded, + onDismissRequest = { alarmRingTimeoutExpanded = false } + ) { + viewModel.ringTimeoutOptionsSeconds.forEach { timeoutSeconds -> + DropdownMenuItem( + text = { Text(FakeCallViewModel.formatRingTimeout(context, timeoutSeconds)) }, + onClick = { + viewModel.onAlarmRingTimeoutChange(timeoutSeconds) + alarmRingTimeoutExpanded = false + } + ) + } + } + } + } + } + item { PreferenceCategoryHeader(stringResource(R.string.settings_category_storage)) } @@ -418,17 +658,6 @@ fun SettingsScreen( onClick = { activeSubmenu = SettingsSubmenu.AUTOMATION } ) } - item { - PreferenceCategoryHeader(stringResource(R.string.settings_category_mailbox)) - } - item { - PreferenceCard( - icon = Icons.Outlined.Folder, - title = stringResource(R.string.settings_category_mailbox), - subtitle = stringResource(R.string.settings_mailbox_hub_subtitle), - onClick = { activeSubmenu = SettingsSubmenu.MAILBOX } - ) - } } if (submenu == SettingsSubmenu.AUTOMATION) { @@ -494,21 +723,13 @@ fun SettingsScreen( } } Text( - text = stringResource(R.string.settings_quick_triggers_audio_note), + text = stringResource(R.string.settings_quick_triggers_audio_note_with_preset), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } - item { - PreferenceCard( - icon = Icons.Outlined.Settings, - title = stringResource(R.string.settings_open_accessibility_settings), - subtitle = stringResource(R.string.settings_accessibility_note), - onClick = { openAccessibilitySettings(context) } - ) - } item { PreferenceCard( icon = Icons.Outlined.Star, @@ -567,6 +788,13 @@ fun SettingsScreen( style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurface ) + if (state.quickTriggerDefaultPresetSlot == index + 1) { + Text( + text = stringResource(R.string.settings_default_for_accessibility), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + } Text( text = "${preset.callerName.ifBlank { stringResource(R.string.settings_preset_unknown_caller) }} • ${preset.callerNumber} • ${FakeCallViewModel.formatDelay(context, preset.delaySeconds)}", style = MaterialTheme.typography.labelLarge, @@ -577,17 +805,93 @@ fun SettingsScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - TextButton( - onClick = { viewModel.applyQuickTriggerPreset(index + 1) }, - modifier = Modifier.bounceClick() + Text( + text = stringResource(R.string.settings_preset_custom_audio_toggle), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Switch( + checked = preset.useCustomAudio, + onCheckedChange = { enabled -> + viewModel.onQuickTriggerPresetUseCustomAudioChange( + index + 1, + enabled + ) + } + ) + } + if (preset.useCustomAudio) { + Text( + text = stringResource( + R.string.settings_preset_custom_audio_current, + preset.customAudioName.ifBlank { stringResource(R.string.settings_no_audio_selected) } + ), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + pendingQuickPresetAudioSlot = index + 1 + quickPresetAudioPickerLauncher.launch(arrayOf("audio/*")) + }, + modifier = Modifier.bounceClick() + ) { + Text(stringResource(R.string.settings_select_audio_title)) + } + TextButton( + onClick = { viewModel.clearQuickTriggerPresetAudio(index + 1) }, + enabled = preset.customAudioUri.isNotBlank(), + modifier = Modifier.bounceClick(enabled = preset.customAudioUri.isNotBlank()) + ) { + Text(stringResource(R.string.action_clear_audio)) + } + } + } + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text(stringResource(R.string.settings_apply_to_defaults)) + TextButton( + onClick = { viewModel.setQuickTriggerDefaultPreset(index + 1) }, + modifier = Modifier.bounceClick(), + enabled = state.quickTriggerDefaultPresetSlot != index + 1 + ) { + Text( + if (state.quickTriggerDefaultPresetSlot == index + 1) { + stringResource(R.string.settings_default_preset_selected) + } else { + stringResource(R.string.settings_set_as_default_for_accessibility) + } + ) + } + TextButton( + onClick = { viewModel.applyQuickTriggerPreset(index + 1) }, + modifier = Modifier.bounceClick() + ) { + Text(stringResource(R.string.settings_apply_to_defaults)) + } } - TextButton( - onClick = { viewModel.removeQuickTriggerPreset(index + 1) }, - modifier = Modifier.bounceClick() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically ) { - Text(stringResource(R.string.action_remove)) + TextButton( + onClick = { viewModel.removeQuickTriggerPreset(index + 1) }, + modifier = Modifier.bounceClick() + ) { + Text(stringResource(R.string.action_remove)) + } } } } @@ -606,63 +910,102 @@ fun SettingsScreen( trailingContent = null ) } - } - - if (submenu == SettingsSubmenu.MAILBOX) { - item { - PreferenceCategoryHeader(stringResource(R.string.settings_category_mailbox)) - } - item { - PreferenceCard( - icon = Icons.Outlined.Folder, - title = stringResource(R.string.settings_import_mailbox_title), - subtitle = stringResource(R.string.settings_import_mailbox_subtitle), - onClick = { ivrImportLauncher.launch(arrayOf("text/xml", "application/xml")) } - ) - } item { PreferenceCard( - icon = Icons.Outlined.Refresh, - title = stringResource(R.string.settings_export_mailbox_title), - subtitle = stringResource(R.string.settings_export_mailbox_subtitle), - onClick = { ivrExportLauncher.launch("fakecall_mailbox.xml") } + icon = Icons.Outlined.Settings, + title = stringResource(R.string.settings_open_accessibility_settings), + subtitle = stringResource(R.string.settings_accessibility_note), + onClick = { openAccessibilitySettings(context) } ) } + } + + if (submenu == SettingsSubmenu.MAILBOX) { item { - PreferenceCard( - icon = Icons.Outlined.Add, - title = stringResource(R.string.settings_add_node_title), - subtitle = stringResource(R.string.settings_add_node_subtitle), - onClick = { showAddNodeDialog = true } - ) + PreferenceCategoryHeader(stringResource(R.string.settings_category_mailbox)) } - - if (ivrNodes.isEmpty()) { + if (state.answerAudioMode == AnswerAudioMode.MP3_IVR) { item { - Text( - text = stringResource(R.string.settings_no_mailbox_nodes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + PreferenceCard( + icon = Icons.Outlined.MusicNote, + title = stringResource(R.string.settings_mp3_ivr_mode_title), + subtitle = stringResource(R.string.settings_mp3_ivr_mode_subtitle), + onClick = null, + trailingContent = null + ) + } + item { + PreferenceCard( + icon = Icons.Outlined.Folder, + title = stringResource(R.string.settings_mp3_ivr_folder_title), + subtitle = stringResource( + R.string.settings_mp3_ivr_folder_subtitle, + state.mp3IvrFolderName + ), + onClick = { mp3IvrFolderLauncher.launch(null) } + ) + } + item { + PreferenceCard( + icon = Icons.Outlined.Close, + title = stringResource(R.string.settings_mp3_ivr_clear_folder_title), + subtitle = stringResource(R.string.settings_mp3_ivr_clear_folder_subtitle), + onClick = viewModel::clearMp3IvrFolderSelection ) } } else { - items(ivrNodes) { node -> - MailboxNodeCard( - node = node, - nodes = ivrNodes, - isRoot = ivrConfig?.rootId == node.id, - onSetRoot = { viewModel.setIvrRoot(node.id) }, - onSelectAudio = { - pendingAudioNodeId = node.id - ivrAudioPicker.launch(arrayOf("audio/*")) - }, - onClearAudio = { viewModel.clearIvrNodeAudio(node.id) }, - onAddMapping = { mappingNodeId = node.id }, - onRemoveMapping = { digit -> viewModel.removeIvrRoute(node.id, digit) }, - onDelete = { viewModel.removeIvrNode(node.id) } + item { + PreferenceCard( + icon = Icons.Outlined.Folder, + title = stringResource(R.string.settings_import_mailbox_title), + subtitle = stringResource(R.string.settings_import_mailbox_subtitle), + onClick = { ivrImportLauncher.launch(arrayOf("text/xml", "application/xml")) } ) } + item { + PreferenceCard( + icon = Icons.Outlined.Refresh, + title = stringResource(R.string.settings_export_mailbox_title), + subtitle = stringResource(R.string.settings_export_mailbox_subtitle), + onClick = { ivrExportLauncher.launch("fakecall_mailbox.xml") } + ) + } + item { + PreferenceCard( + icon = Icons.Outlined.Add, + title = stringResource(R.string.settings_add_node_title), + subtitle = stringResource(R.string.settings_add_node_subtitle), + onClick = { showAddNodeDialog = true } + ) + } + + if (ivrNodes.isEmpty()) { + item { + Text( + text = stringResource(R.string.settings_no_mailbox_nodes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } else { + items(ivrNodes) { node -> + MailboxNodeCard( + node = node, + nodes = ivrNodes, + isRoot = ivrConfig?.rootId == node.id, + onSetRoot = { viewModel.setIvrRoot(node.id) }, + onSelectAudio = { + pendingAudioNodeId = node.id + ivrAudioPicker.launch(arrayOf("audio/*")) + }, + onClearAudio = { viewModel.clearIvrNodeAudio(node.id) }, + onAddMapping = { mappingNodeId = node.id }, + onRemoveMapping = { digit -> viewModel.removeIvrRoute(node.id, digit) }, + onDelete = { viewModel.removeIvrNode(node.id) } + ) + } + } } } @@ -851,6 +1194,18 @@ fun SettingsScreen( } ) } + + if (showSimProviderDialog) { + SimProviderPickerDialog( + options = simProviderOptions, + onSelect = { option -> + viewModel.applySimProviderName(option) + showSimProviderDialog = false + }, + onKeepCurrent = { showSimProviderDialog = false }, + onDismiss = { showSimProviderDialog = false } + ) + } } private enum class SettingsSubmenu { @@ -868,6 +1223,85 @@ private fun PreferenceCategoryHeader(title: String) { ) } +@Composable +private fun answerAudioModeSummary(state: com.upnp.fakeCall.FakeCallUiState): String { + return when (state.answerAudioMode) { + AnswerAudioMode.SILENT -> stringResource(R.string.settings_answer_audio_summary_silent) + AnswerAudioMode.AUDIO_FILE -> stringResource( + R.string.settings_answer_audio_summary_file, + state.selectedAudioName.ifBlank { stringResource(R.string.default_audio_name) } + ) + AnswerAudioMode.CUSTOM_IVR -> stringResource(R.string.settings_answer_audio_summary_custom_ivr) + AnswerAudioMode.MP3_IVR -> stringResource( + R.string.settings_answer_audio_summary_mp3_ivr, + state.mp3IvrFolderName.ifBlank { stringResource(R.string.settings_mp3_ivr_no_folder_selected) } + ) + } +} + +@Composable +private fun AnswerAudioModeOption( + title: String, + subtitle: String, + icon: ImageVector, + selected: Boolean, + onClick: () -> Unit +) { + val containerColor = if (selected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainer + } + val contentColor = if (selected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .bounceClick(onClick = onClick), + shape = RoundedCornerShape(24.dp), + color = containerColor, + contentColor = contentColor, + tonalElevation = if (selected) 2.dp else 1.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = contentColor + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + color = contentColor + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelLarge, + color = contentColor.copy(alpha = 0.78f) + ) + } + if (selected) { + Icon( + imageVector = Icons.Outlined.CheckCircle, + contentDescription = null, + tint = contentColor + ) + } + } + } +} + @Composable private fun PreferenceCard( icon: ImageVector, diff --git a/app/src/main/java/com/upnp/fakeCall/ui/screens/SimProviderPickerDialog.kt b/app/src/main/java/com/upnp/fakeCall/ui/screens/SimProviderPickerDialog.kt new file mode 100644 index 0000000..c5b7fd0 --- /dev/null +++ b/app/src/main/java/com/upnp/fakeCall/ui/screens/SimProviderPickerDialog.kt @@ -0,0 +1,101 @@ +package com.upnp.fakeCall.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.upnp.fakeCall.R +import com.upnp.fakeCall.SimProviderOption +import com.upnp.fakeCall.ui.components.AnimatedIcon + +@Composable +fun SimProviderPickerDialog( + options: List, + onSelect: (SimProviderOption) -> Unit, + onKeepCurrent: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.sim_provider_dialog_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = if (options.isEmpty()) { + stringResource(R.string.sim_provider_no_sims) + } else { + stringResource(R.string.sim_provider_dialog_subtitle) + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + options.forEach { option -> + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(option) }, + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 1.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AnimatedIcon( + imageVector = Icons.Outlined.Phone, + contentDescription = null, + shape = CircleShape, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = option.displayName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.sim_provider_slot_label, option.slotIndex + 1), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + }, + confirmButton = { + if (options.size == 1) { + TextButton(onClick = { onSelect(options.first()) }) { + Text(stringResource(R.string.sim_provider_use_this)) + } + } + }, + dismissButton = { + TextButton(onClick = onKeepCurrent) { + Text(stringResource(R.string.sim_provider_keep_current)) + } + } + ) +} diff --git a/app/src/main/res/drawable/widget_card_background.xml b/app/src/main/res/drawable/widget_card_background.xml new file mode 100644 index 0000000..fcfe4c7 --- /dev/null +++ b/app/src/main/res/drawable/widget_card_background.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_chip_background.xml b/app/src/main/res/drawable/widget_chip_background.xml new file mode 100644 index 0000000..453eb30 --- /dev/null +++ b/app/src/main/res/drawable/widget_chip_background.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_contact_avatar_bg.xml b/app/src/main/res/drawable/widget_contact_avatar_bg.xml new file mode 100644 index 0000000..0780a96 --- /dev/null +++ b/app/src/main/res/drawable/widget_contact_avatar_bg.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/widget_preset_chip.xml b/app/src/main/res/drawable/widget_preset_chip.xml new file mode 100644 index 0000000..4e2975c --- /dev/null +++ b/app/src/main/res/drawable/widget_preset_chip.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_preset_chip_active.xml b/app/src/main/res/drawable/widget_preset_chip_active.xml new file mode 100644 index 0000000..e83ccfc --- /dev/null +++ b/app/src/main/res/drawable/widget_preset_chip_active.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_primary_button.xml b/app/src/main/res/drawable/widget_primary_button.xml new file mode 100644 index 0000000..170688c --- /dev/null +++ b/app/src/main/res/drawable/widget_primary_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/widget_soft_button.xml b/app/src/main/res/drawable/widget_soft_button.xml new file mode 100644 index 0000000..78feda5 --- /dev/null +++ b/app/src/main/res/drawable/widget_soft_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 32f3fc8..820b243 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -1,6 +1,6 @@ + - Fake Call - + FakeCall Willkommen bei FakeCall Ein hochwertiges Fake-Call-Erlebnis mit ausdrucksstarken Animationen und sofortiger Planung. @@ -20,14 +20,21 @@ Präzise Alarme & Erinnerungen Benötigt für zeitgenaue Anrufe ab Android 12+. Präzise Alarme aktivieren + Akku-Optimierung + Hintergrundstart + Empfohlen bei aggressiven ROMs, damit Anrufe auch bei ausgeschaltetem Display durchkommen. + System-Akkuoptimierung deaktivieren + ROM-Hintergrund/Autostart öffnen + HyperOS/Xiaomi: Autostart erlauben und den Energiesparmodus für FakeCall auf Keine Einschränkungen setzen. + OxygenOS/OnePlus: Auto-Start + Hintergrundaktivität erlauben und FakeCall aus der Akkuoptimierung entfernen. + ColorOS/realme UI: Auto-Start erlauben und Hintergrund-Einfrieren bzw. tiefe Optimierung für FakeCall deaktivieren. + Samsung One UI: FakeCall auf Uneingeschränkt stellen und Hintergrundaktivität erlauben. + Wenn Anrufe bei ausgeschaltetem Display nicht ankommen: FakeCall auf Uneingeschränkten Akku setzen und Hintergrundaktivität/Autostart erlauben. Abgeschlossen Kannst du die Anrufkonten nicht öffnen? Einige OnePlus/Oppo/Realme/Samsung-Builds verstecken diesen Bildschirm. Du kannst ihn direkt per ADB öffnen: - adb shell am start -a android.intent.action.MAIN -n com.android.server.telecom/.settings.EnableAccountPreferenceActivity Alles bereit! Du kannst jetzt deinen ersten Fake-Anruf planen. Einrichtung abschließen Einrichtung abschließen (Berechtigungen erforderlich) - FakeCall Plane deinen perfekten Ausweg @@ -53,7 +60,6 @@ Exakte Zeit auswählen MIN SEK - Zurück Einstellungen @@ -65,14 +71,43 @@ Anbietername Anbieter speichern & registrieren Macht dieses Konto für eingehende Anrufe verfügbar. + SIM-Anbietername verwenden + Wähle den stabilen SIM-Kartennamen von einer echten SIM auf diesem Gerät. Anbieter im System aktivieren Anbieter ist aktiviert. Öffne Anrufkonten, um ihn zu aktivieren. Audio + Anrufverhalten Audiodatei auswählen Aktuell: %s Standard-Audio verwenden Benutzerdefinierte Audiowiedergabe deaktivieren. + Audio beim Annehmen + Stumm nach dem Annehmen + Audiodatei: %s + Benutzerdefinierte Keymap-IVR + MP3-Ordner-IVR: %s + Stumm + Nach dem Annehmen wird nichts abgespielt. + Audiodatei + Aktuelle Datei: %s + Wähle eine Datei, um diesen Modus zu nutzen. + Benutzerdefinierte Keymap (IVR) + Nutze manuelle Tasten-Routen und Knoten-Audio. + MP3-Ordner-IVR + Aktueller Ordner: %s + Wähle einen Ordner, um diesen Modus zu nutzen. + Dieser Modus vermeidet Konflikte, indem ausgewählte Dateien und IVR-Quellen ignoriert werden. + Datei wählen + Ordner wählen + Keymap konfigurieren + Klingeldauer (Anruf) + Wie lange normale Fake-Anrufe klingeln sollen, bevor sie ohne Antwort beendet werden. + Normaler Fake-Anruf + Klingeldauer (Alarm) + Wie lange Alarm-Anrufe klingeln sollen, bevor sie ohne Antwort beendet werden. + Alarm-Anruf + Unbegrenzt (klingelt weiter) Mikrofonaufnahme Aktiviert Deaktiviert @@ -81,16 +116,17 @@ Speichern unter: %s Aufnahmeordner zurücksetzen Downloads/FakeCall verwenden - Automatisierung - Automatisierung & Quick-Trigger-Standards - Wird von externen Intents und dem Bedienungshilfen-Kurzbefehl verwendet. - Verwalte externe Trigger, Bedienungshilfen-Kurzbefehle und Standardwerte an einem Ort. + Benutzerdefinierte Presets und Automatisierung + Benutzerdefinierte Presets und Automatisierung + Wird für Schnelleinstellungen-Kacheln, Homescreen-Shortcuts, Widget, Bedienungshilfen-Kurzbefehl und externe API-Intents verwendet. + Verwalte Presets, Shortcuts, Widget-Verhalten, Bedienungshilfen-Trigger und API-Standardwerte an einem Ort. Quick-Trigger-Standardwerte Automatisierungs-API Standard-Anrufername Standard-Anrufernummer Standardverzögerung Quick Trigger verwenden das im Bereich Audio konfigurierte Audio – inklusive deiner ausgewählten Datei oder Standardwiedergabe. + Quick Trigger nutzen standardmäßig die Audio-Einstellung oben. Für jedes gespeicherte Preset kannst du unten optional eine eigene Audio-Datei aktivieren. Bedienungshilfen-Einstellungen öffnen Aktiviere dort den FakeCall-Bedienungshilfen-Dienst, um die Systemtaste oder den Kurzbefehl für sofortige Planung zu nutzen. Name der Voreinstellung (optional) @@ -98,22 +134,51 @@ Noch keine Quick-Trigger-Voreinstellungen. Speichere eine, um sie als App-Aktion und Schnelleinstellungen-Kachel bereitzustellen. Voreinstellung %1$d: %2$s Unbekannter Anrufer + Eigene Audio-Datei für dieses Preset nutzen + Preset-Audio: %1$s + Standard für Bedienungshilfen-Kurzbefehl + Als Bedienungshilfen-Standard setzen + Bedienungshilfen-Standard Auf Standardwerte anwenden - Voreinstellungen erscheinen als App-Aktionen beim langen Drücken auf das App-Icon und als Schnelleinstellungen-Kacheln (Voreinstellung 1–5). + Presets werden für Launcher-Shortcuts, Schnelleinstellungen-Kacheln, Widget-Aktionen, Standardverhalten im Bedienungshilfen-Kurzbefehl und API-Fallback-Werte verwendet. Aktion: `com.upnp.fakeCall.TRIGGER` mit Extras `caller_name`, `caller_number` und `delay`. - Mailbox (IVR) - Verwalte IVR-Knoten, Zuordnungen und Import/Export, ohne die Haupteinstellungen zu überladen. - Mailbox-XML importieren - Einen gespeicherten IVR-Baum laden. - Mailbox-XML exportieren + Favoritenkontakt + Zeitpunkt + Anruf planen + FakeCall öffnen + Noch keine favorisierten Kontakte. Markiere Kontakte in FakeCall mit einem Stern. + Keine Favoriten + Kontakte in der App mit Stern markieren. + Geplanter Anruf abgebrochen. + Schritt 1/3 • Kontakt wählen + Schritt 2/3 • Zeit wählen + Schritt 3/3 • Aktion bestätigen + %1$s • %2$s + Quick-Presets + Noch keine Quick-Presets vorhanden. + Benutzerdefinierte Tastenbelegung (IVR) + Konfiguriere IVR entweder mit benutzerdefinierten Knoten oder MP3-Ordnernavigation. + IVR-XML importieren + Ein gespeichertes IVR-Backup laden. + IVR-Modus + Benutzerdefiniert nutzt deine manuell gepflegten Tasten-Zuordnungen. MP3-Player ordnet Tasten automatisch aus dem gewählten Ordner zu und sagt Optionen per TTS an. + Benutzerdefiniert + MP3-Player + MP3-Player-IVR-Modus + Ordnerinhalte automatisch auf Tasten legen und per TTS ansagen. + MP3-IVR-Ordner auswählen + Aktueller Ordner: %s + MP3-IVR-Ordner löschen + Ordnerbasierte IVR-Quelle deaktivieren. + Kein Ordner ausgewählt + IVR-XML exportieren Deinen aktuellen IVR-Baum teilen. Menüknoten hinzufügen Ein neues IVR-Menü erstellen. - Noch keine Mailbox-Knoten. + Noch keine IVR-Knoten. Über & Updates Aktuelle Version: %s GitHub-Repository - DDOneApps/FakeCall Prüfe auf Updates\u2026 Nach Updates suchen Du nutzt die neueste Version. @@ -142,46 +207,77 @@ Zielknoten Füge einen weiteren Knoten hinzu, um Zuordnungen zu erstellen. Hinzufügen - Anruferdetails + Name + Nummer + Kontakt Anrufername Anrufernummer + Kontakt auswählen + Ausgewählt + Ausgewählter Kontakt + Angeheftete Kontakte + Zuletzt verwendet Wird auf dem eingehenden Anrufbildschirm angezeigt. Zeitplanung Gespeicherte Voreinstellungen Voreinstellung entfernen - Audio-Vorschau + Audio beim Annehmen Tippen, um Audio zu ändern oder zu deaktivieren. + MP3-IVR-Ordner + Benutzerdefinierte IVR + IVR + Fallback-Audio + Ausgewähltes Audio + Kein Audio beim Annehmen + Ausgewähltes Audio + Stumm nach dem Annehmen + Beim Annehmen wird das Ordnermenü angesagt; Tasteneingaben wählen Ordner oder Sounds. + MP3-IVR ist aktiv, aber es ist noch kein Ordner ausgewählt. Der angenommene Anruf fordert zur Ordnerauswahl auf. + Das Audio des IVR-Startknotens wird zuerst abgespielt. Tasteneingaben können zu anderen IVR-Knoten wechseln. + Benutzerdefinierte IVR ist aktiv, aber der Startknoten hat noch kein Audio. Tasten-Routen können nach dem Annehmen trotzdem Knoten-Audio abspielen. + Benutzerdefinierte IVR-Routen sind aktiv, aber der Startknoten hat kein Audio. Das ausgewählte Fallback-Audio wird zuerst abgespielt. + Benutzerdefinierte IVR-Routen sind aktiv, aber Startknoten und Fallback-Audio sind leer. + Dieses ausgewählte Audio wird beim Annehmen des Anrufs abgespielt. + Audiodatei-Modus ist aktiv, aber es ist keine Datei ausgewählt. Wähle eine Datei in den Einstellungen. + Nach dem Annehmen wird kein Audio, TTS und kein IVR-Menü abgespielt. Stopp Abspielen Einstellungen öffnen Audio löschen - + Ordner löschen + Audio ändern + Audio auswählen + IVR konfigurieren + Fallback löschen Abbrechen Entfernen Anwenden - + SIM-Anbietername verwenden? + Wähle, welcher stabile SIM-Kartenname als FakeCall-Anbieter angezeigt werden soll. + Es wurde kein stabiler SIM-Kartenname gefunden. Prüfe die Telefonberechtigungen oder gib den Anbieternamen manuell ein. + SIM-Steckplatz %1$d + Diesen Anbieter verwenden + Aktuellen behalten Standard Fake Call Anbieter - Downloads/FakeCall Menü Unbekannter Anrufer Ausgewählter Ordner Ausgewähltes Audio - Fake-Call-Planung Anrufaufnahme Anruf wird aufgenommen Aufnahme ist aktiv + Anrufaufnahme + Aufnahme ist für angenommene Anrufe aktiviert. + Aufnahme ist aus. Fake-Anruf geplant Fake-Anruf: %s Eingehender Anruf in %1$d Sekunden von %2$s Unbekannt - Voreinstellung %d FakeCall Voreinstellung 1 @@ -195,18 +291,15 @@ Voreinstellung %d ist nicht konfiguriert Voreinstellung %d konnte nicht ausgeführt werden Fake Call konnte nicht geplant werden - Update %s verfügbar! Herunterladen Update-Hinweis schließen - Unbekannt v Jetzt %1$s → %2$s - Erteile Telefonberechtigungen, um fortzufahren. Erteile zuerst Telefonberechtigungen. @@ -216,6 +309,11 @@ Preset nicht gefunden. Preset auf Quick-Trigger-Standardwerte angewendet. Quick-Trigger-Preset entfernt. + Dieses Preset nutzt jetzt eine eigene Audio-Datei. + Dieses Preset nutzt jetzt die Standard-Audio-Einstellung. + Preset-Audio ausgewählt: %1$s + Preset-Audio entfernt. + Preset %1$d ist jetzt der Bedienungshilfen-Standard. Preset bereits gespeichert. Benutzerdefiniertes Preset gespeichert. Preset entfernt. @@ -234,10 +332,31 @@ Erteile Telefonberechtigungen vor der Planung. Aktiviere diese App zuerst in den systemweiten Anrufkonten. Gib eine Anrufernummer ein, bevor du planst. + Wähle einen Kontakt aus, bevor du planst. + Dieser Kontakt konnte nicht gelesen werden. Bitte wähle einen anderen. Eingehender Anruf wird jetzt ausgelöst. Anruf konnte nicht ausgelöst werden. Aktiviere den Anbieter in den Anrufkonten. Aktiviere präzise Alarme, um zeitgenaue Anrufe zu planen. + Wähle einen MP3-IVR-Ordner aus, um diesen Modus zu nutzen. + Wähle eine Audiodatei aus, um diesen Modus zu nutzen. + Audio beim Annehmen auf stumm gesetzt. + Audio beim Annehmen auf ausgewählte Datei gesetzt. + Audio beim Annehmen auf benutzerdefinierte IVR gesetzt. + Audio beim Annehmen auf MP3-Ordner-IVR gesetzt. Timer abgebrochen. Anruf geplant für %s. Timer gestartet für %s. + Der ausgewählte Ordner enthält keine Audiodateien oder Unterordner. + Menü %1$s. + Seite %1$d von %2$d. + Drücke %1$d für Ordner %2$s. + Drücke %1$d für Audio %2$s. + Drücke Raute für die nächste Seite. + Drücke Stern für die vorherige Seite. + Drücke 0, um zum vorherigen Ordner zurückzugehen. + Keine nächste Seite verfügbar. + Keine vorherige Seite verfügbar. + Du bist bereits im Hauptordner. + Ungültige Auswahl. + Diese Audiodatei konnte nicht abgespielt werden. diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index cdbcffe..fb01f05 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -1,6 +1,6 @@ + Fake Call - Καλώς ήρθατε στο FakeCall Μια μοναδική εμπειρία ψεύτικων κλήσεων με εκφραστική κίνηση και άμεσο προγραμματισμό. @@ -23,11 +23,9 @@ Ολοκληρώθηκε Δεν μπορείτε να ανοίξετε τις ρυθμίσεις λογαριασμών κλήσης; Ορισμένες κατασκευές OnePlus/Oppo/Realme/Samsung κρύβουν την οθόνη. Μπορείτε να την ανοίξετε απευθείας μέσω ADB: - adb shell am start -a android.intent.action.MAIN -n com.android.server.telecom/.settings.EnableAccountPreferenceActivity Όλα έτοιμα! Είστε έτοιμοι να προγραμματίσετε την πρώτη σας ψεύτικη κλήση. Ολοκλήρωση ρυθμίσεων Ολοκλήρωση ρυθμίσεων (απαιτούνται άδειες) - FakeCall Προγραμματίστε την τέλεια διαφυγή σας @@ -53,7 +51,6 @@ Επιλέξτε ακριβή ώρα ΛΕΠ ΔΕΥ - Πίσω Ρυθμίσεις @@ -86,7 +83,7 @@ Χρησιμοποιούνται από εξωτερικές προθέσεις και τη συντόμευση προσβασιμότητας. Διαχειριστείτε εξωτερικά triggers, τη συντόμευση προσβασιμότητας και τις προεπιλογές σε ένα σημείο. Προεπιλογές Quick Trigger - Automation API + API Αυτοματισμού Προεπιλεγμένο όνομα καλούντος Προεπιλεγμένος αριθμός καλούντος Προεπιλεγμένη καθυστέρηση @@ -101,19 +98,29 @@ Εφαρμογή στις προεπιλογές Οι προρυθμίσεις εμφανίζονται ως συντομεύσεις του εικονιδίου της εφαρμογής και ως πλακίδια γρήγορων ρυθμίσεων (Προρύθμιση 1-5). Δράση: `com.upnp.fakeCall.TRIGGER` με επιπλέον `caller_name`, `caller_number` και `delay`. - Γραμματοκιβώτιο (IVR) - Διαχειριστείτε κόμβους IVR, αντιστοιχίσεις και εισαγωγή/εξαγωγή χωρίς υπερφόρτωση των κύριων ρυθμίσεων. - Εισαγωγή XML γραμματοκιβωτίου + Προσαρμοσμένοι συνδυασμοί πλήκτρων (IVR) + Ρυθμίστε τη συμπεριφορά IVR, είτε με προσαρμοσμένες αντιστοιχίσεις κόμβων, είτε με πλοήγηση φακέλου MP3. + Εισαγωγή IVR XML Φόρτωση αποθηκευμένου δέντρου IVR. - Εξαγωγή XML γραμματοκιβωτίου + Λειτουργία IVR + Η προσαρμοσμένη λειτουργία χρησιμοποιεί τις χειροκίνητες αντιστοιχίσεις πλήκτρων σας. Η λειτουργία αναπαραγωγής MP3 αντιστοιχεί αυτόματα πλήκτρα από τον επιλεγμένο φάκελο και ανακοινώνει τις επιλογές με TTS. + Προσαρμοσμένη λειτουργία + Λειτουργία αναπαραγωγής MP3 + Λειτουργία IVR αναπαραγωγής MP3 + Αυτόματη χαρτογράφηση πλήκτρων από έναν επιλεγμένο φάκελο και χρήση TTS για την ανακοίνωση καταχωρήσεων. + Επιλογή φακέλου MP3 IVR + Τρέχων φάκελος: %s + Καθαρισμός φακέλου MP3 IVR + Απενεργοποίηση πηγής πλοήγησης IVR βάσει φακέλου. + Δεν επιλέχθηκε φάκελος + Εξαγωγή IVR XML Κοινοποίηση του τρέχοντος δέντρου IVR σας. Προσθήκη κόμβου μενού Δημιουργία νέου μενού IVR. - Κανένας κόμβος γραμματοκιβωτίου ακόμα. + Κανένας κόμβος IVR ακόμα. Σχετικά και ενημερώσεις Τρέχουσα έκδοση: %s Αποθετήριο GitHub - DDOneApps/FakeCall Έλεγχος... Έλεγχος για ενημερώσεις Χρησιμοποιείτε την τελευταία έκδοση. @@ -142,11 +149,17 @@ Κόμβος προορισμού Προσθέστε έναν άλλο κόμβο για να δημιουργήσετε αντιστοιχίσεις. Προσθήκη - Στοιχεία καλούντος + Όνομα + αριθμός + Επαφή Όνομα καλούντος Αριθμός καλούντος + Επιλογή επαφής + Επιλεγμένη + Επιλεγμένη επαφή + Καρφιτσωμένες επαφές + Πρόσφατες επαφές Εμφανίζεται στην οθόνη εισερχόμενης κλήσης. Χρονομέτρηση Αποθηκευμένες προρυθμίσεις @@ -157,21 +170,17 @@ Αναπαραγωγή Άνοιγμα ρυθμίσεων Διαγραφή ήχου - Ακύρωση Αφαίρεση Εφαρμογή - Προεπιλεγμένος Πάροχος Fake Call - Downloads/FakeCall Μενού Άγνωστος καλών Επιλεγμένος φάκελος Επιλεγμένος ήχος - Προγραμματιστής Fake Call Εγγραφή κλήσης @@ -181,7 +190,6 @@ Ψεύτικη κλήση: %s Εισερχόμενη κλήση σε %1$d δευτερόλεπτα από %2$s Άγνωστος - Προρύθμιση %d Προρύθμιση FakeCall 1 @@ -195,18 +203,15 @@ Η προρύθμιση %d δεν έχει ρυθμιστεί Δεν ήταν δυνατή η εκτέλεση της προρύθμισης %d Δεν ήταν δυνατός ο προγραμματισμός του Fake Call - Ενημέρωση %s διαθέσιμη! Λήψη Απόρριψη ενημέρωσης - Άγνωστο v Τώρα %1$s → %2$s - Χορηγήστε άδειες τηλεφώνου για να συνεχίσετε. Χορηγήστε πρώτα άδειες τηλεφώνου. @@ -234,10 +239,26 @@ Χορηγήστε άδειες τηλεφώνου πριν προγραμματίσετε. Ενεργοποιήστε πρώτα αυτήν την εφαρμογή στους λογαριασμούς κλήσης του συστήματος. Εισάγετε έναν αριθμό καλούντος πριν προγραμματίσετε. + Επιλέξτε μια επαφή πριν τον προγραμματισμό. + Αδύνατη η ανάγνωση αυτής της επαφής. Επιλέξτε μια άλλη. Ενεργοποίηση εισερχόμενης κλήσης τώρα. Δεν ήταν δυνατή η ενεργοποίηση κλήσης. Ενεργοποιήστε τον πάροχο στους λογαριασμούς κλήσης. Ενεργοποιήστε τις ακριβείς ειδοποιήσεις για προγραμματισμό ακριβών κλήσεων. + Επιλέξτε έναν φάκελο MP3 IVR για να χρησιμοποιήσετε αυτή τη λειτουργία. Το χρονόμετρο ακυρώθηκε. Κλήση προγραμματισμένη για %s. Το χρονόμετρο ξεκίνησε για %s. + Ο επιλεγμένος φάκελος δεν έχει αρχεία ήχου ή υποφακέλους. + %1$s μενού. + Σελίδα %1$d από %2$d. + Πατήστε %1$d για φάκελο %2$s. + Πατήστε %1$d για ήχο %2$s. + Πατήστε λίρα για την επόμενη σελίδα. + Πατήστε αστέρι για την προηγούμενη σελίδα. + Πατήστε 0 για να επιστρέψετε στον προηγούμενο φάκελο. + Δεν υπάρχει επόμενη σελίδα. + Δεν υπάρχει προηγούμενη σελίδα. + Είστε ήδη στον αρχικό φάκελο. + Μη έγκυρη επιλογή. + Αδυναμία αναπαραγωγής αυτού του αρχείου ήχου. diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-kk-rKZ/strings.xml b/app/src/main/res/values-kk-rKZ/strings.xml index 3e13d7d..66fc550 100644 --- a/app/src/main/res/values-kk-rKZ/strings.xml +++ b/app/src/main/res/values-kk-rKZ/strings.xml @@ -1,7 +1,6 @@ Fake Call - FakeCall-ге қош келдіңіз Экспрессивті анимациясы және жедел жоспарлауы бар жоғары сапалы жалған қоңырау тәжірибесі. @@ -24,11 +23,9 @@ Орындалды Қоңырау аккаунттары параметрлерін аша алмайсыз ба? Кейбір OnePlus/Oppo/Realme/Samsung құрылғыларында экран жасырылған. ADB арқылы тікелей ашуға болады: - adb shell am start -a android.intent.action.MAIN -n com.android.server.telecom/.settings.EnableAccountPreferenceActivity Бәрі дайын! Алғашқы жалған қоңырауды жоспарлауға болады. Баптауды аяқтау Баптауды аяқтау (рұқсаттар қажет) - FakeCall Мінсіз шығу жолыңызды жоспарлаңыз @@ -54,7 +51,6 @@ Нақты уақытты таңдау МИН СЕК - Артқа Параметрлер @@ -102,19 +98,29 @@ Әдепкілерге қолдану Алдын ала орнатулар іске қосқышты ұзақ басқанда қолданба әрекеттері және жылдам параметрлер тақтасы (1-5 алдын ала орнату) ретінде пайда болады. Әрекет: `com.upnp.fakeCall.TRIGGER` және `caller_name`, `caller_number`, `delay` қосымшалары. - Пошта жәшігі (IVR) - Негізгі баптауларды толтырмай, IVR түйіндерін, бағыттауларын және импорт/экспортты басқарыңыз. - Пошта жәшігі XML-ін импорттау + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML Сақталған IVR ағашын жүктеу. - Пошта жәшігі XML-ін экспорттау + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML Ағымдағы IVR ағашыңызды бөлісу. Мәзір түйінін қосу Жаңа IVR мәзірін жасау. - Пошта жәшігі түйіндері әлі жоқ. + No IVR nodes yet. Туралы және жаңартулар Ағымдағы нұсқа: %s GitHub репозиторийі - DDOneApps/FakeCall Тексерілуде\u2026 Жаңартуларды тексеру Сіз соңғы нұсқаны пайдаланасыз. @@ -143,11 +149,17 @@ Мақсатты түйін Бейнелеулер жасау үшін басқа түйін қосыңыз. Қосу - Қоңырау шалушы мәліметтері + Name + number + Contact Қоңырау шалушы атауы Қоңырау шалушы нөмірі + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts Кіріс қоңырау экранында көрсетіледі. Уақыт Сақталған алдын ала орнатулар @@ -158,21 +170,17 @@ Ойнату Параметрлерді ашу Аудиоды тазалау - Болдырмау Жою Қолдану - Әдепкі Жалған қоңырау провайдері - Downloads/FakeCall Мәзір Белгісіз қоңырау шалушы Таңдалған қалта Таңдалған аудио - Жалған қоңырау жоспарлаушысы Қоңырауды жазу @@ -182,7 +190,6 @@ Жалған қоңырау: %s %2$s нөмірінен %1$d секундтан кейін кіріс қоңырау Белгісіз - Алдын ала орнату %d FakeCall алдын ала орнатуы 1 @@ -196,18 +203,15 @@ %d алдын ала орнатуы конфигурацияланбаған %d алдын ала орнатуын іске қосу мүмкін болмады Жалған қоңырауды жоспарлау мүмкін болмады - %s жаңартуы қолжетімді! Жүктеп алу Жаңарту хабарламасын жабу - Белгісіз v Қазір %1$s → %2$s - Жалғастыру үшін телефон рұқсаттарын беріңіз. Алдымен телефон рұқсаттарын беріңіз. @@ -235,10 +239,26 @@ Жоспарлаудан бұрын телефон рұқсаттарын беріңіз. Алдымен жүйелік Қоңырау аккаунттарында осы қолданбаны қосыңыз. Жоспарлаудан бұрын қоңырау шалушы нөмірін енгізіңіз. + Select a contact before scheduling. + Could not read this contact. Please select another one. Кіріс қоңырау қазір іске қосылуда. Қоңырауды іске қосу мүмкін болмады. Қоңырау аккаунттарында провайдерді қосыңыз. Нақты қоңырауларды жоспарлау үшін дәл дабылдарды қосыңыз. + Select an MP3 IVR folder to use this mode. Таймер тоқтатылды. Қоңырау %s уақытына жоспарланды. %s уақытына таймер іске қосылды. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 28a3071..fdf2acb 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -1,7 +1,6 @@ Fake Call - Добро пожаловать в FakeCall Реалистичные фейковые звонки с выразительной анимацией и мгновенным планированием. @@ -24,11 +23,9 @@ Выполнено Не можете открыть настройки аккаунтов звонков? Некоторые прошивки OnePlus/Oppo/Realme/Samsung скрывают этот экран. Можно открыть напрямую через ADB: - adb shell am start -a android.intent.action.MAIN -n com.android.server.telecom/.settings.EnableAccountPreferenceActivity Всё готово! Можно запланировать первый фейковый звонок. Завершить настройку Завершить настройку (нужны разрешения) - FakeCall Запланируйте идеальный выход @@ -54,7 +51,6 @@ Выберите точное время МИН СЕК - Назад Настройки @@ -102,19 +98,29 @@ Применить как значения по умолчанию Пресеты появляются как действия приложения при долгом нажатии на ярлык и как плитки быстрых настроек (Пресет 1–5). Действие: `com.upnp.fakeCall.TRIGGER` с параметрами `caller_name`, `caller_number` и `delay`. - Почтовый ящик (IVR) - Управляйте IVR-узлами, маршрутами и импортом/экспортом, не перегружая основные настройки. - Импортировать XML почтового ящика + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML Загрузить сохранённое IVR-дерево. - Экспортировать XML почтового ящика + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML Поделиться текущим IVR-деревом. Добавить узел меню Создать новое IVR-меню. - Узлов почтового ящика пока нет. + No IVR nodes yet. О приложении и обновления Текущая версия: %s Репозиторий GitHub - DDOneApps/FakeCall Проверка\u2026 Проверить обновления У вас установлена последняя версия. @@ -143,11 +149,17 @@ Целевой узел Добавьте ещё один узел, чтобы создавать привязки. Добавить - Данные звонящего + Name + number + Contact Имя звонящего Номер звонящего + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts Отображается на экране входящего звонка. Время Сохранённые пресеты @@ -158,21 +170,17 @@ Играть Открыть настройки Очистить аудио - Отмена Удалить Применить - По умолчанию Fake Call Provider - Downloads/FakeCall Меню Неизвестный звонящий Выбранная папка Выбранное аудио - Планировщик Fake Call Запись звонка @@ -182,7 +190,6 @@ Фейковый звонок: %s Входящий звонок через %1$d сек. от %2$s Неизвестный - Пресет %d Пресет FakeCall 1 @@ -196,18 +203,15 @@ Пресет %d не настроен Не удалось запустить пресет %d Не удалось запланировать фейковый звонок - Доступно обновление %s! Скачать Скрыть уведомление об обновлении - Неизвестно v Сейчас %1$s → %2$s - Предоставьте разрешения на звонки, чтобы продолжить. Сначала предоставьте разрешения на звонки. @@ -235,10 +239,26 @@ Предоставьте разрешения на звонки перед планированием. Сначала включите это приложение в системных аккаунтах звонков. Введите номер звонящего перед планированием. + Select a contact before scheduling. + Could not read this contact. Please select another one. Входящий звонок запускается прямо сейчас. Не удалось запустить звонок. Включите провайдер в аккаунтах звонков. Включите точные будильники для планирования точных звонков. + Select an MP3 IVR folder to use this mode. Таймер отменён. Звонок запланирован на %s. Таймер запущен на %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml new file mode 100644 index 0000000..6c8ae74 --- /dev/null +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Вітаємо вас в \"FakeCall\" + Преміум-функція здійснення викликів із моментальним рухом та запланованим плануванням. + Основні переваги + Реалістичні вхідні дзвінки + Експресивні виклики з розумним плануванням. + Швидкий набір + точний час + Заплануйте миттєво або обирайте точний час. + Аудіо + IVR-маршрутизація + Відтворення користувацького аудіо та маршруту з голосами клавіатури. + Телефон + доступ до мікрофона + Потрібно симуляцію для дзвінків і запису звуку. + Надати дозволи + Здійснити & керувати вхідними дзвінками + Включити \"FakeCall\" у системних дзвінках + Відкрити системні дзвінки + Точні сигнали & нагадування + Потрібно для дзвінків з точним часом на Android 12+ + Увімкнути точні сигнали + Завершено + Не можна відкрити параметри облікових записів? + Деякі версії OnePlus/Oppo/Realme/Samsung приховують екран. Ви можете відкрити його безпосередньо через ADB: + Все налаштовано! Ви готові назначити ваш перший фальшивий дзвінок. + Закінчити налаштування + Закінчити налаштування(Необхідно дозвіл) + + FakeCall + Сплануй свою ідеальну втечу + Відкрити налаштування + Скасувати виклик + Спланувати виклик + Встановити власний час + Точний час + Таймер зворотного відліку + Ручне перевизначення + Quick preset + Вимкнути точні будильники + Увімкнути точні будильники для здійснення дзвінків + Надати дозвіл та увімкнути провайдера в Налаштуваннях. + Виклик заплановано + Готовий до планування + Спеціальний виклик + Countdown + Точний час + Зберегти як шаблон + Використати цього разу + Натисніть, щоб редагувати точний час + Оберіть точний час + ХВ + Сек + + Назад + Налаштування + Провайдер + Потрібні дозволи для телефону + Надати дозвіл щоб зареєструвати провайдера + Ім\'я провайдера + Відображається у обліковому записі дзвінків + Ім\'я провайдера + Save & register provider + Зробити цей акаунт доступним для вхідних дзвінків. + Увімкнути постачальник в системі + Постачальника увімкнено. + Відкрийте облікові записи для виклику аккаунту. + Audio + Виберіть аудіо файл + Current: %s + Використовувати типове аудіо + Disable custom audio playback. + Запис мікрофона + Ввімкнено + Виключено + Storage + Тека з записами + Save to: %s + Скинути теку записування + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Використовується зовнішніми намірами та ярликами спеціальних можливостей. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Користувацький режим використовує твої ключі управління каркасами. MP3 програвач + Користувацький режим + MP3-player IVR mode + MP3 player mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-v31/widget_colors.xml b/app/src/main/res/values-v31/widget_colors.xml new file mode 100644 index 0000000..1ee4878 --- /dev/null +++ b/app/src/main/res/values-v31/widget_colors.xml @@ -0,0 +1,11 @@ + + + #FF221519 + #FFFDE9E9 + #FFD7B7B7 + #40FFE0E0 + #26FFE0E0 + #33FF9C82 + #FFFF8E66 + #FF411400 + diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..aee8642 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,264 @@ + + + Fake Call + + Welcome to FakeCall + A premium fake call experience with expressive motion and instant scheduling. + Main features + Realistic incoming calls + Expressive call screens with smart scheduling. + Quick presets + exact time + Schedule instantly or pick a precise time. + Audio + IVR routing + Play custom audio and route with keypad tones. + Phone + Microphone permissions + Required to simulate calls and record audio. + Grant permissions + Make & manage phone calls + Enable FakeCall in system Calling Accounts. + Open calling accounts + Precise alarms & reminders + Needed for exact-time calls on Android 12+. + Enable precise alarms + Completed + Can\'t open Calling Accounts settings? + Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: + All set! You\'re ready to schedule your first fake call. + Finish setup + Finish setup (needs permissions) + + FakeCall + Schedule your perfect escape + Open settings + Cancel Call + Schedule Call + Set custom time + Exact time + Countdown timer + Manual override + Quick preset + Exact alarms are off. + Enable precise alarms to schedule exact-time calls. + Grant permissions and enable the provider in Settings. + Call scheduled + Ready to schedule + Custom Call + Countdown + Exact Time + Save as preset + Use this time + Tap to edit exact time + Pick exact time + MIN + SEC + + Back + Settings + Provider + Phone permissions required + Grant access to register the call provider. + Provider name + Shown in Calling Accounts + Provider name + Save & register provider + Make this account available for incoming calls. + Enable provider in system + Provider is enabled. + Open Calling Accounts to enable it. + Audio + Select audio file + Current: %s + Use default audio + Disable custom audio playback. + Microphone recording + Enabled + Disabled + Storage + Recording folder + Save to: %s + Reset recording folder + Use Downloads/FakeCall + Automation + Automation & Quick Trigger Defaults + Used by external intents and the accessibility shortcut. + Manage external triggers, accessibility shortcut and defaults in one place. + Quick Trigger Defaults + Automation API + Default caller name + Default caller number + Default delay + Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Open Accessibility Settings + Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. + Preset name (optional) + Save Current Defaults As Preset (%1$d/%2$d) + No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. + Preset %1$d: %2$s + Unknown Caller + Apply To Defaults + Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML + Load a saved IVR tree. + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML + Share your current IVR tree. + Add menu node + Create a new IVR menu. + No IVR nodes yet. + About & Updates + Current Version: %s + GitHub Repository + Checking\u2026 + Check for Updates + You are on the latest version. + GitHub rate limit reached. Try again later. + Couldn\'t check for updates right now. + Update Available + Version %s is available on GitHub Releases. + Update Now + Later + Node audio + No audio selected + Delete node + Select audio + Clear audio + Digit mappings (0 = back) + No mappings yet. + Set as root + Root menu + Remove mapping + Add mapping + New mailbox node + Node title + Create + Add mapping + Digit + Target node + Add another node to create mappings. + Add + + Caller Details + Name + number + Contact + Caller name + Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts + Shown on the incoming call screen. + Timing + Saved presets + Remove preset + Audio Preview + Tap to change or disable audio. + Stop + Play + Open settings + Clear audio + + Cancel + Remove + Apply + + Default + Fake Call Provider + Menu + Unknown Caller + Selected folder + Selected audio + + Fake Call Scheduler + Call recording + Recording call + Recording is active + Fake call scheduled + Fake call: %s + Incoming call in %1$d seconds from %2$s + Unknown + + Preset %d + FakeCall Preset 1 + FakeCall Preset 2 + FakeCall Preset 3 + FakeCall Preset 4 + FakeCall Preset 5 + Not configured + Triggering Fake Call now\u2026 + Fake Call scheduled in %d seconds + Preset %d is not configured + Could not run preset %d + Fake Call couldn\'t be scheduled + + Update %s available! + Download + Dismiss update notice + + Unknown + v + Now + %1$s → %2$s + + Grant phone permissions to continue. + Grant phone permissions first. + Quick trigger preset saved. + You can only save up to 5 quick trigger presets. + Enter a caller number before saving a preset. + Preset not found. + Preset applied to quick trigger defaults. + Quick trigger preset removed. + Preset already saved. + Custom preset saved. + Preset removed. + Selected audio file: %s + Disabling audio output on Call. + Call recording enabled. + Call recording disabled. + Mailbox exported. + Export failed. + Mailbox imported. + Import failed. + Recording folder set to: %s + Recording folder reset to Downloads/FakeCall. + Provider saved. Verify it is enabled in Calling Accounts. + Could not register provider. Check phone permissions and try again. + Grant phone permissions before scheduling. + Enable this app in system Calling Accounts first. + Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. + Triggering incoming call now. + Could not trigger call. Enable provider in Calling Accounts. + Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Timer cancelled. + Call scheduled for %s. + Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3491e24..31772ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,15 @@ Precise alarms & reminders Needed for exact-time calls on Android 12+. Enable precise alarms + Battery optimization + background startup + Recommended on aggressive ROMs so calls still ring with screen off. + Disable system battery optimization + Open ROM background/autostart settings + HyperOS/Xiaomi: allow Autostart and set Battery saver to No restrictions for FakeCall. + OxygenOS/OnePlus: allow Auto launch + Background activity and remove FakeCall from app battery optimization. + ColorOS/realme UI: allow Auto launch/Startup and disable background freezing or deep optimization for FakeCall. + Samsung One UI: set FakeCall to Unrestricted battery and allow background activity. + If calls fail with screen off, set FakeCall battery mode to Unrestricted and allow background activity/autostart. Completed Can\'t open Calling Accounts settings? Some OnePlus/Oppo/Realme/Samsung builds hide the screen. You can open it directly via ADB: @@ -65,14 +74,43 @@ Provider name Save & register provider Make this account available for incoming calls. + Use SIM provider name + Pick the stable SIM card name from a real SIM on this phone. Enable provider in system Provider is enabled. Open Calling Accounts to enable it. Audio + Call behavior Select audio file Current: %s Use default audio Disable custom audio playback. + Answer audio + Silent after answer + Audio file: %s + Custom keymap IVR + MP3 folder IVR: %s + Silent + Nothing plays after the call is answered. + Audio file + Current file: %s + Choose a file to use this mode. + Custom keymap (IVR) + Use your manual keypad routing and node audio. + MP3 folder IVR + Current folder: %s + Choose a folder to use this mode. + This mode avoids conflicts by ignoring selected files and IVR sources. + Choose file + Choose folder + Configure keymap + Call ring timeout + How long normal fake calls should ring before ending when unanswered. + Normal fake call + Alarm ring timeout + How long alarm-mode calls should ring before ending when unanswered. + Alarm-mode call + Unlimited (keep ringing) Microphone recording Enabled Disabled @@ -81,16 +119,17 @@ Save to: %s Reset recording folder Use Downloads/FakeCall - Automation - Automation & Quick Trigger Defaults - Used by external intents and the accessibility shortcut. - Manage external triggers, accessibility shortcut and defaults in one place. + Custom presets and automation + Custom presets and automation + Used by Quick Settings tiles, home screen shortcuts, widget, accessibility shortcut, and external API intents. + Manage presets, shortcuts, widget behavior, accessibility trigger, and API defaults in one place. Quick Trigger Defaults Automation API Default caller name Default caller number Default delay Quick triggers reuse the audio configured in the Audio section above, including your selected file or default playback behavior. + Quick triggers use the Audio section by default. You can enable a custom audio file per saved preset below. Open Accessibility Settings Enable the FakeCall accessibility service there to use the system accessibility button or shortcut for instant scheduling. Preset name (optional) @@ -98,18 +137,48 @@ No quick trigger presets yet. Save one to expose it as launcher app action and Quick Settings tile. Preset %1$d: %2$s Unknown Caller + Use custom audio for this preset + Preset audio: %1$s + Default for accessibility shortcut + Set as accessibility default + Accessibility default Apply To Defaults - Presets appear as app actions on launcher long-press and as Quick Settings tiles (Preset 1-5). + Presets are used for launcher shortcuts, Quick Settings tiles, widget actions, accessibility default behavior, and API fallback values. Action: `com.upnp.fakeCall.TRIGGER` with extras `caller_name`, `caller_number`, and `delay`. - Mailbox (IVR) - Manage IVR nodes, mappings, and import/export without cluttering main settings. - Import mailbox XML + Favorite Contact + Timing + Schedule Call + Open FakeCall + No starred contacts yet. Star contacts in FakeCall to use this widget. + No favorites + Mark contacts with a star in the app. + Scheduled call cancelled. + Step 1/3 • Pick Contact + Step 2/3 • Pick Timing + Step 3/3 • Confirm Action + %1$s • %2$s + Quick Presets + No quick presets yet. + Custom keybinds (IVR) + Configure IVR behavior with either custom node mappings or MP3 folder navigation. + Import IVR XML Load a saved IVR tree. - Export mailbox XML + IVR mode + Custom mode uses your manual key mappings. MP3 player mode auto-maps keys from the selected folder and announces options with TTS. + Custom mode + MP3 player mode + MP3-player IVR mode + Auto-map keys from a selected folder and use TTS to announce entries. + Select MP3 IVR folder + Current folder: %s + Clear MP3 IVR folder + Disable folder-based IVR navigation source. + No folder selected + Export IVR XML Share your current IVR tree. Add menu node Create a new IVR menu. - No mailbox nodes yet. + No IVR nodes yet. About & Updates Current Version: %s GitHub Repository @@ -145,23 +214,57 @@ Caller Details + Name + number + Contact Caller name Caller number + Select contact + Selected + Selected contact + Pinned contacts + Recent contacts Shown on the incoming call screen. Timing Saved presets Remove preset - Audio Preview + Answer audio Tap to change or disable audio. + MP3 IVR folder + Custom IVR + IVR + fallback audio + Selected audio + No answer audio + Selected audio + Silent after answer + When the call is answered, the folder menu is announced and keypad choices choose folders or sounds. + MP3 IVR is active, but no folder is selected yet. The answered call will ask you to select a folder. + The IVR root node audio plays first. Keypad choices can switch to other IVR nodes. + Custom IVR mode is active, but the root node has no audio yet. Keypad routes can still play node audio after answer. + Custom IVR routing is active, but the root node has no audio. The selected fallback audio plays first. + Custom IVR routing is active, but the root node and fallback audio are empty. + This selected audio plays when the call is answered. + Audio file mode is active, but no file is selected. Choose a file in settings. + No audio, TTS, or IVR menu will play after the call is answered. Stop Play Open settings Clear audio + Clear folder + Change audio + Choose audio + Configure IVR + Clear fallback Cancel Remove Apply + Use a SIM provider name? + Choose which stable SIM card name should be shown as the FakeCall provider. + No stable SIM card name was found. Check phone permissions or enter a provider name manually. + SIM slot %1$d + Use this provider + Keep current Default @@ -177,6 +280,9 @@ Call recording Recording call Recording is active + Call recording + Recording is enabled for answered calls. + Recording is off. Fake call scheduled Fake call: %s Incoming call in %1$d seconds from %2$s @@ -216,6 +322,11 @@ Preset not found. Preset applied to quick trigger defaults. Quick trigger preset removed. + Preset now uses a custom audio file. + Preset now uses the default audio settings. + Preset audio selected: %1$s + Preset audio removed. + Preset %1$d is now the accessibility default. Preset already saved. Custom preset saved. Preset removed. @@ -234,10 +345,85 @@ Grant phone permissions before scheduling. Enable this app in system Calling Accounts first. Enter a caller number before scheduling. + Select a contact before scheduling. + Could not read this contact. Please select another one. Triggering incoming call now. Could not trigger call. Enable provider in Calling Accounts. Enable exact alarms to schedule precise calls. + Select an MP3 IVR folder to use this mode. + Choose an audio file to use this mode. + Answer audio set to silent. + Answer audio set to the selected file. + Answer audio set to custom IVR. + Answer audio set to MP3 folder IVR. Timer cancelled. Call scheduled for %s. Timer started for %s. + The selected folder has no audio files or subfolders. + %1$s menu. + Page %1$d of %2$d. + Press %1$d for folder %2$s. + Press %1$d for audio %2$s. + Press pound for the next page. + Press star for the previous page. + Press 0 to go back to the previous folder. + No next page available. + No previous page available. + You are already at the root folder. + Invalid selection. + Could not play this audio file. + + + Call + Alarm + Alarm + Call-based alarms and reminders + New alarm + No alarms yet + Create your first alarm-style fake call. + Create alarm + Compact setup with repeat, voice, and snooze + Edit alarm + Fine-tune schedule, voice, and snooze behavior + Delete alarm? + Remove the alarm at %1$s? This cannot be undone. + Caller name / number + Schedule + Message + Snooze + Speaker default + Repeat days + One time + Exact clock time alarm + Pick alarm time + App voice (TTS) + Custom voice + TTS message + Repeat app voice message + Keep repeating the spoken message until the call ends. + Hey, this is your reminder call. + %1$s is calling you now. + No custom audio selected + Select audio file + Record audio + Allow snooze + Snooze by rejecting the ringing call or pressing 1 on the dial pad. + %1$d min + Earpiece + Speaker + Save call + Delete + Edit alarm + Delete alarm + Alarm saved for %1$s (%2$s). + Alarm updated for %1$s (%2$s). + Alarm deleted. + Could not schedule this alarm. + Mon + Tue + Wed + Thu + Fri + Sat + Sun diff --git a/app/src/main/res/values/widget_colors.xml b/app/src/main/res/values/widget_colors.xml new file mode 100644 index 0000000..1ee4878 --- /dev/null +++ b/app/src/main/res/values/widget_colors.xml @@ -0,0 +1,11 @@ + + + #FF221519 + #FFFDE9E9 + #FFD7B7B7 + #40FFE0E0 + #26FFE0E0 + #33FF9C82 + #FFFF8E66 + #FF411400 + diff --git a/app/src/main/res/values/widget_dimens.xml b/app/src/main/res/values/widget_dimens.xml new file mode 100644 index 0000000..f9a283a --- /dev/null +++ b/app/src/main/res/values/widget_dimens.xml @@ -0,0 +1,5 @@ + + + 56dp + 36dp + diff --git a/badge.json b/badge.json new file mode 100644 index 0000000..c0bed5a --- /dev/null +++ b/badge.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "translation", + "message": "14%", + "color": "red" +} diff --git a/docs/AI_APP_GUIDE.md b/docs/AI_APP_GUIDE.md new file mode 100644 index 0000000..02c399d --- /dev/null +++ b/docs/AI_APP_GUIDE.md @@ -0,0 +1,1369 @@ +# FakeCall AI Implementation Guide + +This document describes the current implementation of the FakeCall Android app for future AI agents and contributors. It is based on the code in this repository at the time of writing and focuses on how the app behaves, how features are wired, where state is stored, and which areas require extra care. + +## 1. Purpose And Mental Model + +FakeCall is a native Android app that simulates incoming phone calls through Android's real Telecom framework. The central idea is not to draw a fake call screen inside the app. Instead, the app registers a `PhoneAccount` and asks `TelecomManager` to add a new incoming call. The user's normal phone UI then handles ringing, answer, reject, audio routing, and call history integration as much as Android and the default dialer allow. + +The core behavior pipeline is: + +1. A user or external trigger chooses caller details and timing. +2. The app verifies phone permissions and that its calling account is enabled. +3. For immediate calls, the app directly calls `TelecomManager.addNewIncomingCall`. +4. For delayed calls, the app schedules an exact `AlarmManager` broadcast. +5. The broadcast receiver registers/verifies the phone account and triggers the incoming call. +6. `FakeCallConnectionService` creates a `FakeConnection`. +7. `FakeConnection` controls ringing timeout, answer/reject/disconnect behavior, playback, IVR, alarm TTS, snooze, audio routing, and optional microphone recording. + +The app is a single-module Android project using Kotlin, Jetpack Compose, Material 3, AndroidX Navigation Compose, lifecycle ViewModel/state flows, Android Telecom, AlarmManager, Accessibility Service, Quick Settings tiles, dynamic launcher shortcuts, MediaPlayer, MediaRecorder, and TextToSpeech. + +## 2. Project Layout + +Top-level structure: + +```text +. +|-- app/ Main Android application module +|-- gradle/libs.versions.toml Version catalog +|-- build.gradle.kts Root Gradle plugin declarations +|-- settings.gradle.kts Includes :app +|-- README.md User-facing project overview +|-- metadata/ Store/listing metadata +|-- Screenshots/ Screenshots +|-- docs/ AI and developer documentation +``` + +Important source directories: + +```text +app/src/main/java/com/upnp/fakeCall/ +|-- MainActivity.kt +|-- FakeCallViewModel.kt +|-- TelecomHelper.kt +|-- FakeCallConnectionService.kt +|-- FakeConnection.kt +|-- FakeCallAlarmScheduler.kt +|-- FakeCallAlarmReceiver.kt +|-- FakeCallSchedulerService.kt +|-- QuickTriggerManager.kt +|-- QuickTriggerTileServices.kt +|-- QuickTriggerAccessibilityService.kt +|-- ShortcutTriggerActivity.kt +|-- ExternalTriggerReceiver.kt +|-- AlarmModeModels.kt +|-- AlarmModeRepository.kt +|-- AlarmModeScheduler.kt +|-- AlarmModeAlarmReceiver.kt +|-- CallRecordingForegroundService.kt +|-- BatterySetupNavigator.kt +|-- UpdateChecker.kt +|-- DelayFormatter.kt +|-- ivr/ +| |-- IvrModels.kt +| |-- IvrConfigStore.kt +| |-- IvrStateMachine.kt +|-- ui/ + |-- FakeCallApp.kt + |-- components/Components.kt + |-- screens/DashboardScreen.kt + |-- screens/SettingsScreen.kt + |-- screens/OnboardingScreen.kt + |-- screens/AlarmModeScreen.kt + |-- theme/ +``` + +Resources: + +```text +app/src/main/res/ +|-- values/strings.xml Main string resources and feature copy +|-- values-*/strings.xml Localized strings +|-- raw/fake_voice.mp3 Bundled audio file; current call playback uses stored URIs and does not reference R.raw directly +|-- xml/accessibility_service_config.xml +|-- xml/backup_rules.xml +|-- xml/data_extraction_rules.xml +|-- drawable/ Launcher icons, quick trigger icon, widget-style drawables +``` + +The code currently contains string resources and drawables for widget behavior, but there is no app widget provider class or manifest receiver in the current tree. + +## 3. Build Configuration + +Project: + +- Root project name: `Fakecall` +- Included module: `:app` +- Application id and namespace: `com.upnp.fakeCall` +- Minimum SDK: 24 +- Compile SDK: 36 +- Target SDK: 36 +- Version code: 24 +- Version name: `2.4` +- Java compatibility: 11 +- Compose enabled through the Kotlin Compose plugin +- Release minification is disabled + +Main dependencies: + +- Android Gradle Plugin `9.0.1` +- Kotlin Compose plugin `2.0.21` +- AndroidX Core KTX +- Lifecycle runtime, runtime compose, ViewModel KTX, ViewModel Compose +- Activity Compose +- Navigation Compose +- Compose BOM `2024.09.00` +- Compose UI, UI graphics, UI tooling +- Material 3 +- Material icons extended +- JUnit, AndroidX test, Espresso, Compose UI tests + +Test coverage is currently placeholder-only: + +- `ExampleUnitTest.kt` checks `2 + 2 == 4` +- `ExampleInstrumentedTest.kt` checks package name + +## 4. Android Manifest And Platform Surface + +Declared permissions: + +- `READ_PHONE_STATE` +- `READ_PHONE_NUMBERS` +- `READ_CONTACTS` +- `RECORD_AUDIO` +- `INTERNET` +- `MODIFY_AUDIO_SETTINGS` +- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` +- `FOREGROUND_SERVICE` +- `FOREGROUND_SERVICE_MICROPHONE` +- `SCHEDULE_EXACT_ALARM` +- `READ_EXTERNAL_STORAGE` with `maxSdkVersion=32` + +Declared components: + +- `MainActivity` + - Launcher activity. + - Also handles `android.service.quicksettings.action.QS_TILE_PREFERENCES` so Quick Settings preferences can open the app. + +- `ShortcutTriggerActivity` + - Exported, no-history, excluded from recents. + - Used by dynamic launcher shortcuts to execute quick trigger presets. + +- `FakeCallConnectionService` + - Exported and protected by `android.permission.BIND_TELECOM_CONNECTION_SERVICE`. + - Has `android.telecom.ConnectionService` intent filter. + - Creates `FakeConnection` for incoming Telecom calls. + +- `FakeCallSchedulerService` + - Foreground short service for coroutine-based countdown scheduling. + - The current ViewModel path uses exact alarms for scheduled calls; this service still exists and can schedule a delayed call while showing a notification. + +- `CallRecordingForegroundService` + - Foreground service with microphone type. + - Keeps a notification visible while `FakeConnection` records answered-call audio. + +- `QuickTriggerAccessibilityService` + - Exported and protected by `android.permission.BIND_ACCESSIBILITY_SERVICE`. + - Requests the accessibility button and uses it as a quick trigger entry point. + +- `QuickTriggerTile1Service` through `QuickTriggerTile5Service` + - Exported Quick Settings tile services. + - One tile slot per preset. + +- `FakeCallAlarmReceiver` + - Non-exported receiver for one-off/delayed fake calls. + +- `AlarmModeAlarmReceiver` + - Non-exported receiver for alarm-mode calls and snooze/repeat scheduling. + +- `ExternalTriggerReceiver` + - Exported receiver. + - Accepts `com.upnp.fakeCall.TRIGGER` and legacy `com.ddone.fakecall.TRIGGER`. + - Used by Tasker, MacroDroid, ADB, etc. + +## 5. Main Runtime Architecture + +### 5.1 Activity And Compose Entry + +`MainActivity` enables edge-to-edge system bars, determines whether the app should start in settings, and sets Compose content: + +- `FakecallTheme` +- `FakeCallApp(startInSettings = startInSettings)` + +`startInSettings` is true when the incoming intent action is either: + +- `com.upnp.fakeCall.action.OPEN_SETTINGS` +- `android.service.quicksettings.action.QS_TILE_PREFERENCES` + +### 5.2 Navigation + +`FakeCallApp` owns the `NavHost` and top-level route graph: + +- `onboarding` +- `dashboard` +- `alarm` +- `alarm_create` +- `alarm_edit/{alarmId}` +- `settings` + +Start destination: + +- Settings if launched from settings intent and onboarding is complete. +- Dashboard if onboarding is complete. +- Onboarding otherwise. + +Bottom mode navigation is visible only on: + +- `dashboard` +- `alarm` + +The mode bar switches between normal call mode and alarm mode. It is implemented as a custom Compose surface, not the standard `NavigationBar`. + +### 5.3 Permission Bootstrapping + +`FakeCallApp` requests these runtime permissions: + +- `READ_PHONE_STATE` +- `READ_PHONE_NUMBERS` +- `RECORD_AUDIO` + +`READ_CONTACTS` is declared in the manifest but is not included in the top-level `RequiredPermissions` array. Contact picking uses Android contact pick intents and contact resolver access; any future contact-related change should check whether and when `READ_CONTACTS` needs to be requested. + +On launch, `FakeCallApp`: + +1. Computes whether all required permissions are granted. +2. Calls `viewModel.onPermissionStateChanged(granted)`. +3. If not granted, launches the permission request. + +When permissions become available, the ViewModel registers or updates the Telecom phone account and refreshes provider status. + +## 6. ViewModel And App State + +`FakeCallViewModel` is the central state owner. It extends `AndroidViewModel` because it needs an application context for prefs, system services, resources, URI grants, and schedulers. + +The public state is: + +```kotlin +val uiState: StateFlow +``` + +`FakeCallUiState` includes: + +- Onboarding completion +- Provider name and provider enabled state +- Caller name/number +- Caller input mode: manual or contact +- Selected contact +- Pinned and recent contacts +- Delay and schedule kind +- Custom countdown/exact-time values +- Saved custom timing presets +- IVR config +- Selected audio URI/name +- Required permission status +- Timer running state and trigger timestamp +- Status message +- Recording enabled and recording folder label +- Quick trigger defaults +- Quick trigger presets +- Quick trigger default preset slot +- Normal call ring timeout +- Alarm ring timeout +- Alarm mode items +- MP3 IVR mode settings +- Startup update info + +On initialization the ViewModel: + +1. Loads most state from `SharedPreferences`. +2. Loads IVR XML through `IvrConfigStore`. +3. Loads quick trigger defaults and presets through `QuickTriggerManager`. +4. Loads alarm mode items through `AlarmModeRepository`. +5. Starts a coroutine loop every second to: + - sync timer running state + - refresh provider status if permissions are present + - sync alarm mode state +6. Starts a startup update check coroutine. +7. Refreshes launcher shortcuts. + +The ViewModel is deliberately broad. Most user actions from Compose screens call direct ViewModel methods rather than separate use-case classes. + +## 7. SharedPreferences Storage + +The app uses `SharedPreferences` heavily. There is no Room database or DataStore. + +Main prefs file: + +```text +fake_call_prefs +``` + +IVR prefs file: + +```text +fake_call_ivr +``` + +Important `fake_call_prefs` keys used across classes: + +```text +provider_name +caller_name +caller_number +caller_input_mode +selected_contact +pinned_contacts +recent_contacts +delay_seconds +schedule_kind +custom_countdown_minutes +custom_countdown_seconds +custom_exact_hour +custom_exact_minute +custom_presets +timer_ends_at +quick_trigger_active_preset_slot +audio_uri +audio_name +answer_audio_mode +recording_enabled +recordings_tree_uri +recordings_folder_name +mp3_ivr_mode_enabled +mp3_ivr_folder_uri +mp3_ivr_folder_name +onboarding_complete +quick_trigger_preset_name +call_ring_timeout_seconds +alarm_ring_timeout_seconds +quick_trigger_caller_name +quick_trigger_caller_number +quick_trigger_delay_seconds +quick_trigger_use_custom_audio +quick_trigger_custom_audio_uri +quick_trigger_custom_audio_name +quick_trigger_presets_v1 +quick_trigger_default_preset_slot +alarm_mode_items +``` + +Runtime override keys: + +```text +runtime_audio_override_enabled +runtime_audio_override_uri +runtime_audio_override_name +runtime_message_mode +runtime_tts_message +runtime_repeat_tts_message +runtime_speaker_default +runtime_snooze_enabled +runtime_snooze_minutes +runtime_snooze_alarm_id +runtime_snooze_caller_name +runtime_snooze_caller_number +runtime_snooze_provider_name +``` + +Runtime overrides are a critical cross-component handoff. Receivers write them immediately before calling Telecom. `FakeConnection` reads and clears them when it is constructed. Any new feature that relies on per-call metadata must be careful because these values are global and transient, not scoped by a call id. + +Important `fake_call_ivr` key: + +```text +ivr_config_xml +``` + +## 8. Telecom Integration + +### 8.1 Phone Account + +`TelecomHelper` wraps Telecom operations: + +- Builds a `PhoneAccountHandle` with: + - component: `FakeCallConnectionService` + - account id: `fake_call_provider_account` + +- Registers a `PhoneAccount` with: + - label from settings/default provider name + - `PhoneAccount.CAPABILITY_CALL_PROVIDER` + - supported URI scheme: `tel` + +- Checks enabled state: + - `telecomManager.getPhoneAccount(accountHandle())?.isEnabled == true` + +The account must be enabled by the user in Android Calling Accounts. Registering it is not enough. + +### 8.2 Triggering A Call + +`TelecomHelper.triggerIncomingCall(...)`: + +1. Reads the appropriate ring timeout from prefs: + - normal call: `call_ring_timeout_seconds`, default 45 + - alarm call: `alarm_ring_timeout_seconds`, default 0 (unlimited) +2. Builds incoming call extras: + - `extra_fake_caller_name` + - `extra_fake_caller_number` + - `extra_fake_call_source` + - `extra_ring_timeout_seconds` +3. Builds Telecom extras with: + - `TelecomManager.EXTRA_INCOMING_CALL_ADDRESS` + - `TelecomManager.EXTRA_INCOMING_CALL_EXTRAS` +4. Calls `telecomManager.addNewIncomingCall(accountHandle(), extras)`. + +### 8.3 Creating The Connection + +`FakeCallConnectionService.onCreateIncomingConnection(...)` extracts: + +- caller number from `request.address.schemeSpecificPart` or extras +- caller name from extras +- source from extras +- ring timeout from extras + +It returns: + +```kotlin +FakeConnection(context = this, callerName, callerNumber, ringTimeoutSeconds) +``` + +## 9. FakeConnection Behavior + +`FakeConnection` is the runtime representation of the fake call. It extends `android.telecom.Connection`. + +Initialization: + +- Sets the address to `tel:` +- Sets caller display name to caller name or number +- Adds mute capability +- Enables VoIP audio mode +- Transitions to initializing and ringing +- Starts a ring timeout if configured + +Answer: + +1. Cancels ring timeout. +2. Marks `wasAnswered = true`. +3. Calls `setActive()`. +4. Sets `AudioManager.MODE_IN_COMMUNICATION`. +5. Applies default route: + - earpiece by default + - speaker if alarm runtime override says speaker +6. Starts recording if enabled and permitted. +7. Starts voice playback. + +Reject: + +- Cancels ring timeout. +- Disconnects with `DisconnectCause.REJECTED`. +- If not answered and snooze is enabled, `disconnectWithCause` can trigger snooze. + +Disconnect: + +- Cancels timeout. +- Disconnects with `DisconnectCause.LOCAL`. + +Abort: + +- Cancels timeout. +- Disconnects with `DisconnectCause.CANCELED`. + +Audio route changes: + +- Maintains microphone state if recording. +- Keeps `MODE_IN_COMMUNICATION`. +- Applies Bluetooth, wired headset, speaker, or earpiece route using `AudioManager`. + +DTMF: + +- If digit `1` is pressed and snooze is enabled: + - schedule snooze + - disconnect locally +- Else MP3-folder IVR handles digits first if active. +- Else custom IVR state machine handles digits. +- Digit `0` in custom IVR returns to the IVR root. + +Disconnect cleanup: + +- Cancels ring timeout. +- Optionally schedules snooze if the call was not answered. +- Stops media playback. +- Shuts down TTS. +- Clears folder navigation stack. +- Stops recording and exports/deletes temp file. +- Resets audio mode to normal. +- Calls `setDisconnected(...)`. +- Calls `destroy()`. + +### 9.1 Ring Timeout + +Ring timeout seconds: + +- `0` means unlimited ringing. +- Positive values schedule a handler callback. +- If the user does not answer before timeout, the connection disconnects with `DisconnectCause.MISSED`. + +Normal calls and alarm calls use separate timeout prefs. + +### 9.2 Playback Priority + +When the call is answered, `startVoicePlayback()` chooses content in this order: + +1. Alarm runtime TTS override if `runtime_message_mode == tts`. +2. Per-call runtime custom audio override. +3. The normal-call answer audio mode stored in `answer_audio_mode`: + - `SILENT`: no playback. + - `AUDIO_FILE`: global selected audio URI from settings. + - `CUSTOM_IVR`: custom IVR root node audio and keypad routing. + - `MP3_IVR`: MP3-folder IVR menu and keypad navigation. + +Audio playback uses `MediaPlayer`, `USAGE_VOICE_COMMUNICATION`, and loops by default except MP3-folder item playback, which returns to the menu on completion. + +The dashboard answer-audio preview mirrors this mode directly. Keep `DashboardScreen.answerPlaybackPreview()` aligned with `FakeConnection.startVoicePlayback()` whenever the answer audio mode behavior changes. + +### 9.3 MP3 Folder IVR Mode + +MP3-folder IVR is enabled by the unified answer-audio mode and folder prefs: + +- `answer_audio_mode == MP3_IVR` +- `mp3_ivr_mode_enabled` +- `mp3_ivr_folder_uri` +- `mp3_ivr_folder_name` + +`mp3_ivr_mode_enabled` is retained for compatibility, but new UI should treat `answer_audio_mode` as the source of truth. + +Behavior: + +- The selected folder is treated as a navigable menu. +- Subfolders and audio files are listed. +- Directories sort before files, then by localized lowercase display name. +- Up to 9 items are announced per page. +- TTS announces menu choices. + +DTMF mapping: + +- `1` through `9`: open/play the corresponding item on the current page +- `#`: next page +- `*`: previous page +- `0`: back one folder, or announce root-folder message if already at root + +Audio file recognition: + +- MIME type starts with `audio/`, or +- extension is `.mp3`, `.wav`, `.m4a`, `.aac`, `.ogg`, or `.flac` + +### 9.4 Custom IVR Mode + +Custom IVR is stored as XML by `IvrConfigStore`. + +`IvrConfig`: + +- root node id +- map of node id to `IvrNode` + +`IvrNode`: + +- id +- title +- audio URI +- audio label +- route map from DTMF char to target node id + +`IvrStateMachine`: + +- Tracks `currentNodeId`. +- `currentNode()` returns the active node. +- `handleDtmf(digit)`: + - digit `0` moves to root + - other digits use current node routes + - returns the new node or null + +When the active IVR node has audio, `FakeConnection` switches playback to that node's audio. + +### 9.5 TTS + +TTS is used for: + +- Alarm app-voice messages +- MP3-folder IVR menu announcements and errors + +The engine is lazily initialized. A pending message is stored if TTS initialization is still in progress. TTS language is set to the default locale. On Android Lollipop and newer, TTS audio attributes are set to voice communication. + +### 9.6 Recording + +Recording starts only after answering a call and is disabled by default for new installs/unset prefs. + +Requirements: + +- `recording_enabled` preference must be true. +- `RECORD_AUDIO` permission must be granted. + +Runtime behavior: + +1. Start `CallRecordingForegroundService`. +2. Create a timestamped filename: `fake_call_yyyyMMdd_HHmmss.m4a`. +3. Record to a temp file in `cacheDir/recordings_tmp`. +4. Use `MediaRecorder`: + - source: `MIC` + - format: `MPEG_4` + - encoder: `AAC` + - bitrate: 256000 + - sample rate: 48000 + - channels: 1 +5. On stop, export the temp file. + +Recording destination priority: + +1. User-selected document tree URI from `recordings_tree_uri`. The stored tree URI must be converted with `DocumentsContract.getTreeDocumentId(...)` and `DocumentsContract.buildDocumentUriUsingTree(...)` before calling `DocumentsContract.createDocument(...)`. +2. `MediaStore.Downloads` relative path `Downloads/FakeCall` on Android Q+. +3. Internal app storage `filesDir/recordings`. + +If recording stop or export fails, the temp file and destination placeholder are cleaned up. + +## 10. Normal Call Scheduling + +Normal dashboard calls are managed primarily by `FakeCallViewModel.scheduleFakeCall()`. + +Validation before scheduling: + +- Required phone permissions must be present. +- Calling account must be enabled. +- Caller number must not be blank. +- If caller input mode is contact, a contact must be selected. +- For exact-time scheduling, exact alarms must be allowed. + +Schedule kinds: + +- `PRESET` + - Uses `selectedDelaySeconds`. +- `CUSTOM_COUNTDOWN` + - Uses custom minutes and seconds. +- `CUSTOM_EXACT` + - Computes the next occurrence of selected hour/minute. If the time has already passed today, schedules tomorrow. + +Immediate call path: + +1. Register/update phone account. +2. If account is enabled, call `TelecomHelper.triggerIncomingCall`. +3. Clear `timer_ends_at`. +4. Update status. + +Delayed call path: + +1. Compute `triggerAtMillis`. +2. Cancel any existing one-off fake call alarm. +3. Schedule exact alarm through `FakeCallAlarmScheduler`. +4. Save `timer_ends_at`. +5. Update UI state to running. + +Cancel path: + +1. Cancel `FakeCallAlarmScheduler`. +2. Cancel `FakeCallSchedulerService`. +3. Remove `timer_ends_at`. +4. Clear active quick trigger slot. +5. Refresh Quick Settings tiles. +6. Update status. + +`FakeCallSchedulerService` is an alternate foreground-service countdown path. It registers the account, waits using coroutine `delay`, triggers Telecom if enabled, and stops. Current quick trigger and ViewModel delayed paths use `FakeCallAlarmScheduler`, not this service. + +## 11. One-Off Alarm Receiver + +`FakeCallAlarmReceiver` fires for normal delayed calls. + +On receive: + +1. Reads optional runtime audio override extras. +2. Writes or clears runtime audio override prefs. +3. Removes `timer_ends_at`. +4. Sets `quick_trigger_active_preset_slot` to `-1`. +5. Refreshes Quick Settings tiles. +6. Reads caller name, caller number, provider name. +7. Returns early if caller number is blank. +8. Registers/updates phone account. +9. If enabled, triggers incoming call with source `CALL`. + +The receiver is non-exported; external apps trigger through `ExternalTriggerReceiver`, not this receiver. + +## 12. Quick Triggers + +Quick triggers are shared by: + +- Settings defaults +- Quick Settings tiles +- Launcher shortcuts +- Accessibility shortcut +- External broadcast API fallback values + +`QuickTriggerManager` is the central API. + +### 12.1 Defaults + +`QuickTriggerDefaults` stores: + +- caller name +- caller number +- delay seconds +- whether to override audio +- custom audio URI/name + +If quick-trigger caller name/number/delay are missing, defaults fall back to the main caller name/number/delay preferences. + +### 12.2 Presets + +Up to 5 `QuickTriggerPreset` entries are stored in JSON under `quick_trigger_presets_v1`. + +Each preset has: + +- id +- title +- caller name +- caller number +- delay seconds +- optional custom audio flag +- custom audio URI/name + +Slots are positional: preset at index 0 is slot 1, etc. Removing a preset shifts later slots down. Code updates active/default slot references after removal. + +### 12.3 Execution + +Execution starts from one of: + +- `executePreset(context, slot)` +- `executeFromInputs(context, callerName, callerNumber, delaySeconds, presetSlot)` +- `executeFromDefaults(context)` + +Request resolution: + +- Missing caller name/number/delay values fall back to quick trigger defaults. +- Provider name comes from prefs or default provider string. +- Blank resolved caller number fails. + +Execution behavior: + +- Delay `0` or nearly immediate: + - cancel existing fake call alarm + - write runtime audio override if present + - register/update phone account + - trigger Telecom if account enabled + - save caller fields and clear timer +- Positive delay: + - cancel existing fake call alarm + - clear current runtime audio override + - schedule exact alarm + - save caller fields and `timer_ends_at` + - save active preset slot if any + +Return value: + +- `IMMEDIATE` +- `SCHEDULED` +- `FAILED` + +### 12.4 Launcher Shortcuts + +On Android 7.1+ (`N_MR1`), presets become dynamic shortcuts: + +- Shortcut id: `quick_trigger_preset_` +- Activity: `ShortcutTriggerActivity` +- Action: `com.upnp.fakeCall.action.TRIGGER_PRESET` +- Extra: `preset_slot` + +Shortcut labels are shortened: + +- short label max 10 chars +- title max 30 chars when saved + +### 12.5 Quick Settings Tiles + +There are five tile service classes, all extending `BaseQuickTriggerTileService`. + +Tile states: + +- No preset in that slot: `Tile.STATE_UNAVAILABLE` +- Preset exists and is active scheduled slot: `Tile.STATE_ACTIVE` +- Preset exists but not active: `Tile.STATE_INACTIVE` + +Tile click behavior: + +- If locked, runs after unlock. +- Executes the preset. +- Shows toast for immediate, scheduled, or missing preset. +- Refreshes the tile. + +### 12.6 Accessibility Shortcut + +`QuickTriggerAccessibilityService` registers an accessibility button callback on Android O+. + +When clicked: + +1. Loads defaults and default preset slot. +2. Calls `QuickTriggerManager.executeFromDefaults`. +3. Shows a toast based on result. + +The service does not inspect accessibility events or window content. + +### 12.7 External Broadcast API + +`ExternalTriggerReceiver` is exported and accepts: + +- `com.upnp.fakeCall.TRIGGER` +- `com.ddone.fakecall.TRIGGER` + +Extras: + +- `caller_name` as optional string +- `caller_number` as optional string +- `delay` as optional int seconds + +Missing extras fall back to quick trigger defaults. Failure shows a toast. + +ADB example: + +```bash +adb shell am broadcast -a com.upnp.fakeCall.TRIGGER -p com.upnp.fakeCall --es caller_name "Boss" --es caller_number "+49123456789" --ei delay 30 +``` + +## 13. Alarm Mode + +Alarm mode is separate from normal dashboard calls. It schedules call-based alarms/reminders at exact clock times, optionally repeating on weekdays, playing TTS or custom audio, optionally repeating the TTS message until the call ends, using a speaker default, and allowing snooze. + +### 13.1 Data Model + +`AlarmModeItem`: + +- id +- caller name +- caller number +- hour +- minute +- repeat days +- message mode +- TTS message +- repeat TTS message flag +- custom audio URI/name +- snooze enabled +- snooze minutes +- speaker default +- enabled +- next trigger timestamp + +`AlarmModeDraft` mirrors editable fields used by the create/edit UI. + +Message modes: + +- `APP_VOICE_TTS` +- `CUSTOM_AUDIO` + +Speaker defaults: + +- `EARPIECE` +- `SPEAKER` + +Repeat days use `java.time.DayOfWeek.value`: + +- Monday = 1 +- Tuesday = 2 +- Wednesday = 3 +- Thursday = 4 +- Friday = 5 +- Saturday = 6 +- Sunday = 7 + +### 13.2 Persistence + +`AlarmModeRepository` stores all alarms as a JSON array in `fake_call_prefs` under `alarm_mode_items`. + +Parsing is defensive: + +- skips id `0` +- skips blank caller numbers +- coerces hour/minute and snooze range +- ignores invalid repeat days +- defaults invalid enum values + +Items are sorted by `hour * 60 + minute` when saved. + +### 13.3 Scheduling + +`AlarmModeScheduler` computes the next trigger: + +- No repeat days: + - schedule today at hour/minute if future + - otherwise tomorrow +- Repeat days: + - schedule the next date whose day-of-week is in the repeat set + - if today's time has passed, start checking from tomorrow + +Scheduling uses: + +- `AlarmManager.setExactAndAllowWhileIdle` on Android M+ +- `AlarmManager.setExact` below M +- exact alarm permission check on Android S+ + +Request code: + +```text +40000 + (abs(alarmId) % 1000000000) +``` + +### 13.4 Alarm Receiver + +`AlarmModeAlarmReceiver`: + +1. Reads the alarm id and caller number. Returns if invalid. +2. Reads provider name from prefs. +3. Reads alarm message mode, TTS, repeat-TTS, custom audio, snooze, and speaker settings from intent extras. +4. Writes runtime overrides for `FakeConnection`. +5. Registers/updates phone account. +6. If account is enabled, triggers incoming call with source `ALARM`. +7. Handles repeat behavior: + - no repeat days: disable the alarm, set next trigger to 0, cancel pending intent + - repeat days: compute/schedule next trigger and update repository + +Important: alarm-mode TTS/custom-audio/snooze settings are passed to the connection through global runtime override prefs. `FakeConnection.consumeRuntimeOverrides()` clears those keys as soon as the connection is created. + +### 13.5 Snooze + +Snooze can be triggered by: + +- Rejecting/missing a ringing alarm-mode call before it is answered +- Pressing DTMF `1` when snooze is enabled + +`FakeConnection.triggerSnooze()`: + +1. Guards against duplicate snooze. +2. Uses runtime override caller number and snooze settings. +3. Loads the original alarm item if `runtime_snooze_alarm_id` is non-zero. +4. Creates a one-time alarm copy with a new id and empty repeat days. +5. Schedules it for now plus snooze minutes. + +Snooze alarms are scheduled but are not inserted into `AlarmModeRepository` in `triggerSnooze()`. The receiver receives all required fields from the PendingIntent extras generated by `AlarmModeScheduler.scheduleSnooze`. + +## 14. UI Screens + +### 14.1 Onboarding + +`OnboardingScreen` guides initial setup: + +- Feature overview +- Phone and microphone permissions +- Calling account setup +- Exact alarm setup +- Battery optimization and OEM background/autostart setup + +Onboarding completion is stored in `onboarding_complete`. + +### 14.2 Dashboard + +`DashboardScreen` is the normal fake-call scheduling surface. + +It includes: + +- Main title and settings button +- Update/status surfaces +- Caller input card +- Schedule state card +- Timing controls +- Answer-audio preview that reflects the real answer-time playback priority +- Bottom action bar +- Custom call sheet +- Countdown and exact-time pickers +- Custom timing preset handling + +It calls ViewModel methods for all state changes and scheduling. + +### 14.3 Settings + +`SettingsScreen` is the main configuration hub. + +Feature areas include: + +- Provider setup, including optional provider-name selection from stable SIM card names. `FakeCallViewModel.loadSimProviderOptions()` prefers `TelephonyManager.createForSubscriptionId(...).simOperatorName`, then subscription display name, then carrier name, while filtering transient Wi-Fi-calling labels such as `WiFi`/`VoWiFi`. +- Calling account status/action +- Unified answer-audio mode selection for silent, audio file, custom IVR, or MP3-folder IVR +- Normal and alarm ring timeout +- Recording toggle and dashboard quick recording switch +- Recording folder selection/reset +- Automation and quick trigger defaults +- Quick trigger presets and per-preset audio +- Accessibility settings entry +- IVR custom mode and MP3-folder mode details inside the IVR/keymap submenu +- IVR import/export +- Add/delete/configure IVR nodes and routes +- About/update check + +### 14.4 Alarm Mode + +`AlarmOverviewScreen` lists alarms and allows: + +- add alarm +- edit alarm +- delete alarm +- enable/disable alarm + +`AlarmCreateScreen` handles both create and edit: + +- caller name/number +- exact clock time +- repeat weekdays +- message mode +- TTS message +- repeat TTS message toggle +- custom audio selection +- snooze settings +- speaker default + +## 15. Contacts + +Caller input can be manual or contact-based. + +Contact data model: + +- id +- display name +- phone number +- photo URI +- avatar base64 + +The ViewModel resolves contacts from a picked URI by trying: + +1. Phone projection on the URI. +2. Contact projection plus a lookup for primary phone number. + +It also tries to encode a 128x128 PNG avatar as Base64, using direct photo URI first and contact lookup photo second. + +Pinned and recent contacts are persisted as JSON arrays. Recent contacts are deduplicated and pruned: + +- If pinned contacts exist, only one recent item is retained. +- Otherwise up to three recent items are retained by `pruneRecentContacts`. + +Constants also define broader limits: + +- `MAX_RECENT_CONTACTS = 12` +- `MAX_PINNED_CONTACTS = 8` + +If changing contact behavior, inspect both UI pruning and ViewModel constants because not all declared limits are necessarily applied in the same place. + +## 16. Audio And URI Permissions + +The app stores persistent string versions of user-selected URIs. + +User-selected audio: + +- Stored as `audio_uri` and `audio_name`. +- Used as the fallback playback source for normal calls. + +Per-preset audio: + +- Stored inside quick trigger preset JSON. +- Copied into runtime override prefs before the call. + +Alarm custom audio: + +- Stored inside alarm JSON and alarm PendingIntent extras. +- Copied into runtime override prefs by `AlarmModeAlarmReceiver`. + +MP3 IVR folder: + +- Stored as `mp3_ivr_folder_uri` and `mp3_ivr_folder_name`. +- `FakeConnection` queries children through `DocumentsContract`. + +Recording folder: + +- Stored as `recordings_tree_uri` and `recordings_folder_name`. +- `FakeConnection` creates output documents with `DocumentsContract.createDocument` using the tree's root document URI, not the raw tree URI. + +Any code that accepts a URI should ensure the app takes persistable URI permissions when the URI comes from Storage Access Framework. The ViewModel currently has methods such as `onAudioFileSelected`, `onRecordingFolderSelected`, and `onMp3IvrFolderSelected` that are responsible for this handoff. + +## 17. Update Checking + +`UpdateChecker` calls: + +```text +https://api.github.com/repos/DDOneApps/FakeCall/releases/latest +``` + +It sets: + +- `Accept: application/vnd.github+json` +- `User-Agent: FakeCall-Android` + +Response handling: + +- 200: parse `tag_name` and `html_url` +- 403: rate limited +- other: unavailable + +Version comparison: + +- Strips leading `v` or `V` +- Splits on non-digits +- Compares numeric parts with missing parts treated as 0 + +`FakeCallApp` shows a top update banner when `startupUpdate` is present. + +## 18. Battery Optimization Helpers + +`BatterySetupNavigator` detects ROM families: + +- Xiaomi/Redmi/Poco/HyperOS +- OnePlus/OxygenOS +- Oppo/Realme/ColorOS +- Samsung/One UI +- Generic + +It can open: + +- Android battery optimization exemption request/settings +- OEM-specific autostart/background settings +- App details as fallback + +This is used in onboarding to help scheduled calls work while the screen is off or the app is backgrounded. + +## 19. Localization + +The project has many `values-*/strings.xml` resource folders and a Crowdin workflow. New user-visible strings should be added to `app/src/main/res/values/strings.xml`; if they need to avoid translation, mark `translatable="false"`. + +The default strings include newer alarm-mode and widget-related strings. Be careful to keep localized resource completeness in mind, especially when adding required strings referenced from code. + +## 20. Known Current Gaps And Implementation Notes + +These are not necessarily bugs, but they matter for future AI work: + +- The repo has placeholder tests only. Behavioral changes to scheduling, alarms, IVR, or recording should add real unit tests where possible. +- `SharedPreferences` keys are duplicated as private constants across several classes. Renaming a key in one place can silently break another feature. +- Runtime override prefs are global and transient. Concurrent calls or overlapping scheduled triggers could race because overrides are not scoped by call id. +- `FakeCallSchedulerService` still exists but current delayed scheduling paths use exact alarms. Confirm intended behavior before refactoring it away. +- Widget resources/strings exist, but no current app widget component is declared in the manifest. +- Contact access and `READ_CONTACTS` permission should be revisited before expanding contact features. +- Android 12+ exact alarm permission is essential for scheduled calls. Always preserve checks and user guidance. +- Telecom behavior varies by OEM, default dialer, and Android version. Test on physical devices when changing account registration or connection behavior. +- `RECORD_AUDIO` is requested at startup because recording and voice-call behavior depend on it. If making recording optional at permission time, test answered-call playback and recording startup thoroughly. +- Quick trigger preset slots are positional. Deleting or reordering presets affects launcher shortcuts and Quick Settings tile semantics. +- Alarm-mode snooze schedules a one-time PendingIntent but does not persist a visible alarm item. That appears intentional for transient snooze calls. +- `UpdateChecker` uses network calls with plain `HttpURLConnection`; failures are intentionally collapsed into `Unavailable`. + +## 21. Common Change Recipes + +### Add A New Setting + +1. Add a field to `FakeCallUiState` if the UI needs to observe it. +2. Add a prefs key in `FakeCallViewModel` or a more appropriate owner. +3. Load the value in initial state. +4. Add an `on...Change` method that writes prefs and updates state. +5. Add UI in `SettingsScreen`. +6. Add string resources. +7. If another runtime component needs the setting, either: + - read the stable pref directly, or + - pass a one-time runtime override if it is per-call. + +### Add Per-Call Metadata + +Prefer passing data through Telecom incoming call extras if it only affects connection creation. If the data needs to be consumed by `FakeConnection` but is not currently in `FakeCallConnectionService`, consider adding it to `TelecomHelper.triggerIncomingCall` extras and reading it in the service. + +Avoid adding more global runtime override prefs unless the metadata is truly transient and there is no cleaner Telecom extra path. + +### Add A Quick Trigger Source + +Use `QuickTriggerManager` rather than duplicating scheduling logic. + +Call one of: + +- `executePreset(context, slot)` +- `executeFromDefaults(context)` +- `executeFromInputs(context, callerName, callerNumber, delaySeconds)` + +Then map `QuickTriggerExecution` to the source-specific feedback UI. + +### Add A New Alarm Option + +1. Add the field to `AlarmModeItem` and `AlarmModeDraft`. +2. Update `AlarmModeRepository` JSON save/parse. +3. Update `AlarmModeScheduler.scheduleAt` extras. +4. Update `AlarmModeAlarmReceiver` extra parsing. +5. Decide how `FakeConnection` receives the setting: + - direct Telecom extra, preferred for connection-scoped data + - runtime override pref, consistent with current alarm settings +6. Update `AlarmCreateScreen` and `AlarmOverviewScreen` if visible. +7. Add strings and tests for scheduling/persistence. + +### Add Or Change IVR Behavior + +Custom IVR: + +- Update `IvrNode`/`IvrConfig` if data shape changes. +- Update XML serialization and parser. +- Update `IvrStateMachine`. +- Update settings UI import/export and node editor. +- Update `FakeConnection.onPlayDtmfTone`. + +MP3-folder IVR: + +- Update folder listing/filtering in `FakeConnection`. +- Update TTS strings. +- Preserve page navigation semantics unless intentionally changing the user contract. + +### Change Recording + +Touchpoints: + +- `FakeConnection.maybeStartMicRecording` +- `FakeConnection.createRecordingDestination` +- `FakeConnection.stopAndReleaseRecording` +- `CallRecordingForegroundService` +- Settings recording toggle/folder UI +- Dashboard quick recording toggle +- Manifest foreground service permissions + +Always test: + +- answer and hang up normally +- reject before answer +- recorder stop failure path +- Android Q+ MediaStore destination +- SAF selected folder destination +- no `RECORD_AUDIO` permission + +## 22. Suggested Tests To Add + +High-value unit tests: + +- `AlarmModeScheduler.computeNextTriggerAtMillis` + - one-time future today + - one-time past schedules tomorrow + - repeat day today before time + - repeat day today after time + - multi-day repeats + +- `AlarmModeRepository` + - JSON round trip + - invalid enum fallback + - invalid repeat day filtering + - blank caller number skipped + +- `QuickTriggerManager` + - defaults fallback + - preset save limit + - removing slots shifts active/default slots + - immediate vs scheduled execution branches with fakes or wrappers + +- `IvrConfigStore` + - XML round trip + - missing root falls back to first node + - route parsing + +- `IvrStateMachine` + - digit routing + - `0` returns to root + - unknown digit no-op + +- `DelayFormatter` + - zero, seconds-only, minutes-only, mixed minutes/seconds + +Instrumentation/manual tests: + +- Calling account registration and enabled-state flow +- Real incoming fake call UI through the default phone app +- Answer playback +- Reject/missed timeout +- Alarm TTS call +- Alarm custom audio call +- Snooze by reject and by DTMF `1` +- Quick Settings tile execution +- Launcher shortcut execution +- Accessibility button execution +- External ADB broadcast +- Recording export to Downloads and SAF folder +- MP3-folder IVR navigation + +## 23. Important Files By Responsibility + +```text +MainActivity.kt + Compose entry point and start-in-settings intent handling. + +ui/FakeCallApp.kt + Navigation graph, permission launcher, update banner, bottom mode switch. + +FakeCallViewModel.kt + Main app state, prefs loading/saving, scheduling, provider status, contact handling, + IVR config operations, quick trigger settings, alarm mode orchestration. + +TelecomHelper.kt + PhoneAccount registration/status and Telecom incoming call trigger. + +FakeCallConnectionService.kt + Converts Telecom incoming connection requests into FakeConnection instances. + +FakeConnection.kt + Call lifecycle, ringing timeout, audio playback, IVR, TTS, snooze, recording, + audio route handling, cleanup. + +FakeCallAlarmScheduler.kt / FakeCallAlarmReceiver.kt + One-off exact alarm scheduling and delayed normal fake call trigger. + +QuickTriggerManager.kt + Quick trigger defaults, presets, dynamic shortcuts, tile refresh, execution logic. + +QuickTriggerTileServices.kt + Five Quick Settings tile services backed by preset slots. + +QuickTriggerAccessibilityService.kt + Accessibility button as quick trigger. + +ShortcutTriggerActivity.kt + Dynamic launcher shortcut trampoline. + +ExternalTriggerReceiver.kt + Exported automation API receiver. + +AlarmModeModels.kt + Alarm-mode data models and enums. + +AlarmModeRepository.kt + Alarm-mode JSON persistence. + +AlarmModeScheduler.kt + Alarm-mode exact scheduling and next-trigger calculation. + +AlarmModeAlarmReceiver.kt + Alarm trigger handling, runtime override setup, repeat rescheduling. + +ivr/IvrConfigStore.kt + IVR XML persistence/import/export parser. + +ivr/IvrStateMachine.kt + Custom IVR DTMF state transitions. + +CallRecordingForegroundService.kt + Foreground notification while microphone recording is active. + +BatterySetupNavigator.kt + Battery optimization and OEM settings navigation helpers. + +UpdateChecker.kt + GitHub latest-release update check. + +DelayFormatter.kt + Locale-aware duration formatting. +``` + +## 24. Safe Development Guidance For AI Agents + +- Read `FakeCallViewModel.kt`, `FakeConnection.kt`, and `QuickTriggerManager.kt` before changing behavior. They contain most cross-feature contracts. +- Treat prefs keys as public internal API. Search all usages before renaming, deleting, or changing defaults. +- Prefer central helpers: + - use `TelecomHelper` for phone account and incoming calls + - use `QuickTriggerManager` for quick trigger execution + - use `AlarmModeScheduler` for alarm-mode scheduling + - use `IvrConfigStore` for IVR serialization +- Keep feature-specific behavior in the existing owner unless there is a strong reason to refactor. +- Do not remove manifest permissions without checking all platform entry points. +- Test exact alarms on Android 12+ behavior whenever scheduling changes. +- Test Telecom changes on a real device when possible. Emulators and OEM devices vary. +- Avoid making the in-app UI imitate the phone call screen. The defining behavior is integration with the real dialer via Telecom. +- Preserve user-selected URI permission handling when touching audio, folders, contacts, or recordings. +- Be conservative with foreground services because Android target SDK 36 restrictions may be strict. +- Be careful with exported components: + - `ExternalTriggerReceiver` is intentionally exported. + - Telecom, accessibility, and Quick Settings services are permission-protected platform integrations. + - Receivers for internal alarms are non-exported. diff --git a/metadata/en-US/changelogs/22.txt b/metadata/en-US/changelogs/22.txt new file mode 100644 index 0000000..0ba744f --- /dev/null +++ b/metadata/en-US/changelogs/22.txt @@ -0,0 +1,5 @@ +
    +
  • added contact support with favourites feature.
  • +
  • Improved IVR description
  • +
  • added "MP3-Player" IVR mode
  • +
diff --git a/metadata/en-US/changelogs/23.txt b/metadata/en-US/changelogs/23.txt new file mode 100644 index 0000000..cef5315 --- /dev/null +++ b/metadata/en-US/changelogs/23.txt @@ -0,0 +1,8 @@ +
    +
  • New Crowdin updates by @DDOneApps in #27 +
  • New Crowdin updates by @DDOneApps in #28 +
  • Add Fastlane metadata for F-droid support by @DDOneApps in #32 to hopefully close issue #1 in a few days +
  • Add battery optimization step to setup screen to close issue #29 (rom specific) +
  • added custom audio feature for shortcuts +
  • improved accessibility shortcut handeling +
diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt new file mode 100644 index 0000000..bb0634f --- /dev/null +++ b/metadata/en-US/full_description.txt @@ -0,0 +1,17 @@ +FakeCall schedules simulated incoming calls using Android's Telecom framework, so the call appears in the device's regular phone experience instead of a custom fake-call screen. + +Features: + +
    +
  • Native phone UI: incoming fake calls are routed through Android Telecom.
  • +
  • Scheduling: start a call after a delay or from saved quick-trigger presets.
  • +
  • Custom caller details: choose the displayed caller name, number, photo, ringtone behavior, and SIM/provider label.
  • +
  • Answer audio: keep the call silent, play one selected audio file, use a folder-based MP3 IVR, or configure custom keypad audio flows.
  • +
  • Alarm mode: create alarms that use fake-call behavior and optional spoken messages.
  • +
  • Call history: optional call-log integration makes simulated calls easier to review later.
  • +
  • Recording: optionally record microphone audio during a fake call.
  • +
  • Automation: trigger calls from compatible automation apps or ADB via broadcast intents.
  • +
  • Shortcuts: use app shortcuts, Quick Settings tiles, and the accessibility shortcut for faster triggering.
  • +
+ +The app is free software and is designed to work without accounts, ads, or analytics. diff --git a/metadata/en-US/images/icon.jpg b/metadata/en-US/images/icon.jpg new file mode 100644 index 0000000..52a2a3c Binary files /dev/null and b/metadata/en-US/images/icon.jpg differ diff --git a/metadata/en-US/images/phoneScreenshots/1.png b/metadata/en-US/images/phoneScreenshots/1.png new file mode 100644 index 0000000..6b4ad9b Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/1.png differ diff --git a/metadata/en-US/images/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png new file mode 100644 index 0000000..5970a6c Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/2.png differ diff --git a/metadata/en-US/images/phoneScreenshots/3.png b/metadata/en-US/images/phoneScreenshots/3.png new file mode 100644 index 0000000..9f3dda8 Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/3.png differ diff --git a/metadata/en-US/images/phoneScreenshots/4.png b/metadata/en-US/images/phoneScreenshots/4.png new file mode 100644 index 0000000..de43c41 Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/4.png differ diff --git a/metadata/en-US/short_description.txt b/metadata/en-US/short_description.txt new file mode 100644 index 0000000..266757e --- /dev/null +++ b/metadata/en-US/short_description.txt @@ -0,0 +1 @@ +Simulate incoming calls with Android's native phone UI diff --git a/metadata/en-US/title.txt b/metadata/en-US/title.txt new file mode 100644 index 0000000..a6295fc --- /dev/null +++ b/metadata/en-US/title.txt @@ -0,0 +1 @@ +FakeCall