├── .gitignore ├── LICENSE ├── README.md ├── aph-magisk-module ├── META-INF │ └── com │ │ └── google │ │ └── android │ │ ├── update-binary │ │ └── updater-script ├── common │ ├── post-fs-data.sh │ ├── service.sh │ └── system.prop ├── install.sh ├── module.prop └── system │ └── etc │ └── permissions │ └── privapp-permissions-com.nll.helper.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── appGallery │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ ├── galaxyStore │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ ├── magiskStore │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nll │ │ │ └── helper │ │ │ ├── App.kt │ │ │ ├── Constants.kt │ │ │ ├── IStoreConfig.kt │ │ │ ├── ServerContentProvider.kt │ │ │ ├── bridge │ │ │ ├── AccessibilityServiceBridge.kt │ │ │ └── RecorderBridge.kt │ │ │ ├── debug │ │ │ ├── DebugLogActivity.kt │ │ │ ├── DebugLogAttachmentProvider.java │ │ │ ├── DebugLogService.kt │ │ │ ├── DebugLogServiceCommand.kt │ │ │ ├── DebugLogServiceMessage.kt │ │ │ └── DebugNotification.kt │ │ │ ├── recorder │ │ │ ├── AndroidMediaAudioRecorder.kt │ │ │ ├── AudioRecordPermissionNotification.kt │ │ │ ├── CLog.kt │ │ │ ├── CacheFileProvider.kt │ │ │ ├── Encoder.kt │ │ │ ├── MediaCodecAudioEncoder2.kt │ │ │ ├── MediaCodecAudioRecorder2.kt │ │ │ ├── Recorder.kt │ │ │ ├── RecorderConfig.kt │ │ │ └── mediacodec │ │ │ │ ├── AsynchronousMediaCodecAdapter.java │ │ │ │ ├── AsynchronousMediaCodecBufferEnqueuer.java │ │ │ │ ├── Clock.java │ │ │ │ ├── ConditionVariable.java │ │ │ │ ├── HandlerWrapper.java │ │ │ │ ├── IntArrayQueue.java │ │ │ │ ├── MediaCodecAdapter.java │ │ │ │ ├── MediaCodecAsyncCallback.java │ │ │ │ ├── MediaCodecInputBufferEnqueuer.java │ │ │ │ ├── SynchronousMediaCodecAdapter.java │ │ │ │ ├── SynchronousMediaCodecBufferEnqueuer.java │ │ │ │ ├── SystemClock.java │ │ │ │ └── SystemHandlerWrapper.java │ │ │ ├── server │ │ │ ├── ClientContentProviderHelper.kt │ │ │ ├── ClientVersionData.kt │ │ │ ├── IAccessibilityServiceBridge.kt │ │ │ ├── IRecorderBridge.kt │ │ │ ├── IRemoteService.kt │ │ │ ├── IRemoteServiceListener.kt │ │ │ ├── RecorderError.kt │ │ │ ├── RemoteResponseCodes.kt │ │ │ ├── RemoteService.kt │ │ │ ├── RemoteServiceImpl.kt │ │ │ ├── ServerContentProviderHelper.kt │ │ │ ├── ServerRecorderListener.kt │ │ │ ├── ServerRecordingState.kt │ │ │ └── ServerVersionData.kt │ │ │ ├── support │ │ │ ├── AccessibilityCallRecordingService.kt │ │ │ ├── AccessibilityChangeObserver.kt │ │ │ └── AccessibilityChangeObserverInitiator.kt │ │ │ ├── ui │ │ │ ├── DialogTerms.kt │ │ │ ├── MainActivity.kt │ │ │ └── MainActivityViewModel.kt │ │ │ ├── update │ │ │ ├── HttpProvider.kt │ │ │ ├── UpdateChecker.kt │ │ │ ├── UpdateRequest.kt │ │ │ ├── UpdateRequestImpl.kt │ │ │ ├── UpdateResult.kt │ │ │ ├── contract │ │ │ │ └── IDownloadUrlOpener.kt │ │ │ └── version │ │ │ │ ├── LocalAppVersion.kt │ │ │ │ └── RemoteAppVersion.kt │ │ │ └── util │ │ │ ├── AppSettings.kt │ │ │ ├── AutoClearedValue.kt │ │ │ ├── Extentions.kt │ │ │ ├── LiveEvent.kt │ │ │ └── Util.kt │ └── res │ │ ├── drawable │ │ ├── baseline_cancel_24.xml │ │ ├── crash_log_discard.xml │ │ ├── crash_log_send.xml │ │ ├── ic_empty_icon.xml │ │ ├── ic_green_checked_24dp.xml │ │ ├── ic_helper_notification2.xml │ │ ├── ic_info_24dp.xml │ │ ├── ic_red_error_24dp.xml │ │ ├── ic_warning_24.xml │ │ └── notification_debug.xml │ │ ├── layout │ │ ├── activity_debug_log.xml │ │ ├── activity_main.xml │ │ ├── dialog_terms.xml │ │ └── row_debug_log.xml │ │ ├── menu │ │ └── main_activity_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_helper_launcher.xml │ │ └── ic_helper_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_helper_launcher.png │ │ ├── ic_helper_launcher_foreground.png │ │ └── ic_helper_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_helper_launcher.png │ │ ├── ic_helper_launcher_foreground.png │ │ └── ic_helper_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_helper_launcher.png │ │ ├── ic_helper_launcher_foreground.png │ │ └── ic_helper_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_helper_launcher.png │ │ ├── ic_helper_launcher_foreground.png │ │ └── ic_helper_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_helper_launcher.png │ │ ├── ic_helper_launcher_foreground.png │ │ └── ic_helper_launcher_round.png │ │ ├── values-af │ │ └── strings.xml │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-az │ │ └── strings.xml │ │ ├── values-bg │ │ └── strings.xml │ │ ├── values-bs │ │ └── strings.xml │ │ ├── values-ca │ │ └── strings.xml │ │ ├── values-cs │ │ └── strings.xml │ │ ├── values-da │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-el │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-et │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-gl │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-hr │ │ └── strings.xml │ │ ├── values-hu │ │ └── strings.xml │ │ ├── values-hy │ │ └── strings.xml │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-ko │ │ └── strings.xml │ │ ├── values-ku │ │ └── strings.xml │ │ ├── values-lt │ │ └── strings.xml │ │ ├── values-ms │ │ └── strings.xml │ │ ├── values-nb-rNO │ │ └── strings.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-no-rNO │ │ └── strings.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-pt-rPT │ │ └── strings.xml │ │ ├── values-ro │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-si │ │ └── strings.xml │ │ ├── values-sk │ │ └── strings.xml │ │ ├── values-sq │ │ └── strings.xml │ │ ├── values-sr │ │ └── strings.xml │ │ ├── values-sv │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-uz │ │ └── strings.xml │ │ ├── values-vi │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── app_helper_known_certs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ └── helper_call_recording_accessibility_service.xml │ ├── nllStore │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ ├── oppoAppMarket │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ ├── vivoAppStore │ └── java │ │ └── com │ │ └── nll │ │ └── helper │ │ ├── StoreConfigImpl.kt │ │ └── update │ │ └── DownloadUrlOpenerImpl.kt │ └── xiaomiGetApps │ └── java │ └── com │ └── nll │ └── helper │ ├── StoreConfigImpl.kt │ └── update │ └── DownloadUrlOpenerImpl.kt ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /buildSrc/build/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ACR Phone Helper (APH) 3 | Source code of ACR Phone Helper (APH). Published in the interest of transparency. 4 | 5 | APH is a companion app for ACR Phone. It records calls with the help of Accessibility API and shares them with ACR Phone. 6 | You need ACR Phone for APH to work and you need APH to record calls with ACR Phone on Android 10+. Without APH, call recordings on Android 10+ will have no sound. 7 | 8 | It feels like déjà vu! We had same problem couple of years before with ACR Call Recorder. At the time we thought creating a variant of ACR Call Recorder Called ACR Unchained would be a good idea but it confused many people. This time we are creating a companion app with a distinctive name. Hopefully people will understand the connection between the two apps. 9 | 10 | Reason for creating APH and download link can be found at [https://acr.app](https://acr.app) 11 | ACR Phone can be downloaded from [https://play.google.com/store/apps/details?id=com.nll.cb](https://play.google.com/store/apps/details?id=com.nll.cb) 12 | 13 | --- 14 | 15 | **Android 15 Update (24/April/2024)** 16 |   17 | 18 | As we have suspected before, Google might be preparing to put the final nail on the coffin of call recording and end the cat and mouse game for good. 19 |   20 | I have just read a [dooming article](https://www.androidauthority.com/android-15-enhanced-confirmation-mode-3436697/) about improvements to the “Enhanced Confirmation Mode” 21 | I can confirm that **there will be no call recording possibility with Accessibility Service on Android 15**, if Google releases improved “Enhanced Confirmation Mode”. 22 |   23 | 24 | Improvements to “Enhanced Confirmation Mode” will prevent enabling Accessibility Service of any app that is not installed by a “Trusted Store” essentially ending possibility of call recording on Android 15+ 25 | 26 | --- 27 | 28 | Note to other developers. 29 | - It is not possible to provide reproducible builds as APH needs to be signed with the same key ACR Phone is signed with. 30 | - APH put together in around a week. Do not expect trendy architectural implementations. 31 | - Code comments make references to main ACR Phone app which is not open source. Just ignore them. 32 | - Client implementation is not open source but you can probably guess how it works by looking at the code. 33 | - Never mind the spelling mistakes. 34 | - Might accept pull requests. 35 | --- 36 | Further details about Google's never-ending war against call recording 37 | - [Possibly end of call recording for good on Android 15+](https://www.androidauthority.com/android-15-enhanced-confirmation-mode-3436697/) 38 | - [https://nllapps.com/no](https://nllapps.com/no) 39 | - [https://nllapps.com/android9](https://nllapps.com/android9) (Related to legacy ACR Call Recorder) 40 | - [https://nllapps.com/android11](https://nllapps.com/android11) (Related to once in a lifetime opportunity that was missed) 41 | - [https://nllapps.com/apps/acr/google-denies-phone-number-accesss.htm](https://nllapps.com/apps/acr/google-denies-phone-number-accesss.htm) (Related to Google's block on Call Log Access) -------------------------------------------------------------------------------- /aph-magisk-module/META-INF/com/google/android/update-binary: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | 3 | ################# 4 | # Initialization 5 | ################# 6 | 7 | umask 022 8 | 9 | # echo before loading util_functions 10 | ui_print() { echo "$1"; } 11 | 12 | require_new_magisk() { 13 | ui_print "*******************************" 14 | ui_print " Please install Magisk v20.4+! " 15 | ui_print "*******************************" 16 | exit 1 17 | } 18 | 19 | ######################### 20 | # Load util_functions.sh 21 | ######################### 22 | 23 | OUTFD=$2 24 | ZIPFILE=$3 25 | 26 | mount /data 2>/dev/null 27 | 28 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk 29 | . /data/adb/magisk/util_functions.sh 30 | [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk 31 | 32 | install_module 33 | exit 0 34 | -------------------------------------------------------------------------------- /aph-magisk-module/META-INF/com/google/android/updater-script: -------------------------------------------------------------------------------- 1 | #MAGISK 2 | -------------------------------------------------------------------------------- /aph-magisk-module/common/post-fs-data.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Do NOT assume where your module will be located. 3 | # ALWAYS use $MODDIR if you need to know where this script 4 | # and module is placed. 5 | # This will make sure your module will still work 6 | # if Magisk change its mount point in the future 7 | MODDIR=${0%/*} 8 | 9 | # This script will be executed in post-fs-data mode 10 | -------------------------------------------------------------------------------- /aph-magisk-module/common/service.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | # Do NOT assume where your module will be located. 3 | # ALWAYS use $MODDIR if you need to know where this script 4 | # and module is placed. 5 | # This will make sure your module will still work 6 | # if Magisk change its mount point in the future 7 | MODDIR=${0%/*} 8 | 9 | # This script will be executed in late_start service mode 10 | -------------------------------------------------------------------------------- /aph-magisk-module/common/system.prop: -------------------------------------------------------------------------------- 1 | # This file will be read by resetprop 2 | # Example: Change dpi 3 | # ro.sf.lcd_density=320 4 | -------------------------------------------------------------------------------- /aph-magisk-module/module.prop: -------------------------------------------------------------------------------- 1 | id=nll-aph 2 | name=ACR Phone Helper (APH) 3 | version=6 4 | versionCode=6 5 | author=NLL 6 | description=Root version of ACR Phone Helper (APH) -------------------------------------------------------------------------------- /aph-magisk-module/system/etc/permissions/privapp-permissions-com.nll.helper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keepnames class com.google.android.material.** { *; } 24 | -keepnames class android.** { *; } 25 | -keepnames class androidx.** { *; } 26 | -keepnames class kotlinx.** { *; } 27 | 28 | -keepclassmembers class * implements android.os.Parcelable { 29 | static ** CREATOR; 30 | } 31 | 32 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 33 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 34 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 35 | -dontwarn org.conscrypt.Conscrypt$Version 36 | -dontwarn org.conscrypt.Conscrypt 37 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 38 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 39 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 40 | -dontwarn org.openjsse.net.ssl.OpenJSSE -------------------------------------------------------------------------------- /app/src/appGallery/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = true 32 | override fun canLinkToGooglePlayStore() = true 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-app-gallery.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = true 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/appGallery/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 13 | private const val logTag = "DownloadUrlOpenerImpl" 14 | override fun getOpenDownloadUrlIntent( 15 | context: Context, 16 | remoteAppVersion: RemoteAppVersion 17 | ): Intent { 18 | 19 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 20 | 21 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 22 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 23 | } 24 | } 25 | 26 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 27 | 28 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 29 | 30 | 31 | try { 32 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 33 | try { 34 | /** 35 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 36 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 37 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 38 | */ 39 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 40 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 41 | } 42 | context.startActivity(openIntent) 43 | } catch (e: Exception) { 44 | CLog.logPrintStackTrace(e) 45 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 46 | } 47 | } catch (e: Exception) { 48 | CLog.logPrintStackTrace(e) 49 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/galaxyStore/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = true 32 | override fun canLinkToGooglePlayStore()= false 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-galaxy-store.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = false 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/galaxyStore/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 13 | private const val logTag = "DownloadUrlOpenerImpl" 14 | override fun getOpenDownloadUrlIntent( 15 | context: Context, 16 | remoteAppVersion: RemoteAppVersion 17 | ): Intent { 18 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 19 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 20 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 21 | } 22 | } 23 | 24 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 25 | 26 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 27 | 28 | 29 | try { 30 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 31 | try { 32 | /** 33 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 34 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 35 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 36 | */ 37 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 38 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 39 | } 40 | context.startActivity(openIntent) 41 | } catch (e: Exception) { 42 | CLog.logPrintStackTrace(e) 43 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 44 | } 45 | } catch (e: Exception) { 46 | CLog.logPrintStackTrace(e) 47 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/magiskStore/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/magiskStore/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = true 32 | override fun canLinkToGooglePlayStore()= true 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-magisk-store.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = false 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/magiskStore/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | 13 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 14 | private const val logTag = "DownloadUrlOpenerImpl" 15 | override fun getOpenDownloadUrlIntent( 16 | context: Context, 17 | remoteAppVersion: RemoteAppVersion 18 | ): Intent { 19 | 20 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 21 | 22 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 23 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 24 | } 25 | } 26 | 27 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 28 | 29 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 30 | 31 | 32 | try { 33 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 34 | try { 35 | /** 36 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 37 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 38 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 39 | */ 40 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 41 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 42 | } 43 | context.startActivity(openIntent) 44 | } catch (e: Exception) { 45 | CLog.logPrintStackTrace(e) 46 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 47 | } 48 | } catch (e: Exception) { 49 | CLog.logPrintStackTrace(e) 50 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | object Constants { 4 | const val contactEmail = "cb@nllapps.com" 5 | 6 | 7 | const val foregroundNotificationId = 1 8 | const val updateNotificationId = 2 9 | const val permissionNotificationId = 3 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/IStoreConfig.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.Context 4 | 5 | interface IStoreConfig { 6 | fun getStoreContactEmail() = "cb@nllapps.com" 7 | fun openACRPhoneDownloadLink(context: Context, packageName: String): Boolean 8 | fun canLinkToWebSite(): Boolean 9 | fun canLinkToGooglePlayStore(): Boolean 10 | fun getUpdateCheckUrl(): String 11 | fun requiresProminentPrivacyPolicyDisplay(): Boolean 12 | fun getPrivacyPolicyUrl(): String 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/bridge/AccessibilityServiceBridge.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.bridge 2 | 3 | import android.content.Context 4 | import com.nll.helper.recorder.CLog 5 | import com.nll.helper.server.IAccessibilityServiceBridge 6 | import com.nll.helper.support.AccessibilityCallRecordingService 7 | 8 | 9 | class AccessibilityServiceBridge : IAccessibilityServiceBridge { 10 | private val logTag = "CR_AccessibilityServiceBridge" 11 | override fun isHelperServiceEnabled(context: Context): Boolean { 12 | val isHelperServiceEnabled = AccessibilityCallRecordingService.isHelperServiceEnabled(context) 13 | CLog.log(logTag, "isHelperServiceEnabled -> isHelperServiceEnabled: $isHelperServiceEnabled") 14 | if (isHelperServiceEnabled) { 15 | AccessibilityCallRecordingService.startHelperServiceIfIsNotRunning(context) 16 | } 17 | return isHelperServiceEnabled 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/debug/DebugLogServiceCommand.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.debug 2 | 3 | sealed class DebugLogServiceCommand { 4 | 5 | object Stop : DebugLogServiceCommand() 6 | object Save : DebugLogServiceCommand() 7 | object Clear : DebugLogServiceCommand() 8 | 9 | override fun toString(): String { 10 | return when (this) { 11 | is Stop -> "Stop" 12 | is Save -> "Save" 13 | is Clear -> "Clear" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/debug/DebugLogServiceMessage.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.debug 2 | 3 | import java.util.* 4 | 5 | sealed class DebugLogServiceMessage { 6 | override fun toString(): String { 7 | return when (this) { 8 | is Saved -> "Saved(success: $success, path: $path)" 9 | is Started -> "Started(currentLogs: ${currentLogs.size})" 10 | Stopped -> "Stopped" 11 | } 12 | } 13 | 14 | class Started(val currentLogs: LinkedList) : DebugLogServiceMessage() 15 | object Stopped : DebugLogServiceMessage() 16 | class Saved(val success: Boolean, val path: String?) : DebugLogServiceMessage() 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/debug/DebugNotification.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.debug 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import androidx.core.app.NotificationCompat 6 | import com.nll.helper.R 7 | import com.nll.helper.util.extGetThemeAttrColor 8 | import io.karn.notify.Notify 9 | import io.karn.notify.entities.Payload 10 | import com.google.android.material.R as MaterialResources 11 | 12 | 13 | object DebugNotification { 14 | private fun alertPayload(context: Context) = Payload.Alerts( 15 | channelKey = "cb-common-notifications", 16 | lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC, 17 | channelName = context.getString(R.string.debug_log), 18 | channelDescription = context.getString(R.string.debug_log), 19 | channelImportance = Notify.IMPORTANCE_LOW, 20 | showBadge = false 21 | 22 | ) 23 | 24 | fun getDebugEnabledNotification(context: Context, startIntent: PendingIntent): NotificationCompat.Builder { 25 | val alertPayload =alertPayload(context) 26 | return Notify.with(context) 27 | .meta { 28 | cancelOnClick = false 29 | sticky = true 30 | clickIntent = startIntent 31 | group = "debug-enabled" 32 | } 33 | .alerting(alertPayload.channelKey) { 34 | lockScreenVisibility = alertPayload.lockScreenVisibility 35 | channelName = alertPayload.channelName 36 | channelDescription = alertPayload.channelDescription 37 | channelImportance = alertPayload.channelImportance 38 | 39 | } 40 | .header { 41 | icon = R.drawable.notification_debug 42 | color = context.extGetThemeAttrColor(MaterialResources.attr.colorPrimary) 43 | showTimestamp = true 44 | } 45 | .content { 46 | title = context.getString(R.string.debug_log) 47 | 48 | } 49 | .asBuilder() 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/AudioRecordPermissionNotification.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Color 7 | import androidx.core.app.NotificationCompat 8 | import com.nll.helper.Constants 9 | import com.nll.helper.R 10 | import com.nll.helper.ui.MainActivity 11 | import io.karn.notify.Notify 12 | import io.karn.notify.entities.Payload 13 | 14 | object AudioRecordPermissionNotification { 15 | private const val logTag = "AudioRecordPermissionNotification" 16 | private fun getChannel(context: Context) = Payload.Alerts( 17 | channelKey = "helper_permission_notification", 18 | lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC, 19 | channelName = context.getString(R.string.permissions_title), 20 | channelDescription = context.getString(R.string.permissions_title), 21 | channelImportance = Notify.IMPORTANCE_HIGH, 22 | showBadge = false 23 | 24 | ) 25 | 26 | fun show(context: Context) { 27 | CLog.log(logTag, "showNeedsAudioRecordPermissionNotification()") 28 | val launchIntent = Intent(context, MainActivity::class.java) 29 | val pendingOpenIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) 30 | val alertPayload = getChannel(context) 31 | 32 | Notify.with(context) 33 | .alerting(alertPayload.channelKey) { 34 | lockScreenVisibility = alertPayload.lockScreenVisibility 35 | channelName = alertPayload.channelName 36 | channelDescription = alertPayload.channelDescription 37 | channelImportance = alertPayload.channelImportance 38 | } 39 | .header { 40 | icon = R.drawable.ic_warning_24 41 | color = Color.RED 42 | showTimestamp = true 43 | } 44 | .meta { 45 | group = "helper_permission_notification" 46 | sticky = true 47 | cancelOnClick = true 48 | clickIntent = pendingOpenIntent 49 | } 50 | .content { 51 | title = context.getString(R.string.audio_record_permission) 52 | text = context.getString(R.string.call_rec_permissions_message) 53 | }.show(Constants.permissionNotificationId) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/CLog.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import com.nll.helper.BuildConfig 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.flow.MutableSharedFlow 8 | import kotlinx.coroutines.flow.asSharedFlow 9 | import kotlinx.coroutines.launch 10 | import java.text.SimpleDateFormat 11 | import java.util.Locale 12 | 13 | object CLog { 14 | private const val logTag = "CLog" 15 | private val loggerDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ROOT) 16 | private val loggerScope: CoroutineScope by lazy { CoroutineScope(Dispatchers.Main + SupervisorJob()) } 17 | private var _debug = true//BuildConfig.DEBUG 18 | private val _observableLog = MutableSharedFlow() 19 | fun observableLog() = _observableLog.asSharedFlow() 20 | 21 | fun isDebug() = _debug 22 | fun enableDebug() { 23 | _debug = true 24 | } 25 | 26 | fun disableDebug() { 27 | _debug = false 28 | } 29 | 30 | 31 | fun logPrintStackTrace(e: Throwable) { 32 | //We do not want to print stack trace in to log if it is debug build. This would create douple printing of stack traces to logcat 33 | val shouldLog = isDebug() && !BuildConfig.DEBUG 34 | if (shouldLog) { 35 | log(logTag, e.stackTraceToString()) 36 | } 37 | e.printStackTrace() 38 | } 39 | 40 | fun log(extraTag: String, message: String) { 41 | android.util.Log.d("APH_$extraTag", message) 42 | loggerScope.launch { 43 | _observableLog.emit("[${loggerDateFormat.format(System.currentTimeMillis())}] [APH_$extraTag] => $message") 44 | } 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/CacheFileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import android.content.Context 4 | import java.io.File 5 | 6 | object CacheFileProvider { 7 | fun provideCacheFile(context: Context, fileName: String) = File(getExternalCacheDirectory(context), fileName) 8 | 9 | fun getExternalCacheDirectory(context: Context): File { 10 | /** 11 | * We used to use externalCacheDir but noticed that Android randomly deletes our cache files if recording time is long 12 | */ 13 | return File(context.getExternalFilesDir(null), "recordings").also { folder -> 14 | if (folder.exists().not()) { 15 | folder.mkdirs() 16 | } 17 | } 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/Encoder.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import android.os.Build 4 | 5 | enum class Encoder(val id: Int) { 6 | MediaCodec(1), AndroidMediaRecorder(2); 7 | 8 | override fun toString(): String { 9 | return when (this) { 10 | AndroidMediaRecorder -> "AndroidMediaRecorder" 11 | MediaCodec -> "MediaCodecRecorder" 12 | } 13 | } 14 | 15 | companion object { 16 | private const val logTag = "Encoder" 17 | private val map = values().associateBy(Encoder::id) 18 | fun fromIdOrDefault(id: Int): Encoder { 19 | return when (id) { 20 | 0 -> { 21 | if (forceAndroidMediaRecorder()) { 22 | CLog.log(logTag, "fromIdOrDefault -> forceAndroidMediaRecorder() is true. Returning AndroidMediaRecorder") 23 | 24 | AndroidMediaRecorder 25 | } else { 26 | getDefaultEncoder() 27 | } 28 | } 29 | else -> { 30 | val encoder = map[id] ?: getDefaultEncoder() 31 | CLog.log(logTag, "fromIdOrDefault -> User changed -> Returning $encoder") 32 | 33 | encoder 34 | } 35 | } 36 | } 37 | 38 | 39 | /** 40 | * LGE Has issues with MediaCodec if Earpieces used 41 | * OnePlus cannot seem to be able to record with mediacodec on 11+ 42 | */ 43 | private fun forceAndroidMediaRecorder() = isLGE() || (isOnePlus() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 44 | 45 | private fun isLGE() = Build.MANUFACTURER.equals("LGE", ignoreCase = true) 46 | 47 | private fun isOnePlus() = Build.MANUFACTURER.equals("ONEPLUS", ignoreCase = true) 48 | 49 | /** 50 | * Return AndroidMediaRecorder 51 | * We seem to get a lot ANRs with MediaCodec 52 | */ 53 | private fun getDefaultEncoder() = AndroidMediaRecorder.also { 54 | CLog.log(logTag, "getDefaultEncoder -> Android 10 and above. Returning AndroidMediaRecorder") 55 | } 56 | 57 | 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/Recorder.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import android.content.Context 4 | import com.nll.helper.server.ServerRecordingState 5 | 6 | interface Recorder { 7 | /** 8 | * So that we can deputising if recording will be silent. 9 | */ 10 | fun isSIPRecorder(): Boolean 11 | fun isHelperRecorder(): Boolean 12 | fun startRecording() 13 | fun stopRecording() 14 | fun pauseRecording() 15 | fun resumeRecording() 16 | fun getState(): ServerRecordingState 17 | fun getConfig(): RecorderConfig 18 | fun needsPermissions(context: Context): Array 19 | fun getRoughRecordingTimeInMillis(): Long 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/RecorderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.recorder 2 | 3 | import com.nll.helper.server.ServerRecorderListener 4 | import java.io.File 5 | 6 | data class RecorderConfig( 7 | val encoder: Int, 8 | val recordingFile: File, 9 | val audioChannels: Int, 10 | val encodingBitrate: Int, 11 | val audioSamplingRate: Int, 12 | val audioSource: Int, 13 | val mediaRecorderOutputFormat: Int, 14 | val mediaRecorderAudioEncoder: Int, 15 | val recordingGain: Int, 16 | val serverRecorderListener: ServerRecorderListener 17 | ) { 18 | companion object { 19 | fun fromPrimitives( 20 | encoder: Int, 21 | recordingFile: File, 22 | audioChannels: Int, 23 | encodingBitrate: Int, 24 | audioSamplingRate: Int, 25 | audioSource: Int, 26 | mediaRecorderOutputFormat: Int, 27 | mediaRecorderAudioEncoder: Int, 28 | recordingGain: Int, 29 | serverRecorderListener: ServerRecorderListener 30 | ) = RecorderConfig( 31 | encoder = encoder, 32 | recordingFile = recordingFile, 33 | audioChannels = audioChannels, 34 | encodingBitrate = encodingBitrate, 35 | audioSamplingRate = audioSamplingRate, 36 | audioSource = audioSource, 37 | mediaRecorderOutputFormat = mediaRecorderOutputFormat, 38 | mediaRecorderAudioEncoder = mediaRecorderAudioEncoder, 39 | recordingGain = recordingGain, 40 | serverRecorderListener = serverRecorderListener 41 | ) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/Clock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.nll.helper.recorder.mediacodec; 17 | 18 | import android.os.Handler; 19 | import android.os.Looper; 20 | 21 | import androidx.annotation.Nullable; 22 | 23 | /** 24 | * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The 25 | * {@link #DEFAULT} implementation must be used for all non-test cases. 26 | */ 27 | public interface Clock { 28 | 29 | /** 30 | * Default {@link Clock} to use for all non-test cases. 31 | */ 32 | Clock DEFAULT = new SystemClock(); 33 | 34 | /** 35 | * Returns the current time in milliseconds since the Unix Epoch. 36 | * 37 | * @see System#currentTimeMillis() 38 | */ 39 | long currentTimeMillis(); 40 | 41 | /** 42 | * @see android.os.SystemClock#elapsedRealtime() 43 | */ 44 | long elapsedRealtime(); 45 | 46 | /** 47 | * @see android.os.SystemClock#uptimeMillis() 48 | */ 49 | long uptimeMillis(); 50 | 51 | /** 52 | * @see android.os.SystemClock#sleep(long) 53 | */ 54 | void sleep(long sleepTimeMs); 55 | 56 | /** 57 | * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling 58 | * messages. 59 | * 60 | * @see Handler#Handler(Looper, Handler.Callback) 61 | */ 62 | HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/HandlerWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.nll.helper.recorder.mediacodec; 17 | 18 | import android.os.Handler; 19 | import android.os.Looper; 20 | import android.os.Message; 21 | 22 | import androidx.annotation.Nullable; 23 | 24 | /** 25 | * An interface to call through to a {@link Handler}. Instances must be created by calling {@link 26 | * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases. 27 | */ 28 | public interface HandlerWrapper { 29 | 30 | /** @see Handler#getLooper() */ 31 | Looper getLooper(); 32 | 33 | /** @see Handler#obtainMessage(int) */ 34 | Message obtainMessage(int what); 35 | 36 | /** @see Handler#obtainMessage(int, Object) */ 37 | Message obtainMessage(int what, @Nullable Object obj); 38 | 39 | /** @see Handler#obtainMessage(int, int, int) */ 40 | Message obtainMessage(int what, int arg1, int arg2); 41 | 42 | /** @see Handler#obtainMessage(int, int, int, Object) */ 43 | Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj); 44 | 45 | /** @see Handler#sendEmptyMessage(int) */ 46 | boolean sendEmptyMessage(int what); 47 | 48 | /** @see Handler#sendEmptyMessageAtTime(int, long) */ 49 | boolean sendEmptyMessageAtTime(int what, long uptimeMs); 50 | 51 | /** @see Handler#removeMessages(int) */ 52 | void removeMessages(int what); 53 | 54 | /** @see Handler#removeCallbacksAndMessages(Object) */ 55 | void removeCallbacksAndMessages(@Nullable Object token); 56 | 57 | /** @see Handler#post(Runnable) */ 58 | boolean post(Runnable runnable); 59 | 60 | /** @see Handler#postDelayed(Runnable, long) */ 61 | boolean postDelayed(Runnable runnable, long delayMs); 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/MediaCodecInputBufferEnqueuer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.nll.helper.recorder.mediacodec; 18 | 19 | 20 | /** Abstracts operations to enqueue input buffer on a {@link android.media.MediaCodec}. */ 21 | interface MediaCodecInputBufferEnqueuer { 22 | 23 | /** 24 | * Starts this instance. 25 | * 26 | *

Call this method after creating an instance. 27 | */ 28 | void start(); 29 | 30 | /** 31 | * Submits an input buffer for decoding. 32 | * 33 | * @see android.media.MediaCodec#queueInputBuffer 34 | */ 35 | void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags); 36 | 37 | 38 | /** Flushes the instance. */ 39 | void flush(); 40 | 41 | /** Shut down the instance. Make sure to call this method to release its internal resources. */ 42 | void shutdown(); 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/SynchronousMediaCodecAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.nll.helper.recorder.mediacodec; 18 | 19 | import android.media.MediaCodec; 20 | import android.media.MediaCrypto; 21 | import android.media.MediaFormat; 22 | import android.view.Surface; 23 | 24 | import androidx.annotation.Nullable; 25 | 26 | /** 27 | * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. 28 | */ 29 | /* package */ final class SynchronousMediaCodecAdapter implements com.nll.helper.recorder.mediacodec.MediaCodecAdapter { 30 | 31 | private final MediaCodec codec; 32 | 33 | public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { 34 | this.codec = mediaCodec; 35 | } 36 | 37 | @Override 38 | public void configure( 39 | @Nullable MediaFormat mediaFormat, 40 | @Nullable Surface surface, 41 | @Nullable MediaCrypto crypto, 42 | int flags) { 43 | codec.configure(mediaFormat, surface, crypto, flags); 44 | } 45 | 46 | @Override 47 | public void start() { 48 | codec.start(); 49 | } 50 | 51 | @Override 52 | public int dequeueInputBufferIndex() { 53 | return codec.dequeueInputBuffer(0); 54 | } 55 | 56 | @Override 57 | public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { 58 | return codec.dequeueOutputBuffer(bufferInfo, 0); 59 | } 60 | 61 | @Override 62 | public MediaFormat getOutputFormat() { 63 | return codec.getOutputFormat(); 64 | } 65 | 66 | @Override 67 | public void queueInputBuffer( 68 | int index, int offset, int size, long presentationTimeUs, int flags) { 69 | codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); 70 | } 71 | 72 | @Override 73 | public void flush() { 74 | codec.flush(); 75 | } 76 | 77 | @Override 78 | public void shutdown() {} 79 | 80 | @Override 81 | public MediaCodec getCodec() { 82 | return codec; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/SynchronousMediaCodecBufferEnqueuer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.nll.helper.recorder.mediacodec; 18 | 19 | import android.media.MediaCodec; 20 | 21 | /** 22 | * A {@link MediaCodecInputBufferEnqueuer} that forwards queueing methods directly to {@link 23 | * MediaCodec}. 24 | */ 25 | class SynchronousMediaCodecBufferEnqueuer implements com.nll.helper.recorder.mediacodec.MediaCodecInputBufferEnqueuer { 26 | private final MediaCodec codec; 27 | 28 | /** 29 | * Creates an instance that queues input buffers on the specified {@link MediaCodec}. 30 | * 31 | * @param codec The {@link MediaCodec} to submit input buffers to. 32 | */ 33 | SynchronousMediaCodecBufferEnqueuer(MediaCodec codec) { 34 | this.codec = codec; 35 | } 36 | 37 | @Override 38 | public void start() {} 39 | 40 | @Override 41 | public void queueInputBuffer( 42 | int index, int offset, int size, long presentationTimeUs, int flags) { 43 | codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); 44 | } 45 | 46 | @Override 47 | public void flush() {} 48 | 49 | @Override 50 | public void shutdown() {} 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/SystemClock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.nll.helper.recorder.mediacodec; 17 | 18 | import android.os.Handler; 19 | import android.os.Handler.Callback; 20 | import android.os.Looper; 21 | 22 | import androidx.annotation.Nullable; 23 | 24 | /** 25 | * The standard implementation of {@link Clock}, an instance of which is available via {@link 26 | * SystemClock#DEFAULT}. 27 | */ 28 | public class SystemClock implements com.nll.helper.recorder.mediacodec.Clock { 29 | 30 | protected SystemClock() {} 31 | 32 | @Override 33 | public long currentTimeMillis() { 34 | return System.currentTimeMillis(); 35 | } 36 | 37 | @Override 38 | public long elapsedRealtime() { 39 | return android.os.SystemClock.elapsedRealtime(); 40 | } 41 | 42 | @Override 43 | public long uptimeMillis() { 44 | return android.os.SystemClock.uptimeMillis(); 45 | } 46 | 47 | @Override 48 | public void sleep(long sleepTimeMs) { 49 | android.os.SystemClock.sleep(sleepTimeMs); 50 | } 51 | 52 | @Override 53 | public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { 54 | return new SystemHandlerWrapper(new Handler(looper, callback)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/recorder/mediacodec/SystemHandlerWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.nll.helper.recorder.mediacodec; 17 | 18 | import android.os.Looper; 19 | import android.os.Message; 20 | 21 | import androidx.annotation.Nullable; 22 | 23 | /** The standard implementation of {@link HandlerWrapper}. */ 24 | /* package */ final class SystemHandlerWrapper implements com.nll.helper.recorder.mediacodec.HandlerWrapper { 25 | 26 | private final android.os.Handler handler; 27 | 28 | public SystemHandlerWrapper(android.os.Handler handler) { 29 | this.handler = handler; 30 | } 31 | 32 | @Override 33 | public Looper getLooper() { 34 | return handler.getLooper(); 35 | } 36 | 37 | @Override 38 | public Message obtainMessage(int what) { 39 | return handler.obtainMessage(what); 40 | } 41 | 42 | @Override 43 | public Message obtainMessage(int what, @Nullable Object obj) { 44 | return handler.obtainMessage(what, obj); 45 | } 46 | 47 | @Override 48 | public Message obtainMessage(int what, int arg1, int arg2) { 49 | return handler.obtainMessage(what, arg1, arg2); 50 | } 51 | 52 | @Override 53 | public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) { 54 | return handler.obtainMessage(what, arg1, arg2, obj); 55 | } 56 | 57 | @Override 58 | public boolean sendEmptyMessage(int what) { 59 | return handler.sendEmptyMessage(what); 60 | } 61 | 62 | @Override 63 | public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { 64 | return handler.sendEmptyMessageAtTime(what, uptimeMs); 65 | } 66 | 67 | @Override 68 | public void removeMessages(int what) { 69 | handler.removeMessages(what); 70 | } 71 | 72 | @Override 73 | public void removeCallbacksAndMessages(@Nullable Object token) { 74 | handler.removeCallbacksAndMessages(token); 75 | } 76 | 77 | @Override 78 | public boolean post(Runnable runnable) { 79 | return handler.post(runnable); 80 | } 81 | 82 | @Override 83 | public boolean postDelayed(Runnable runnable, long delayMs) { 84 | return handler.postDelayed(runnable, delayMs); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/ClientContentProviderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.nll.helper.recorder.CLog 6 | 7 | 8 | /** 9 | * 10 | *These are commands/requests server (APH) sends to client (ACR Phone) and expects response 11 | * Client should implement a Content provider just like ServerContentProvider and respond to these commands/requests 12 | * 13 | */ 14 | object ClientContentProviderHelper { 15 | private const val logTag = "CR_ClientContentProviderHelper" 16 | 17 | //Ignore can be private! 18 | const val clientAuthority = "com.nll.helper.ClientContentProvider" //Same as client manifest 19 | 20 | //Ignore can be private! 21 | const val clientMethodAskToConnect = "client_ask_to_connect" 22 | 23 | //Ignore can be private! 24 | const val clientMethodGetClientVersionData = "client_version_data" 25 | 26 | //Ignore can be private! 27 | const val clientPackageName = "com.nll.cb" 28 | 29 | //Ignore can be private! 30 | fun askToClientToConnect(context: Context) { 31 | CLog.log(logTag, "askToClientToConnect()") 32 | 33 | /** 34 | * Make sure we handle crash as user may not have main app installed. 35 | * Client in response calls attempts to connect by creating instance of 36 | * IRemoteService_Proxy(context.applicationContext, IRemoteService.SERVICE_INTENT) 37 | * 38 | * Our client implementation uses coroutines, fo instantiating IRemoteService_Proxy also attempts to connect. 39 | * See https://github.com/josesamuel/remoter#kotlin-support-with-suspend-functions 40 | */ 41 | try { 42 | context.contentResolver.call(Uri.parse("content://$clientAuthority"), clientMethodAskToConnect, null, null) 43 | } catch (ignored: Exception) { 44 | } 45 | 46 | } 47 | 48 | //Ignore can be private! 49 | fun getClientVersionData(context: Context): ClientVersionData? { 50 | 51 | CLog.log(logTag, "getClientVersionData()") 52 | /** 53 | * Make sure we handle crash as user may not have helper installed 54 | */ 55 | val cmdResult = try { 56 | context.contentResolver.call(Uri.parse("content://${clientAuthority}"), clientMethodGetClientVersionData, null, null) 57 | } catch (e: Exception) { 58 | null 59 | } 60 | return if (cmdResult != null) { 61 | val clientVersionData = ClientVersionData.fromBundle(cmdResult) 62 | CLog.log(logTag, "getClientVersionData() -> clientVersionData: $clientVersionData") 63 | clientVersionData 64 | } else { 65 | CLog.log(logTag, "getClientVersionData() -> clientVersionData: null") 66 | null 67 | } 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/ClientVersionData.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.os.Bundle 4 | 5 | data class ClientVersionData(val clientVersion: Int){ 6 | fun toBundle() = Bundle().apply { 7 | 8 | putInt(ARG_CLIENT_VERSION, clientVersion) 9 | 10 | 11 | } 12 | companion object { 13 | private const val ARG_CLIENT_VERSION = "ARG_CLIENT_VERSION" 14 | fun fromBundle(bundle: Bundle?): ClientVersionData? { 15 | return bundle?.let { 16 | val clientVersion = it.getInt(ARG_CLIENT_VERSION, -1) 17 | ClientVersionData(clientVersion) 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/IAccessibilityServiceBridge.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * Implementation for server is provided. 7 | * Client should implement no-op version of this interface in the same package 8 | * 9 | * While it won't be used, due to nature of AIDL, server package has to be in client code. 10 | * We use bridge package to provide no-op version of this interface to client app. 11 | */ 12 | interface IAccessibilityServiceBridge { 13 | fun isHelperServiceEnabled(context: Context): Boolean 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/IRecorderBridge.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * Must be copy of IRemoteService 7 | * 8 | * Implementation for server is provided. 9 | * Client should implement no-op version of this interface in the same package 10 | * 11 | * While it won't be used, due to nature of AIDL, server package has to be in client code. 12 | * We use bridge package to provide no-op version of this interface to client app. 13 | */ 14 | interface IRecorderBridge { 15 | fun startRecording( 16 | context: Context, 17 | encoder: Int, 18 | recordingFile: String, 19 | audioChannels: Int, 20 | encodingBitrate: Int, 21 | audioSamplingRate: Int, 22 | audioSource: Int, 23 | mediaRecorderOutputFormat: Int, 24 | mediaRecorderAudioEncoder: Int, 25 | recordingGain: Int, 26 | serverRecorderListener: ServerRecorderListener 27 | ): Int 28 | 29 | 30 | fun stopRecording() 31 | fun pauseRecording() 32 | fun resumeRecording() 33 | 34 | fun showNeedsAudioRecordPermissionNotification(context: Context) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/IRemoteService.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.os.IBinder 4 | import remoter.annotations.Oneway 5 | import remoter.annotations.ParamIn 6 | import remoter.annotations.Remoter 7 | 8 | 9 | /** 10 | * 11 | * See https://github.com/josesamuel/remoter 12 | * 13 | */ 14 | @Remoter 15 | interface IRemoteService { 16 | suspend fun startRecording( encoder: Int, 17 | recordingFile: String, 18 | audioChannels: Int, 19 | encodingBitrate: Int, 20 | audioSamplingRate: Int, 21 | audioSource: Int, 22 | mediaRecorderOutputFormat: Int, 23 | mediaRecorderAudioEncoder: Int, 24 | recordingGain: Int) : Int 25 | suspend fun stopRecording() 26 | suspend fun pauseRecording() 27 | suspend fun resumeRecording() 28 | suspend fun registerClientProcessDeath(@ParamIn clientDeathListener: IBinder) 29 | @Oneway 30 | fun registerListener(listener: IRemoteServiceListener) 31 | @Oneway 32 | fun unRegisterListener(listener: IRemoteServiceListener) 33 | companion object { 34 | /**Intent to connect for this service. Will be used by the client.*/ 35 | const val SERVICE_INTENT = "com.nll.helper.RemoteService" 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/IRemoteServiceListener.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import remoter.annotations.Remoter 4 | 5 | 6 | 7 | /** 8 | * 9 | * See https://github.com/josesamuel/remoter 10 | * 11 | */ 12 | @Remoter 13 | interface IRemoteServiceListener { 14 | fun onRecordingStateChange(recordingState: Int) 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/RecorderError.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | enum class RecorderError { 4 | AudioRecordInUse, 5 | MediaCodecQueueInputBufferFailed, 6 | AudioRecordReadFailed, 7 | EmptyInputBuffer, 8 | MediaMuxerWriteFailed, 9 | MediaRecorderError, 10 | MediaCodecException, 11 | RemoteError 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/RemoteResponseCodes.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | object RemoteResponseCodes { 4 | const val HELPER_IS_NOT_RUNNING = -3 5 | //Ignore unused 6 | const val REMOTE_AIDL_ERROR = -2 7 | const val REMOTE_ERROR = -1 8 | const val REMOTE_OK = 0 9 | 10 | const val RECORDING_PAUSED = 1 11 | const val RECORDING_RECORDING= 2 12 | const val RECORDING_STOPPED= 3 13 | const val RECORDING_ERROR= 4 14 | 15 | 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/RemoteService.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.content.Intent 4 | import android.os.IBinder 5 | import androidx.lifecycle.LifecycleService 6 | import com.nll.helper.recorder.CLog 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlin.properties.Delegates 10 | 11 | /** 12 | * Wrapper for IRemoteService_Proxy 13 | */ 14 | class RemoteService : LifecycleService() { 15 | private val logTag = "CR_RemoteService (${Integer.toHexString(System.identityHashCode(this))})" 16 | 17 | /** 18 | * We need to make sure we always provide same instance in case client re connects. 19 | * Without it (although RemoteServiceImpl receives callback and stops recording at RemoteServiceImpl#registerClientProcessDeath) we may have a dangling instance of this class recording continuously! 20 | */ 21 | private val remoteServiceStub: IRemoteService_Stub by lazy { 22 | IRemoteService_Stub(RemoteServiceImpl(applicationContext)) 23 | } 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | CLog.log(logTag, "onCreate()") 28 | } 29 | 30 | override fun onDestroy() { 31 | super.onDestroy() 32 | CLog.log(logTag, "onDestroy()") 33 | } 34 | 35 | override fun onBind(intent: Intent): IBinder { 36 | super.onBind(intent) 37 | CLog.log(logTag, "onBind()") 38 | connectionCount++ 39 | return remoteServiceStub 40 | } 41 | 42 | override fun onUnbind(intent: Intent): Boolean { 43 | connectionCount-- 44 | CLog.log(logTag, "onUnbind()") 45 | return super.onUnbind(intent) 46 | } 47 | 48 | companion object { 49 | private val clientConnectionCount = MutableStateFlow(0) 50 | fun observeClientConnectionCount() = clientConnectionCount.asStateFlow() 51 | 52 | private var connectionCount by Delegates.observable(0) { _, oldValue, newValue -> 53 | clientConnectionCount.tryEmit(newValue) 54 | } 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/ServerRecorderListener.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | interface ServerRecorderListener { 4 | fun onRecordingStateChange(newState: ServerRecordingState) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/ServerRecordingState.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | sealed class ServerRecordingState { 4 | companion object { 5 | fun fromResponseCode(code: Int) = when (code) { 6 | RemoteResponseCodes.RECORDING_ERROR -> Error(RecorderError.RemoteError, Exception("Remote error")) 7 | RemoteResponseCodes.RECORDING_PAUSED -> Paused 8 | RemoteResponseCodes.RECORDING_RECORDING -> Recording 9 | RemoteResponseCodes.RECORDING_STOPPED -> Stopped 10 | else -> throw (java.lang.IllegalArgumentException("Unknown RemoteResponseCode ($code)")) 11 | 12 | } 13 | } 14 | 15 | fun asResponseCode() = when (this) { 16 | is Error -> RemoteResponseCodes.RECORDING_ERROR 17 | Paused -> RemoteResponseCodes.RECORDING_PAUSED 18 | Recording -> RemoteResponseCodes.RECORDING_RECORDING 19 | Stopped -> RemoteResponseCodes.RECORDING_STOPPED 20 | HelperIsNotRunning -> RemoteResponseCodes.HELPER_IS_NOT_RUNNING 21 | RemoteAIDLError -> RemoteResponseCodes.REMOTE_AIDL_ERROR 22 | } 23 | 24 | 25 | object Recording : ServerRecordingState() 26 | object Stopped : ServerRecordingState() 27 | object Paused : ServerRecordingState() 28 | object HelperIsNotRunning : ServerRecordingState() 29 | object RemoteAIDLError : ServerRecordingState() 30 | class Error(val recorderError: RecorderError, val exception: Exception) : ServerRecordingState() 31 | 32 | override fun toString(): String { 33 | return when (this) { 34 | Recording -> "Recording" 35 | Stopped -> "Stopped" 36 | Paused -> "Paused" 37 | HelperIsNotRunning -> "HelperIsNotRunning" 38 | RemoteAIDLError -> "RemoteAIDLError" 39 | is Error -> "Error ($exception)" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/server/ServerVersionData.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.server 2 | 3 | import android.os.Bundle 4 | 5 | data class ServerVersionData(val serverVersion: Int) { 6 | 7 | fun toBundle() = Bundle().apply { 8 | 9 | putInt(ARG_SERVER_VERSION, serverVersion) 10 | 11 | 12 | 13 | } 14 | 15 | companion object { 16 | private const val ARG_SERVER_VERSION = "ARG_SERVER_VERSION" 17 | fun fromBundle(bundle: Bundle?): ServerVersionData? { 18 | return bundle?.let { 19 | val serverVersion = it.getInt(ARG_SERVER_VERSION, -1) 20 | ServerVersionData(serverVersion) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/support/AccessibilityChangeObserver.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.support 2 | 3 | import android.content.Context 4 | import android.database.ContentObserver 5 | import android.provider.Settings 6 | import com.nll.helper.recorder.CLog 7 | 8 | object AccessibilityChangeObserver { 9 | private const val logTag = "CR_AccessibilityChangeObserver" 10 | fun registerAccessibilityServiceChangeContentObserver(context: Context) { 11 | CLog.log(logTag, "registerAccessibilityServiceChangeContentObserver()") 12 | 13 | context.contentResolver.registerContentObserver(Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES), false, object : ContentObserver(null) { 14 | override fun onChange(self: Boolean) { 15 | 16 | val isAccessibilityServiceEnabledNow = AccessibilityCallRecordingService.isHelperServiceEnabled(context) 17 | 18 | CLog.log(logTag, "onChange() -> self: $self, isAccessibilityServiceEnabledNow: $isAccessibilityServiceEnabledNow") 19 | 20 | if (!isAccessibilityServiceEnabledNow) { 21 | CLog.log(logTag, "onChange() -> Warn user that AccessibilityService is not enabled on CallRecordingSupportType that needs it") 22 | AccessibilityCallRecordingService.postEnableHelperServiceNotificationAndToast(context, false) 23 | } 24 | if(isAccessibilityServiceEnabledNow){ 25 | AccessibilityCallRecordingService.startHelperServiceIfIsNotRunning(context) 26 | } 27 | } 28 | }) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/support/AccessibilityChangeObserverInitiator.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.support 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import com.nll.helper.recorder.CLog 6 | 7 | 8 | /** 9 | * Used to observer Accessibility Service changes 10 | */ 11 | class AccessibilityChangeObserverInitiator : Initializer { 12 | private val logTag = "CR_AccessibilityChangeObserverInitiator" 13 | 14 | override fun create(context: Context) { 15 | CLog.log(logTag, "create()") 16 | AccessibilityChangeObserver.registerAccessibilityServiceChangeContentObserver(context) 17 | 18 | } 19 | 20 | override fun dependencies(): List>> { 21 | // No dependencies on other libraries. 22 | return emptyList() 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/ui/DialogTerms.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.ui 2 | 3 | import android.app.Dialog 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AlertDialog 9 | import androidx.fragment.app.DialogFragment 10 | import androidx.fragment.app.FragmentManager 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 12 | import com.nll.helper.R 13 | import com.nll.helper.StoreConfigImpl 14 | import com.nll.helper.databinding.DialogTermsBinding 15 | import com.nll.helper.recorder.CLog 16 | import com.nll.helper.util.autoCleared 17 | import com.nll.helper.util.extSetHTML 18 | 19 | class DialogTerms : DialogFragment() { 20 | private var listener: Listener? = null 21 | 22 | private var binding by autoCleared() 23 | 24 | 25 | companion object { 26 | private const val logTag = "DialogTerms" 27 | 28 | private const val fragmentTag = "terms-dialog" 29 | 30 | fun display(fragmentManager: FragmentManager, listener: Listener) { 31 | val fragment = DialogTerms().apply { 32 | this.listener = listener 33 | } 34 | fragment.show(fragmentManager, fragmentTag) 35 | } 36 | } 37 | 38 | fun interface Listener { 39 | fun onTermsChoice(accepted: Boolean) 40 | } 41 | 42 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 43 | binding = DialogTermsBinding.inflate(requireActivity().layoutInflater) 44 | val message = String.format(getString(R.string.privacy_policy_warning), StoreConfigImpl.getPrivacyPolicyUrl()) 45 | binding.termsAgreed.extSetHTML(true, message) { urlToOpen -> 46 | try { 47 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(urlToOpen))) 48 | } catch (e: Exception) { 49 | CLog.logPrintStackTrace(e) 50 | Toast.makeText(requireContext(), e.message, Toast.LENGTH_SHORT).show() 51 | } 52 | } 53 | 54 | 55 | val alertDialog = MaterialAlertDialogBuilder(requireContext(), theme) 56 | .setCancelable(false) 57 | .setView(binding.root) 58 | .setPositiveButton(R.string.agree) { _, _ -> 59 | listener?.onTermsChoice(binding.termsAgreed.isChecked) 60 | } 61 | .setNegativeButton(android.R.string.cancel) { _, _ -> 62 | listener?.onTermsChoice(false) 63 | } 64 | .create() 65 | 66 | 67 | binding.termsAgreed.setOnCheckedChangeListener { _, isChecked -> 68 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = isChecked 69 | } 70 | 71 | alertDialog.setOnShowListener { 72 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false 73 | } 74 | 75 | return alertDialog 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/ui/MainActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.ui 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.ViewModelProvider 9 | import androidx.lifecycle.viewModelScope 10 | import com.nll.helper.recorder.CLog 11 | import com.nll.helper.server.RemoteService 12 | import com.nll.helper.support.AccessibilityCallRecordingService 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | 16 | 17 | class MainActivityViewModel(app: Application) : AndroidViewModel(app) { 18 | private val logTag = "CR_MainActivityViewModel" 19 | 20 | private val _accessibilityServicesChanged = MutableLiveData() 21 | fun observeAccessibilityServicesChanges(): LiveData = _accessibilityServicesChanged 22 | 23 | private val _clientConnected = MutableLiveData() 24 | fun observeClientConnected(): LiveData = _clientConnected 25 | 26 | 27 | init { 28 | 29 | 30 | 31 | val isHelperServiceEnabledOnInit = AccessibilityCallRecordingService.isHelperServiceEnabled(app) 32 | CLog.log(logTag, "init() -> isHelperServiceEnabledOnInit: $isHelperServiceEnabledOnInit") 33 | 34 | _accessibilityServicesChanged.postValue(isHelperServiceEnabledOnInit) 35 | 36 | AccessibilityCallRecordingService.observeAccessibilityServicesChangesLiveData().observeForever { isEnabled -> 37 | CLog.log(logTag, "observeAccessibilityServicesChangesLiveData() -> isEnabled: $isEnabled") 38 | _accessibilityServicesChanged.postValue(isEnabled!!) 39 | } 40 | 41 | RemoteService.observeClientConnectionCount().onEach { connectionCount -> 42 | CLog.log(logTag, "observeClientConnectionCount() -> connectionCount: $connectionCount") 43 | _clientConnected.postValue(connectionCount > 0) 44 | }.launchIn(viewModelScope) 45 | } 46 | 47 | 48 | @Suppress("UNCHECKED_CAST") 49 | class Factory(private val application: Application) : ViewModelProvider.Factory { 50 | override fun create(modelClass: Class): T { 51 | return MainActivityViewModel(application) as T 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/HttpProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import com.nll.helper.BuildConfig 4 | import com.nll.helper.recorder.CLog 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import java.util.concurrent.TimeUnit 9 | 10 | object HttpProvider { 11 | private const val logTag = "HttpProvider" 12 | private const val connectionTimeoutMs: Long = 10000 13 | private const val readTimeoutMs: Long = 50000 14 | private const val writeTimeoutMs: Long = 50000 15 | private val client: OkHttpClient by lazy { 16 | val loggingInterceptor = HttpLoggingInterceptor { message -> 17 | if (CLog.isDebug()) { 18 | CLog.log(logTag, message) 19 | } 20 | }.apply { 21 | val logLevel = if (BuildConfig.DEBUG) { 22 | HttpLoggingInterceptor.Level.BODY 23 | } else { 24 | HttpLoggingInterceptor.Level.NONE 25 | } 26 | setLevel(logLevel) 27 | //Security 28 | //redactHeader("Authorization"); 29 | //redactHeader("Cookie"); 30 | } 31 | 32 | OkHttpClient().newBuilder() 33 | .addInterceptor(loggingInterceptor) 34 | .connectTimeout(connectionTimeoutMs, TimeUnit.MILLISECONDS) 35 | .readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS) 36 | .writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS).build() 37 | } 38 | 39 | 40 | fun provideOkHttpClient(): OkHttpClient { 41 | return client 42 | } 43 | 44 | fun provideRequestForOwnServer(url: String): Request.Builder { 45 | return Request.Builder() 46 | .header("User-Agent", "ACR Phone") // Do not change. Keeps these as server requests validated according to these 47 | .header("Accept", "*/*") 48 | .url(url) 49 | 50 | 51 | } 52 | 53 | fun provideRequest(url: String): Request.Builder { 54 | return Request.Builder() 55 | .header("User-Agent", "ACR Phone") 56 | .header("Accept", "*/*") 57 | .url(url) 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/UpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import com.nll.helper.recorder.CLog 5 | import com.nll.helper.update.version.RemoteAppVersion 6 | import com.nll.helper.util.AppSettings 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.withContext 10 | 11 | object UpdateChecker { 12 | private const val logTag = "UpdateChecker" 13 | 14 | 15 | suspend fun checkUpdate(context: Context): UpdateResult = withContext(Dispatchers.IO) { 16 | //Init settings 17 | AppSettings.initIfNeeded(context) 18 | val updateRequest = UpdateRequestImpl() 19 | 20 | if (updateRequest.shouldCheckUpdate()) { 21 | if (CLog.isDebug()) { 22 | CLog.log(logTag, "checkUpdate() -> Check updates from server after delay and return up to date result") 23 | } 24 | delay(updateRequest.getUpdateCheckDelayInMillis()) 25 | realDownloadUpdate(updateRequest) 26 | } else { 27 | if (CLog.isDebug()) { 28 | CLog.log(logTag, "checkUpdate() -> No update check needed. Return previous update result") 29 | } 30 | } 31 | 32 | updateRequest.getUpdateRequestResult() 33 | } 34 | 35 | 36 | private fun realDownloadUpdate(updateRequest: UpdateRequestImpl) { 37 | if (CLog.isDebug()) { 38 | CLog.log(logTag, "realDownloadUpdate") 39 | } 40 | try { 41 | HttpProvider.provideOkHttpClient().newCall(HttpProvider.provideRequestForOwnServer(updateRequest.getUpdateCheckUrl()).build()).execute().use { response -> 42 | if (response.isSuccessful) { 43 | if (CLog.isDebug()) { 44 | CLog.log(logTag, "realDownloadUpdate -> response.isSuccessful. Save last update check time") 45 | } 46 | updateRequest.saveLastUpdateCheckTime() 47 | 48 | val bodyString = response.body.string() 49 | if (CLog.isDebug()) { 50 | CLog.log(logTag, "realDownloadUpdate -> response.body: $bodyString") 51 | } 52 | 53 | try { 54 | val remoteVersion = RemoteAppVersion(bodyString) 55 | if (CLog.isDebug()) { 56 | CLog.log(logTag, "realDownloadUpdate success. Save remoteVersion: $remoteVersion") 57 | } 58 | updateRequest.saveRemoteVersion(bodyString) 59 | } catch (e: Exception) { 60 | if (CLog.isDebug()) { 61 | CLog.log(logTag, "realDownloadUpdate failed while parsing response. Will try again later") 62 | } 63 | CLog.logPrintStackTrace(e) 64 | } 65 | 66 | 67 | } 68 | } 69 | } catch (e: Exception) { 70 | CLog.logPrintStackTrace(e) 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/UpdateRequest.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import com.nll.helper.update.version.RemoteAppVersion 4 | 5 | 6 | interface UpdateRequest { 7 | fun getUpdateCheckUrl(): String 8 | fun saveRemoteVersion(remoteVersionJson: String) 9 | fun saveLastUpdateCheckTime() 10 | fun getUpdateRequestResult(): UpdateResult 11 | fun getRemoteVersionInfo(): RemoteAppVersion? 12 | fun shouldCheckUpdate(): Boolean 13 | //Some delay different then MessageRequest so that we do not bombard the server with 2 request on first install 14 | fun getUpdateCheckDelayInMillis() = 200L 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/UpdateResult.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import com.nll.helper.update.version.RemoteAppVersion 5 | 6 | 7 | sealed class UpdateResult { 8 | class Required(val remoteAppVersion: RemoteAppVersion, val forceUpdate: Boolean) : UpdateResult(){ 9 | fun openDownloadUrl(context: Context) { 10 | DownloadUrlOpenerImpl.openDownloadUrl(context, remoteAppVersion) 11 | } 12 | } 13 | 14 | /** 15 | * No update required 16 | */ 17 | object NotRequired : UpdateResult() 18 | 19 | override fun toString(): String { 20 | return when (this) { 21 | is NotRequired -> "NotRequired" 22 | is Required -> "Required(forceUpdate: $forceUpdate)" 23 | 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/contract/IDownloadUrlOpener.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update.contract 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import com.nll.helper.update.version.RemoteAppVersion 6 | 7 | interface IDownloadUrlOpener { 8 | fun getOpenDownloadUrlIntent(context: Context, remoteAppVersion: RemoteAppVersion): Intent 9 | fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/version/LocalAppVersion.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update.version 2 | 3 | import com.nll.helper.BuildConfig 4 | 5 | object LocalAppVersion { 6 | 7 | const val versionCode = BuildConfig.VERSION_CODE 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/update/version/RemoteAppVersion.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update.version 2 | 3 | import org.json.JSONObject 4 | /* 5 | Expected Json response format is 6 | { 7 | "versionCode": 1, 8 | "downloadUrl": "https://acr.app/aph.apk", 9 | "whatsNewMessage": "", 10 | "forceUpdate": false 11 | } 12 | 13 | */ 14 | class RemoteAppVersion(json: String) : JSONObject(json) { 15 | val versionCode = this.optInt("versionCode") 16 | val downloadUrl: String = this.optString("downloadUrl") 17 | val whatsNewMessage: String = this.optString("whatsNewMessage") 18 | val forceUpdate: Boolean = this.optBoolean("forceUpdate") 19 | 20 | override fun toString(): String { 21 | return "RemoteVersionInfo(versionCode=$versionCode, downloadUrl='$downloadUrl', whatsNewMessage='$whatsNewMessage', forceUpdate='$forceUpdate')" 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/util/AppSettings.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.util 2 | 3 | 4 | import android.content.Context 5 | import com.chibatching.kotpref.Kotpref 6 | import com.chibatching.kotpref.KotprefModel 7 | 8 | object AppSettings : KotprefModel() { 9 | fun initIfNeeded(context: Context) { 10 | if (!Kotpref.isInitialized) { 11 | Kotpref.init(context.applicationContext) 12 | } 13 | } 14 | 15 | //For mapping KotPref to sharedPref used in SettingsFragment 16 | //Mapping to preference fragments done in BasePreferenceCompatFragment 17 | override val kotprefName: String = "app-recorder" 18 | 19 | 20 | var remoteVersionJson: String by stringPref( 21 | key = "remoteVersionJson", 22 | default = "" 23 | ) 24 | var remoteVersionLastUpdateCheck: Long by longPref( 25 | key = "remoteVersionLastUpdateCheck", 26 | default = 0 27 | ) 28 | 29 | /** 30 | * 31 | * As per 32 | * https://developer.android.com/guide/components/activities/background-starts 33 | * https://developer.android.com/about/versions/oreo/background 34 | * 35 | * Since client connects to server with AIDL bound service, helper app is excluded from background restrictions 36 | * as long as client app is in the background. In our case, since our app is bound by system telecom service 37 | * we are safe 38 | * 39 | * We however still have give option to users to start foreground service just in case some devices behave badly 40 | * 41 | */ 42 | var actAsForegroundService: Boolean by booleanPref( 43 | key = "actAsForegroundService", 44 | default = false 45 | ) 46 | var privacyPolicyAccepted: Boolean by booleanPref( 47 | key = "privacyPolicyAccepted", 48 | default = false 49 | ) 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/util/AutoClearedValue.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.util 2 | 3 | /* 4 | * Copyright (C) 2018 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | import androidx.fragment.app.Fragment 21 | import androidx.lifecycle.DefaultLifecycleObserver 22 | import androidx.lifecycle.LifecycleOwner 23 | import androidx.lifecycle.Observer 24 | import kotlin.properties.ReadWriteProperty 25 | import kotlin.reflect.KProperty 26 | 27 | /** 28 | * Updated fix via https://medium.com/@Zhuinden/an-update-to-the-fragmentviewbindingdelegate-the-bug-weve-inherited-from-autoclearedvalue-7fc0a89fcae1 29 | * Pull request https://github.com/Zhuinden/architecture-components-samples/commit/5c8fb401a4c907089d201ab905366bb2546acb3e 30 | * 31 | * A lazy property that gets cleaned up when the fragment's view is destroyed. 32 | * 33 | * Accessing this variable while the fragment's view is destroyed will throw NPE. 34 | */ 35 | class AutoClearedValue(val fragment: Fragment) : ReadWriteProperty { 36 | private var _value: T? = null 37 | 38 | init { 39 | val viewLifecycleOwnerLiveDataObserver = Observer { 40 | val viewLifecycleOwner = it ?: return@Observer 41 | 42 | viewLifecycleOwner.lifecycle.addObserver(object: DefaultLifecycleObserver { 43 | override fun onDestroy(owner: LifecycleOwner) { 44 | _value = null 45 | } 46 | }) 47 | } 48 | 49 | fragment.lifecycle.addObserver(object: DefaultLifecycleObserver { 50 | override fun onCreate(owner: LifecycleOwner) { 51 | fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerLiveDataObserver) 52 | } 53 | 54 | override fun onDestroy(owner: LifecycleOwner) { 55 | fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerLiveDataObserver) 56 | } 57 | }) 58 | } 59 | 60 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T { 61 | return _value ?: throw IllegalStateException( 62 | "should never call auto-cleared-value get when it might not be available" 63 | ) 64 | } 65 | 66 | override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { 67 | _value = value 68 | } 69 | } 70 | 71 | /** 72 | * Creates an [AutoClearedValue] associated with this fragment. 73 | */ 74 | fun Fragment.autoCleared() = AutoClearedValue(this) -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/util/LiveEvent.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.util 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.collection.ArraySet 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.MediatorLiveData 7 | import androidx.lifecycle.Observer 8 | 9 | //https://github.com/hadilq/LiveEvent 10 | 11 | 12 | open class LiveEvent : MediatorLiveData() { 13 | object Event 14 | private val observers = ArraySet>() 15 | 16 | @MainThread 17 | override fun observe(owner: LifecycleOwner, observer: Observer) { 18 | observers.find { it.observer === observer }?.let { _ -> // existing 19 | return 20 | } 21 | val wrapper = ObserverWrapper(observer) 22 | observers.add(wrapper) 23 | super.observe(owner, wrapper) 24 | } 25 | 26 | @MainThread 27 | override fun observeForever(observer: Observer) { 28 | observers.find { it.observer === observer }?.let { _ -> // existing 29 | return 30 | } 31 | val wrapper = ObserverWrapper(observer) 32 | observers.add(wrapper) 33 | super.observeForever(wrapper) 34 | } 35 | 36 | @MainThread 37 | override fun removeObserver(observer: Observer) { 38 | if (observer is ObserverWrapper && observers.remove(observer)) { 39 | super.removeObserver(observer) 40 | return 41 | } 42 | val iterator = observers.iterator() 43 | while (iterator.hasNext()) { 44 | val wrapper = iterator.next() 45 | if (wrapper.observer == observer) { 46 | iterator.remove() 47 | super.removeObserver(wrapper) 48 | break 49 | } 50 | } 51 | } 52 | 53 | @MainThread 54 | override fun setValue(t: T?) { 55 | observers.forEach { it.newValue() } 56 | super.setValue(t) 57 | } 58 | 59 | private class ObserverWrapper(val observer: Observer) : Observer { 60 | 61 | private var pending = false 62 | 63 | override fun onChanged(value: T) { 64 | if (pending) { 65 | pending = false 66 | observer.onChanged(value) 67 | } 68 | } 69 | 70 | fun newValue() { 71 | pending = true 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nll/helper/util/Util.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.util 2 | 3 | 4 | import android.content.Context 5 | import com.nll.helper.recorder.CLog 6 | 7 | 8 | object Util { 9 | fun dpToPx(context: Context, dp: Float): Float { 10 | return dp * context.resources.displayMetrics.density 11 | } 12 | 13 | fun getVersionCode(context: Context): Long { 14 | return try { 15 | context.applicationContext.packageManager.getPackageInfo(context.applicationContext.packageName, 0).longVersionCode 16 | } catch (e: Exception) { 17 | CLog.logPrintStackTrace(e) 18 | 0L 19 | } 20 | } 21 | fun isAppInstalled(context: Context, packageName: String): Boolean { 22 | return try { 23 | context.applicationContext.packageManager.getPackageInfo(packageName, 0)!=null 24 | } catch (e: Exception) { 25 | CLog.logPrintStackTrace(e) 26 | false 27 | } 28 | } 29 | 30 | fun getVersionName(context: Context): String { 31 | return try { 32 | context.applicationContext.packageManager.getPackageInfo(context.applicationContext.packageName, 0).versionName ?: "Cannot get version name!" 33 | } catch (e: Exception) { 34 | CLog.logPrintStackTrace(e) 35 | "Cannot get version name!" 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_cancel_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/crash_log_discard.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/crash_log_send.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_green_checked_24dp.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_helper_notification2.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_red_error_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warning_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_debug.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_terms.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_debug_log.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_activity_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_helper_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_helper_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_helper_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-hdpi/ic_helper_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_helper_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-hdpi/ic_helper_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_helper_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-hdpi/ic_helper_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_helper_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-mdpi/ic_helper_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_helper_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-mdpi/ic_helper_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_helper_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-mdpi/ic_helper_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_helper_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xhdpi/ic_helper_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_helper_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xhdpi/ic_helper_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_helper_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xhdpi/ic_helper_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_helper_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxhdpi/ic_helper_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_helper_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxhdpi/ic_helper_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_helper_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxhdpi/ic_helper_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/app/src/main/res/mipmap-xxxhdpi/ic_helper_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-af/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH het omgeval 6 | Daar was \'n fout wat veroorsaak het dat APH omgeval en gestop het.\nHelp ons asseblief om dit reg te stel deur vir ons die fout-data te stuur, al wat jy moet doen is om op \'GOED\' te tik en hierdie verslag met e-pos te stuur. 7 | Al die toestemmings is nodig vir volle funksionaliteit 8 | Toestemmingsversoek 9 | Stuur 10 | Kanselleer 11 | Omval kennisgewing 12 | Waarskuwing! 13 | 14 | 15 | Nuwe weergawe gevind 16 | Kan nie enige toep kry om hierdie skakel oop te maak nie 17 | Kan nie hierdie skakel ontleed nie 18 | 19 | 20 | Skakel aan 21 | 22 | 23 | Tik op Laat Toe om die vereiste toestemmings vir oproepopname toe te laat 24 | 25 | 26 | Oproep Opnemer Hulp 27 | Hierdie diens word gebruik vir toegang tot oproepklank. Geen oproepe kan opgeneem word as dit af is nie. Om toegang tot jou data te beperk, beperk die toep waarnemings tot sy eie gebeurtenisse en versoek geen funksies nie\n\nSommige fone kan rapporteer dat die diens nie werk nie. Probeer om dit af en weer aan te skakel, as dit gebeur. 28 | Tik hier om oproep opnemer hulp aan te skakel 29 | Skakel asseblief Oproep Opnemer Hulp aan op die skerm vir geïnstalleerde toeganklikheidsdienste 30 | Oproep Opnemer Hulp is afgeskakel 31 | 32 | 33 | Laai af 34 | Kennisgewing 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | لقد تعطل APH 6 | حدث خطأ تسبب في تعطل APH وإيقافه.\nيُرجى مساعدتنا في إصلاح هذا العطل عن طريق إرسال بيانات الخطأ إلينا، كل ما عليك فعله هو النقر فوق \"موافق\" وإرسال هذا التقرير عبر البريد الإلكتروني. 7 | جميع الأذونات مطلوبة للوظائف الكاملة 8 | طلب إذن 9 | إرسال 10 | إلغاء 11 | إشعار الأعطال 12 | تحذير! 13 | 14 | 15 | تم العثور على إصدار جديد 16 | لا يمكن العثور على أي تطبيق لفتح هذا الرابط 17 | تعذر تحليل هذا الرابط 18 | 19 | 20 | ممكن 21 | 22 | 23 | اضغط على السماح لرؤية ومنح الأذونات المطلوبة لتسجيل المكالمات 24 | 25 | 26 | مساعد تسجيل المكالمات 27 | تُستخدم هذه الخدمة للوصول إلى صوت المكالمات. لا يمكن تسجيل صوت المكالمة عند تعطيلها. من أجل تقييد الوصول إلى بياناتك، يقيد التطبيق المراقبة على الأحداث الخاصة به ولا يطلب أي إمكانية قد تبلغ بعض الهواتف عن الخدمة على أنها لا تعمل. حاول تعطيله وتمكينه مرة أخرى إذا حدث ذلك. 28 | انقر هنا لتمكين مساعد تسجيل المكالمات 29 | يرجى تمكين مساعد تسجيل المكالمات في شاشة خدمات الوصول المثبتة 30 | لم يتم تمكين مساعد تسجيل المكالمات 31 | إشعار 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values-az/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH qəzaya uğrayıb 6 | APH-un qəzaya uğramasına və dayanmasına səbəb olan xəta oldu.\nLütfən səhv məlumatları göndərməklə onu düzəltməyə kömək edin, sadəcə \'OK\' vurun və bu hesabatı e-poçtla göndərin. 7 | Tam işləmə üçün bütün icazələr tələb olunur 8 | İcazə istəyi 9 | Göndər 10 | İmtina 11 | Qəza xəbərdarlığı 12 | Xəbərdarlıq! 13 | 14 | 15 | Yeni versiya tapıldı 16 | Bu linki açmaq üçün heç bir tətbiq tapılmadı 17 | Bu linki analiz etmək olmur 18 | 19 | 20 | Aktiv 21 | 22 | 23 | Zəng qeydləri üçün lazımi icazələri görmək və vermək üçün İcazə ver-ə toxunun 24 | 25 | 26 | Zəng qeydi köməkçisi 27 | Bu xidmət zəng səslərinə daxil olmaq üçün istifadə olunur. Bağlı olduqda heç bir səs qeydi edilə bilməz. Verilərinizə girişi məhdudlaşdırmaq üçün tətbiqetmə hadisələri ilə müşahidəçiliyi məhdudlaşdırır və hər hansı bir qabiliyyət tələb etmir\n\nBəzi telefonlar xidməti işləmədiyini bildirə bilər. Bu baş verərsə, onu söndürməyə və yenidən işə salmağa çalışın. 28 | Zəng Qeyd Köməkçisini aktiv etmək üçün bura toxunun 29 | Lütfən Yüklənmiş Əlçatan Xidmətləri ekranında Zəng Qeyd Köməkçisini aktiv edin 30 | Zəng Qeydi Köməkçisi aktiv edilməyib 31 | 32 | 33 | Endir 34 | 35 | 36 | ACR Telefonun Google Play Mağaza versiyası quraşdırılmayıb və ya yenilənməlidir. Bu proqram yalnız ACR Telefonunun uyğun versiyası ilə birlikdə istifadə edilə bilər 37 | Quraşdır 38 | ACR Telefonunun zəng yazma funksiyası üçün işlək vəziyyətdə saxlanılmalıdır 39 | Bu proqram, düzgün işləməsi üçün güncəllənməlidir 40 | Audio qeyd icazəsi 41 | Bildiriş 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-bg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Телефон се затвори 6 | Поради грешка APH се затвори. 7 | Моля да ни помогнете да поправим грешката, като ни изпратите допълнителна информация. 8 | Всичко, което трябва да направите е да изберете \'ОК\' и да ни изпратите протокол за грешката по email. 9 | Всички разрешения са необходими за пълна фунционалност. 10 | Заявка за разрешение 11 | Изпрати 12 | Отказ 13 | Нотификация за затваряне. 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values-bs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH se srušio 6 | Došlo je do greške zbog čega se APH srušio i zaustavio.\nMolimo vas da nam pomognete da to popravimo tako što ćete nam poslati podatke o pogrešci, sve što trebate je dodirnuti \'U redu\' i poslati ovaj izvještaj e-poštom. 7 | Sve dozvole su potrebne za punu funkcionalnost 8 | Zahtjev za dozvolu 9 | Poslati 10 | Otkaži 11 | Obavijest o padu aplikacije 12 | Upozorenje! 13 | 14 | 15 | Nova verzija pronađena 16 | Nije moguće pronaći aplikaciju za otvaranje ove URL veze 17 | Nije moguće raščlaniti ovu vezu 18 | 19 | 20 | Omogući 21 | 22 | 23 | Dodirnite Dozvoli da biste vidjeli i odobrili potrebne dozvole za snimanje poziva 24 | 25 | 26 | Pomoćnik za Snimanje Poziva 27 | Ova usluga se koristi za pristup zvuku poziva. Nijedan zvuk poziva ne može se snimiti kad je onemogućen. Da bi ograničili pristup vašim podacima, aplikacija ograničava promatranje na vlastite događaje i ne zahtijeva nikakve mogućnosti.\n\nNeki telefoni mogu prijaviti da usluga ne radi. Pokušajte ga onemogućiti i ponovo omogućiti ako se to dogodi. 28 | Dodirnite ovdje da omogućite Pomoćnika za Snimanje Poziva 29 | Molimo omogućite Pomoćnika za Snimanje Poziva na ekranu Instalirane Usluge Pristupačnosti 30 | Pomoćnik za Snimanje Poziva nije omogućen 31 | 32 | 33 | Preuzimanje 34 | Obavijest 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH havaroval 6 | Chyba způsobila pád a zastavení funkce APH.\nPomozte nám opravit tento problém tím, že nám pošlete údaje o chybách. Stačí kliknout na \"OK\" a odeslat tuto zprávu emailem. 7 | Všechna oprávnění jsou vyžadována pro plnou funkčnost 8 | Žádost o oprávnění 9 | Odeslat 10 | Zrušit 11 | Oznámení o pádu 12 | Upozornění! 13 | 14 | 15 | Byla nalezena nová verze 16 | Nelze najít žádnou aplikaci pro otevření tohoto odkazu 17 | Nelze analyzovat tento odkaz 18 | 19 | 20 | Povolit 21 | 22 | 23 | Klepnutím na Povolit zobrazení a udělení požadovaných oprávnění pro nahrávání hovorů 24 | 25 | 26 | Pomocník pro nahrávání hovorů 27 | Tato služba se používá pro přístup ke zvukovému hovoru. Pokud je zakázán, nelze zvuk volání zaznamenat. Pro omezení přístupu k vašim datům aplikace omezuje pozorování na své vlastní události a nevyžaduje žádnou funkci\n\nNěkteré telefony mohou hlásit, že služba nefunguje. Zkuste znovu zakázat a povolit, pokud se tak stane. 28 | Klepnutím sem povolíte pomocníka pro nahrávání hovorů 29 | Prosím povolte pomocníka pro nahrávání hovorů na obrazovce Služby usnadnění přístupu 30 | Pomocník pro nahrávání hovorů není povolen 31 | Oznámení 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values-da/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH er gået ned 6 | Der opstod en fejl, der forårsagede, at APH gik ned og stoppede.\nHjælp os med at ordne dette ved at sende os fejldata, alt hvad du skal gøre er at trykke på \'OK\' og sende denne rapport via e-mail. 7 | Alle tilladelser kræves for fuld funktionalitet 8 | Tilladelsesanmodning 9 | Send 10 | Annuller 11 | Meddelelse om nedbrud 12 | Advarsel! 13 | 14 | 15 | Ny version fundet 16 | Kan ikke finde nogen app til at åbne dette link 17 | Kan ikke fortolke dette link 18 | 19 | 20 | Aktiver 21 | 22 | 23 | Tryk på Tillad at se og give de nødvendige tilladelser til opkaldsoptagelse 24 | 25 | 26 | Hjælp til opkaldsoptager 27 | Denne tjeneste bruges til at få adgang til opkaldslyd. Ingen opkaldslyd kan optages, når den er deaktiveret. For at begrænse adgangen til dine data, begrænser app\'en observation til sine egne begivenheder og anmoder ikke om nogen kapacitet\n\nNogle telefoner kan rapportere tjeneste som ikke fungerer. Prøv at deaktivere og aktivere det igen, hvis dette sker. 28 | Tryk her for at aktivere Call Recording Helper 29 | Aktivér venligst Opkaldsoptagelseshjælper på skærmen Installerede Tilgængelighedstjenester 30 | Opkaldsoptagelseshjælper er ikke aktiveret 31 | 32 | 33 | Download 34 | 35 | 36 | Google Play Butik version ACR Phone er ikke installeret eller skal opdateres. Denne app kan kun bruges sammen med en kompatibel version af ACR Phone 37 | Installer 38 | Skal holdes kørende for opkald optagelse funktionalitet ACR Phone 39 | Denne app skal opdateres for at fungere korrekt 40 | Tilladelse til lydoptagelse 41 | Notifikation 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH ist abgestürzt 6 | Es gab einen Fehler, der APH zum Absturz brachte und stoppte.\nBitte hilf uns, solche Fehler zu beheben, indem du uns die Fehlerdaten schickst. Alles, was du machen musst ist, auf \'OK\' zu tippen und uns diesen Bericht per E-Mail zu schicken. Danke! 7 | Diese Berechtigungen sind für die volle Funktionalität erforderlich 8 | Berechtigungsanfrage 9 | Senden 10 | Abbrechen 11 | Absturzbenachrichtigung 12 | Achtung! 13 | 14 | 15 | Neue Version gefunden 16 | Keine App zum Öffnen dieses Links gefunden 17 | Link kann nicht verarbeitet werden 18 | 19 | 20 | Aktivieren 21 | 22 | 23 | Tippe auf Erlauben, um den erforderlichen Berechtigungen für die Anrufaufzeichnung zuzustimmen 24 | 25 | 26 | Anrufeaufzeichnungsdienst 27 | Dieser Dienst wird zum Aufzeichnen von Anrufen verwendet. Wenn der Dienst deaktiviert ist, können keine Anrufe aufgezeichnet werden. Der Dienst bezieht sich nur auf die App und greift nicht auf andere Daten zu\n\nWenn dein Telefon den Dienst als nicht funktionierend anzeigt, versuche ihn zu deaktivieren und anschließend wieder zu aktivieren. 28 | Hier tippen, um den Anrufeaufzeichnungsdienst zu aktivieren 29 | Bitte aktiviere den Anrufeaufzeichnungsdienst bei den Bedienungshilfen. 30 | Der Anrufeaufzeichnungsdienst ist nicht aktiviert 31 | 32 | 33 | Herunterladen 34 | 35 | 36 | Die Google Play Store Version von ACR Phone ist nicht installiert oder muss aktualisiert werden. Diese App kann nur zusammen mit einer kompatiblen Version von ACR Phone verwendet werden 37 | Installieren 38 | Muss weiterhin aktiv sein, damit ACR Phone Anrufe aufzeichnen kann 39 | Diese App muss aktualisiert werden, um korrekt zu funktionieren 40 | Audio-Aufnahme-Berechtigung 41 | Benachrichtigung 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-el/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Το APH κατέρρευσε 6 | Παρουσιάστηκε σφάλμα προκαλώντας κατάρρευση και διακοπή λειτουργίας του APH.\nΠαρακαλώ βοηθήστε μας να διορθώσουμε αυτό στέλνοντας μας δεδομένα σφάλματος, το μόνο που πρέπει να κάνετε είναι να πατήσετε \'ΟΚ\' και να στείλετε αυτή την αναφορά μέσω email. 7 | Όλα τα δικαιώματα απαιτούνται για την πλήρη λειτουργικότητα 8 | Απαιτείται Άδεια 9 | Αποστολή 10 | Ακύρωση 11 | Crash notification 12 | Προειδοποίηση! 13 | 14 | 15 | Βρέθηκε νέα έκδοση 16 | Δεν είναι δυνατή η εύρεση εφαρμογής για το άνοιγμα αυτού του συνδέσμου 17 | Δεν είναι δυνατή η ανάλυση αυτού του συνδέσμου 18 | 19 | 20 | Ενεργοποίηση 21 | 22 | 23 | Πατήστε το κουμπί Επιτρέπω για να δείτε και να παραχωρήσετε τις απαιτούμενες άδειες για καταγραφή κλήσεων 24 | 25 | 26 | Βοηθός καταγραφής κλήσεων 27 | Αυτή η υπηρεσία χρησιμοποιείται για πρόσβαση στον ήχο κλήσεων. Δεν είναι δυνατή η εγγραφή ήχου κλήσης όταν είναι απενεργοποιημένη. Για να περιορίσετε την πρόσβαση στα δεδομένα σας, η εφαρμογή περιορίζει την παρατήρηση στα δικά της συμβάντα και δεν απαιτεί καμία δυνατότητα \n\nΟρισμένα τηλέφωνα ενδέχεται να αναφέρουν ότι η υπηρεσία δεν λειτουργεί. Δοκιμάστε να το απενεργοποιήσετε και να το ενεργοποιήσετε ξανά εάν συμβεί αυτό. 28 | Πατήστε εδώ για να ενεργοποιήσετε το Βοηθό καταγραφής κλήσεων 29 | Ενεργοποιήστε το Βοηθό καταγραφής κλήσεων στην οθόνη Υπηρεσίες εγκατεστημένης προσβασιμότητας 30 | Ο βοηθός καταγραφής κλήσεων δεν είναι ενεργοποιημένος 31 | 32 | 33 | Λήψη 34 | Γνωστοποίηση 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-et/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH jooksis kokku 6 | APH-i kokkujooksmise ja seiskamise tõttu ilmnes tõrge. \nPalun aidake meil see parandada, saates meile tõrkeandmed. Teil tuleb vaid toksata \"OK\" ja saata see aruanne e-kirjaga. 7 | Kõigi funktsioonide kasutamiseks on vaja kõiki õigusi 8 | Luba nõutud 9 | Saada 10 | Katkesta 11 | Kokkujooksmise teade 12 | Hoiatus! 13 | 14 | 15 | Leitud uus versioon 16 | Selle lingi avamiseks ei leia ühtegi rakendust 17 | Seda linki ei saa töödelda 18 | 19 | 20 | Luba 21 | 22 | 23 | Puudutage valikut \"Luba\", et näha ja anda kõne salvestamiseks vajalikke õigusi 24 | 25 | 26 | Kõnesalvesti abimees 27 | Seda teenust kasutatakse kõne helile juurdepääsu saamiseks. Keelatud kõne heli ei saa salvestada kui see on keelatud. Teie andmetele juurdepääsu piiramiseks piirdub rakendus vaatlemise oma sündmustega\n\nMõned telefonid võivad teatada, et teenus ei tööta. Kui see juhtub, proovige see uuesti keelata ja lubada. 28 | Kõne salvestamise abistaja lubamiseks puudutage siin 29 | Lubage kõne salvestamise abimees installitud juurdepääsetavusteenuste ekraanil 30 | Kõnesalvestamise abimees pole lubatud 31 | 32 | 33 | Lae alla 34 | Teade 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH خراب شد 6 | هنگام خرابی و توقف APH خطایی روی داد.\nلطفاً با ارسال اطلاعات خطا برای رفع این مشکل به ما کمک کنید ، فقط کافی است روی \"تأیید\" ضربه بزنید و این گزارش را از طریق ایمیل ارسال کنید. 7 | برای عملکرد کامل ، همه مجوزها لازم است 8 | درخواست مجوز 9 | ارسال 10 | لغو 11 | اعلان خرابی 12 | هشدار 13 | 14 | 15 | نسخه جدید پیدا شد 16 | برای باز کردن این پیوند نمی توانید هیچ برنامه ای پیدا کنید 17 | تجزیه این پیوند امکان پذیر نیست 18 | 19 | 20 | فعال کردن 21 | 22 | 23 | برای مشاهده و اعطای مجوزهای لازم برای ضبط تماس ، روی «مجاز» ضربه بزنید 24 | 25 | 26 | مکالمه ضبط تماس 27 | این سرویس برای دسترسی به صدای تماس استفاده می شود. وقتی صدا غیرفعال باشد ، هیچ فراخوانی ضبط نمی شود. برای محدود کردن دسترسی به داده های شما ، برنامه مشاهده به وقایع خاص خود را محدود می کند و هیچ گونه قابلیتی نیاز نیست. ممکن است برخی از تلفن ها سرویس را گزارش دهند که کار نمی کند. اگر این اتفاق افتاد ، دوباره آن را غیرفعال و فعال کنید. 28 | برای فعال کردن راهنمای ضبط تماس ، روی اینجا ضربه بزنید 29 | لطفاً راهنمای ضبط تماس را در صفحه خدمات دسترسی قابل نصب نصب کنید 30 | راهنمای ضبط تماس فعال نیست 31 | اطلاع 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values-gl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH fallou 6 | Produciuse un erro que causou que APH fallase e se detivese.\nPor favor, axúdenos a solucionalo enviándonos os datos do erro, só ten que premer \"Si\" e enviar este informe por correo electrónico. 7 | Son necesarios todos os permisos para obter funcionalidade completa 8 | Solicitude de permiso 9 | Enviar 10 | Cancelar 11 | Notificación de fallo 12 | Aviso! 13 | 14 | 15 | Atopouse unha nova versión 16 | Non se atopou ningunha aplicación para abrir esta ligazón 17 | Non é posible analizar esta ligazón 18 | 19 | 20 | Activar 21 | 22 | 23 | Toque \"Permitir\" para ver e conceder os permisos necesarios para a gravación de chamadas 24 | 25 | 26 | Asistente de Gravación de Chamadas 27 | Este servizo úsase para acceder ao audio das chamadas. Se está desactivado o audio non poderá ser gravado. Para restrinxir o acceso aos seus datos, a aplicación limítase a observar os seus propios eventos e non solicita ningunha competencia.\n\nPode que algúns teléfonos indiquen que o servizo non funciona. Se isto pasa, intente desactivando e reactivando o servizo. 28 | Tocar aquí para activar o Asistente de Gravación de Chamadas 29 | Por favor, active o Asistente de Gravación de Chamadas na pantalla de Servizos de Accesibilidade Instalados 30 | O Asistente de Gravación de Chamadas non está activado 31 | 32 | 33 | Descargar 34 | 35 | Notificación 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-hi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH क्रैश हो गया है 6 | क्रैश करने और रोकने के लिए ACR Phone के कारण त्रुटि हुई थी.\nकृपया हमें त्रुटि डेटा भेजकर इसे ठीक करने में मदद करें, इसके लिए आपको \'ओके\' पर टैप करना होगा और ईमेल द्वारा यह रिपोर्ट भेजनी होगी. 7 | पूर्ण कार्यक्षमता के लिए सभी अनुमतियों की आवश्यकता होती है 8 | अनुमति अनुरोध 9 | संदेश 10 | रद्द करें 11 | क्रैश नोटिफिकेशन 12 | चेतावनी! 13 | 14 | 15 | नया संस्करण मिला 16 | इस लिंक को खोलने के लिए कोई ऐप नहीं ढूंढ सकता 17 | इस लिंक को पार्स करने में असमर्थ 18 | 19 | 20 | सक्षम करें 21 | 22 | 23 | कॉल रिकॉर्डिंग के लिए आवश्यक अनुमति देखने और देने की अनुमति पर टैप करें 24 | 25 | 26 | कॉल रिकॉर्डिंग हेल्पर 27 | इस सेवा का उपयोग कॉल ऑडियो तक पहुंचने के लिए किया जाता है. अक्षम होने पर कोई कॉल ऑडियो रिकॉर्ड नहीं किया जा सकता है. आपके डेटा तक पहुंच को सीमित करने के लिए, ऐप अपने स्वयं के ईवेंट का अवलोकन करता है और किसी भी क्षमता का अनुरोध नहीं करता है\n कुछ फोन सेवा को काम नहीं करने के रूप में रिपोर्ट कर सकते हैं. ऐसा होने पर इसे फिर से अक्षम और सक्षम करने का प्रयास करें. 28 | कॉल रिकॉर्डिंग हेल्पर सक्षम करने के लिए यहां टैप करें 29 | स्थापित एक्सेसिबिलिटी सर्विसेज स्क्रीन पर कॉल रिकॉर्डिंग हेल्पर सक्षम करें 30 | कॉल रिकॉर्डिंग हेल्पर सक्षम नहीं है 31 | 32 | अधिसूचना 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values-hr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH se srušio 6 | Došlo je do greške zbog čega se APH srušio i zaustavio.\nMolimo vas da nam pomognete da to popravimo tako što ćete nam poslati podatke o pogrešci, sve što trebate je dodirnuti \'U redu\' i poslati ovaj izvještaj e-poštom. 7 | Sve dozvole su potrebne za punu funkcionalnost 8 | Zahtjev za dozvolu 9 | Poslati 10 | Otkaži 11 | Obavijest o padu aplikacije 12 | Upozorenje! 13 | 14 | 15 | Nova verzija pronađena 16 | Nije moguće pronaći aplikaciju za otvaranje ove URL veze 17 | Nije moguće raščlaniti ovu vezu 18 | 19 | 20 | Omogući 21 | 22 | 23 | Dodirnite Dozvoli da biste vidjeli i odobrili potrebne dozvole za snimanje poziva 24 | 25 | 26 | Pomoćnik za Snimanje Poziva 27 | Ova usluga se koristi za pristup zvuku poziva. Nijedan zvuk poziva ne može se snimiti kad je onemogućen. Da bi ograničili pristup vašim podacima, aplikacija ograničava promatranje na vlastite događaje i ne zahtijeva nikakve mogućnosti.\n\nNeki telefoni mogu prijaviti da usluga ne radi. Pokušajte ga onemogućiti i ponovo omogućiti ako se to dogodi. 28 | Dodirnite ovdje da omogućite Pomoćnika za Snimanje Poziva 29 | Molimo omogućite Pomoćnika za Snimanje Poziva na zaslonu Instalirane Usluge Pristupačnosti 30 | Pomoćnik za Snimanje Poziva nije omogućen 31 | 32 | 33 | Preuzimanje 34 | Obavijest 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-hu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Az APH összeomlott 6 | Egy hiba miatt az APH összeomlott.\nKérjük segítsen a hibajavításban, riport küldésével! Csak nyomjon az OK gombra a riport emailben való küldéséhez! 7 | Minden engedély megadására szükség van a működéshez 8 | Engedély kérés 9 | Küldés 10 | Mégse 11 | Összeomlási jelzés 12 | Figyelem! 13 | 14 | 15 | Új verzió található 16 | Nem található alkalmazás a link megnyitásához 17 | Nem sikerült értelmezni a linket 18 | 19 | 20 | Engedélyez 21 | 22 | 23 | Érintse meg az Engedélyezés gombot a hívásrögzítéshez szükséges engedélyek megtekintéséhez és megadásához 24 | 25 | 26 | Call Recording Helper 27 | Ez a szolgáltatás a híváshang elérésére szolgál. A hívás hangja nem rögzíthető, ha ez le van tiltva. Az adatokhoz való hozzáférés korlátozása érdekében az alkalmazás saját eseményeire korlátozza a megfigyelést, és nem kér semmilyen lehetőséget.\n\nEgyes telefonok jelezhetik, hogy a szolgáltatás nem működik. Ha ez történik, próbálja meg letiltani, majd újra engedélyezni a szolgáltatást. 28 | Koppintson ide a Call Recording Helper engedélyezéséhez 29 | Engedélyezze a Call Recording Helper funkciót a Telepített kisegítő lehetőségeknél 30 | A Call Recording Helper nincs engedélyezve 31 | 32 | 33 | Letöltés 34 | 35 | 36 | Telepítés 37 | Értesítés 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/values-hy/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH-ը վթարվեց 6 | Վրեպի պատճառով APH-ը վթարվեց և փակվեց։\nԽնդրում ենք, որ մեզ օգնեք, որ խնդիրը կարգավորենք։ Սեղմեք «Լավ» և ուղարկեք վրեպի զեկույցը էլ. փոստով։ 7 | Բոլոր թույլտվությունները անհրաժեշտ են ամբողջական ֆունկցիոնալության համար 8 | Թույլտվության Հարցում 9 | Ուղարկել 10 | Չեղարկել 11 | Վթարի ծանուցում 12 | Ուշադրություն. 13 | 14 | 15 | Հասանելի է նոր տարբերակ 16 | Չհաջողվեց գտնել հավելված այս հղումը բացելու համար 17 | Հղումը չի ճանաչվում 18 | 19 | 20 | Միացնել 21 | 22 | 23 | Հպեք «Թույլատրել» կոճգամը, որպեսզի տեսնեք և տաք ձայնագրմանը անհրաժեշտ թույլտվությունները 24 | 25 | 26 | Զանգերի Ձայնագրման Օգնական 27 | Հպեք, որպեսզի պատրաստվել զանգերի ձայնագրմանը 28 | Միացրեք «Զանգերի Ձայնագրման Օգնականը» Տեղադրված Մատչելիության Ծառայություններ էկրանում 29 | Զանգերի Ձայնագրման Օգնականը միացված չէ 30 | 31 | 32 | Ներբեռնել 33 | Ծանուցում 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH crash 6 | Ada kesalahan yang membuat APH crash dan berhenti.\nHarap bantu kami membenahinya dengan mengirimkan data kesalahan, yang perlu anda lakukan hanyalah menyentuh \'OK\' dan mengirim laporan ini melalui email. 7 | Semua perizinan diperlukan untuk fungsionalitas penuh 8 | Permintaan Perizinan 9 | Kirim 10 | Batal 11 | Pemberitahuan crash 12 | Perhatian! 13 | 14 | 15 | Versi baru ditemukan 16 | Tidak dapat menemukan apl untuk membuka tautan ini 17 | Tidak dapat mengurai tautan ini 18 | 19 | 20 | Aktifkan 21 | 22 | 23 | Ketuk Izinkan untuk melihat dan memberi izin yang dibutuhkan perekam panggilan 24 | Pemberitahuan 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH קרס 6 | היתה שגיאה שגרמה ל-APH לקרוס ולעצור. \n אנא עזרו לנו לתקן זאת על ידי שליחת מידע על השגיאה, כל אשר עליך לעשות זה ללחוץ על ה-\'OK\' ולשלוח את הדו\"ח במייל. 7 | כל ההרשאות חיוניות עבור ביצוע מלא 8 | בקשת הרשאה 9 | שלח/י 10 | בטל/י 11 | התראת קריסה 12 | אזהרה! 13 | 14 | 15 | נמצאה גירסה חדשה 16 | לא נמצא יישום מתאים לפתיחת קישור זה 17 | לא ניתן לנתח את הקישור הזה 18 | 19 | 20 | לְאַפשֵׁר 21 | 22 | 23 | הקש על אישור כדי לראות ולהעניק הרשאות נדרשות להקלטת שיחות 24 | 25 | 26 | עוזר הקלטת שיחות 27 | שירות זה משמש לגישה לאודיו של שיחה. לא ניתן להקליט אודיו של שיחה כשהוא מושבת. על מנת להגביל את הגישה לנתונים שלך, האפליקציה מגבילה את התצפית לאירועים שלה ואינה מבקשת יכולת כלשהי. טלפונים מסוימים עשויים לדווח כי השירות אינו פועל. נסה להשבית ולהפעיל אותו שוב אם זה קורה. 28 | הקש כאן כדי להפעיל עוזר להקלטת שיחות 29 | אנא הפעל עוזר להקלטת שיחות במסך שירותי נגישות מותקנים 30 | עוזר הקלטת שיחות אינו מופעל 31 | 32 | 33 | הורד 34 | התראות 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APHがクラッシュしました 6 | APHのクラッシュおよび停止の原因となるエラーがありました。\nエラーデータを送信してこの修正をお手伝いください。「OK」をタップしてこのレポートをメールで送信するだけです。 7 | 全機能を使うには、すべての許可が必要です 8 | 許可リクエスト 9 | 送信 10 | キャンセル 11 | クラッシュ通知 12 | 警告! 13 | 14 | 15 | 新しいバージョンが見つかりました 16 | このリンクを開くアプリが見つかりません 17 | このリンクを解析できません 18 | 19 | 20 | 有効にする 21 | 22 | 23 | [許可] をタップして、通話録音に必要な許可を表示して付与します 24 | 25 | 26 | 通話録音ヘルパーに電話する 27 | このサービスは、通話音声へのアクセスに使われます。無効にすると、通話音声を録音できません。データへのアクセスを制限するため、アプリは監視を独自のイベントに制限し、機能を要求しません。\n\n一部の電話のサービスが機能していないと報告する場合があります。これが発生した場合、一旦無効後再度有効にしてみてください。 28 | 通話録音ヘルパーを有効にするには、ここをタップしてください 29 | インストールされているユーザー補助サービス画面で通話録音ヘルパーを有効にしてください 30 | 通話録音ヘルパーが有効になっていません 31 | 32 | 33 | ダウンロード 34 | お知らせ 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH 작동이 중단되었습니다 6 | APH을 작동 중지시키는 오류가 발생했습니다.\n이 문제를 해결할 수 있도록 오류 데이터를 보내주세요. \'확인\'을 탭하고 보고서를 이메일로 보내기만 하면됩니다. 7 | 전체 기능을 사용하려면 모든 권한이 필요합니다 8 | 허가 요청 9 | 전송 10 | 취소 11 | 작동 중지 알림 12 | 경고! 13 | 14 | 15 | 새 버전 발견 16 | 이 링크를 열 수 있는 앱을 찾을 수 없습니다 17 | 이 링크를 구문 분석할 수 없습니다. 18 | 19 | 20 | 활성화 21 | 22 | 23 | 허용을 탭하여 통화 녹음에 필요한 권한을 확인하고 부여하십시오 24 | 25 | 26 | 통화 녹음 도우미 27 | 이 서비스는 통화 오디오에 액세스하는데 사용됩니다. 비활성화되면 통화 오디오를 녹음 할 수 없습니다. 데이터에 대한 액세스를 제한하기 위해 앱은 자체 이벤트에 대한 관찰을 제한하고 기능을 요청하지 않습니다\n\n일부 휴대폰은 서비스가 작동하지 않는 것으로 보고될 수 있습니다. 이 경우 비활성화했다가 다시 활성화하십시오. 28 | 통화 녹음 도우미를 활성화하려면 여기를 누르십시오 29 | 설치된 접근성 서비스 화면에서 통화 녹음 도우미를 활성화하십시오 30 | 통화 녹음 도우미가 활성화되지 않았습니다 31 | 32 | 33 | 내려받기 34 | 알림 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-ku/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | مۆبایلی ئەی سی ئاڕ تێکشکا 6 | هەڵەیەک ڕوویدا کە وای لە APH کرد کە تێکبدا و بوەستێ.\nتکایە یارمەتیمان بدە بۆ چارەسەرکردنی ئەم زانیاریە بە ناردنی داتای هەڵە بۆ ئێمە، هەموو ئەوەی کە دەبێت نەرم لێبدەی لە \'OK\' و ئەم ڕاپۆرتە بنێرە بە ئیمەیڵ 7 | هەموو مۆڵەتەکان پێویستە بۆ کرداری تەواو 8 | داواکردنی مۆڵەت پێدان 9 | ناردن 10 | لابردن 11 | ئاگانامەی پێکدادان 12 | ئاگادارکردنه‌وه‌ ! 13 | 14 | 15 | وەشانی نوێ دۆزرایەوە: %1$s 16 | هیچ ئەپێک نەدۆزرایەوە بۆ کردنەوەی ئەم پەڕگەیە 17 | ناتوانێت ئەم لینکە شیبکاتەوە 18 | 19 | 20 | چالاککردن 21 | 22 | 23 | نەرم لێبدە لەسەر ڕێپێدان بۆ بینین و پێدانی ڕێپێدانە پێویستەکان بۆ تۆمارکردنی پەیوەندی 24 | 25 | 26 | پەیوەندی کردن تۆمارکردن یارمەتیدەر 27 | ئەم خزمەتگوزاریە بەکاردێت بۆ گەیشتن بە دەنگی پەیوەندی. هیچ دەنگی پەیوەندیکردن ێک ناتوانێت تۆمار بکرێت کاتێک ناچالاک کراوە. بۆ سنووردارکردنی دەستگەیشتن بە داتاکەت، کاربەرنامە چاودێری ڕووداوەکانی خۆی سنووردار دەکات و داوای هیچ توانایەک ناکات\n\nSome لەوانەیە خزمەتگوزاری ڕاپۆڕت بکات وەک کارناکات. هەوڵدە لەکار کەیت و دووبارە چالاک بکە ئەگەر ئەمە ڕوویدا 28 | نەرم لێبدە لێرە بۆ بەتواناکردنی یارمەتیدەری تۆمارکردنی پەیوەندی 29 | تکایە یارمەتیدەری تۆمارکردنی پەیوەندی چالاک بکە لەسەر شاشەی خزمەتگوزاریە دامەزراوەکان 30 | یارمەتیدەری تۆمارکردنی پەیوەندی چالاک نەکراوە 31 | 32 | 33 | دابەزاندن 34 | ئاگادارکردنەوە 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values-lt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH užlūžo 6 | Įvyko klaida, dėl kurios APH užlūžo ir nustojo veikti.\nPadėkite mums ateityje išvengti panašių klaidų, viskas, ką turėtumėte padaryti, tau spustelėti \'OK\' ir nusiųsti šią ataskaitą el. paštu. 7 | Pilnam funkcionavimui reikalingi visi leidimai 8 | Leidimo užklausa 9 | Siųsti 10 | Atšaukti 11 | Pranešimas apie klaidą 12 | Įspėjimas! 13 | 14 | 15 | Rasta nauja versija 16 | Nerasta programa nuorodai atidaryti 17 | Klaida nuskaitant nuorodą 18 | 19 | 20 | Įjungti 21 | 22 | 23 | Spustelkite ant Leisti norint pamatyti bei suteikti reikalingus leidimus skambučių įrašymui 24 | 25 | 26 | Skambučio Įrašymo Pagalbininkas 27 | 28 | Priminimas 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/values-ms/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH telah terhenti 6 | Terdapat kegagalan yang menyebabkan APH terhenti.\nBantu kami untuk baiki kelemahan ini dengan menghantar data kegagalan, hanya tekan \'OK\' untuk menghantar laporan melalui email. 7 | Semua kebenaran diperlukan untuk fungsi penuh 8 | Meminta Kebenaran 9 | Hantar 10 | Batal 11 | Notifikasi Kegagalan 12 | Amaran! 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-nb-rNO/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH har krasjet 6 | Det oppstod en feil som fikk APH til å krasje og stoppe.\nHjelp oss med å fikse dette ved å sende oss feildata. Alt du trenger å gjøre er å trykke på \'OK\' og sende rapporten via e-post. 7 | Alle tillatelser er påkrevd for full funksjonalitet 8 | Forespørsel om tillatelse 9 | Send 10 | Avbryt 11 | Krasj varsler 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH is gecrasht 6 | Er is een fout opgetreden waardoor APH crashte en stopte.\nHelp ons dit op te lossen door ons foutgegevens te sturen, je hoeft alleen maar op \'OK\' te tikken en dit rapport per e-mail te verzenden. 7 | Alle bevoegdheden zijn vereist voor volledige functionaliteit 8 | Machtigingsverzoek 9 | Verzenden 10 | Annuleren 11 | Crash melding 12 | Waarschuwing! 13 | 14 | 15 | Nieuwe versie gevonden 16 | Kan geen enkele app vinden om deze link te openen 17 | Kan deze link niet parsen 18 | 19 | 20 | Inschakelen 21 | 22 | 23 | Tik op Toestaan om de vereiste machtigingen voor oproepopname te bekijken en te geven 24 | 25 | 26 | Oproep Opname Helper 27 | Deze service wordt gebruikt om toegang te krijgen tot de audio van een telefoongesprek. Er kan geen audio worden opgenomen wanneer deze uitgeschakeld is. Om de toegang tot uw gegevens te beperken, beperkt de app waarnemingen tot zijn eigen gebeurtenissen\n\nSommige telefoons kunnen aangeven dat ze niet werken. Probeer dit uit te schakelen en weer in te schakelen als dit gebeurt. 28 | Tik hier om de Oproep Opname Helper in te schakelen 29 | Schakel de Oproep Opname Helper in op Geïnstalleerde Toegankelijkheid Services scherm 30 | Oproep Opname Helper is niet ingeschakeld 31 | 32 | 33 | Download 34 | 35 | 36 | Google Play Store versie ACR Phone is niet geïnstalleerd of moet worden bijgewerkt. Deze app kan alleen samen met een compatibele versie van de ACR Phone gebruikt worden 37 | Installeren 38 | Moet actief blijven voor gespreksopnamefunctie van ACR Phone 39 | Deze app moet worden bijgewerkt om correct te kunnen functioneren 40 | Audio-opname rechten 41 | Melding 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-no-rNO/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH har krasjet 6 | Det oppstod en feil som fikk APH til å krasje og stoppe.\nHjelp oss med å fikse dette ved å sende oss feildata. Alt du trenger å gjøre er å trykke på \'OK\' og sende rapporten via e-post. 7 | Alle tillatelser er påkrevd for full funksjonalitet 8 | Forespørsel om tillatelse 9 | Send 10 | Avbryt 11 | Krasj varsler 12 | Advarsel! 13 | 14 | 15 | Ny versjon funnet 16 | Finner ikke noen app for å åpne denne lenken 17 | Kan ikke åpne denne koblingen 18 | 19 | 20 | Aktiver 21 | 22 | 23 | Trykk på tillat for å se og gi nødvendige tillatelser for opptak av anrop 24 | 25 | 26 | Hjelp for samtaleopptak 27 | Denne tjenesten brukes for å få tilgang til lyd fra samtaler. Ingen samtaler kan tas opp hvis den er deaktivert. For å begrense tilgangen til dine data, ser ACR app kun egne hendelser og ber ikke om utvidet tilgang\n\nNoen telefoner kan rapportere tjenester som ikke fungerende. Prøv å deaktivere og aktivere det igjen hvis dette skjer. 28 | Trykk her for å aktivere samtaleopptakshjelper 29 | Vennligst aktiver samtaleopptakshjelper på skjermen for tilgjengelighetstjenester 30 | Samtaleopptakhjelper er ikke aktivert 31 | 32 | 33 | Last ned 34 | Varsling 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wystąpił błąd z aplikacją APH 6 | Wystąpił błąd z aplikacją APH i została zatrzymana. \nPomóż nam naprawić ten błąd wysyłając nam raport o błędach, kliknij \"OK\" aby wysłać ten raport pocztą elektroniczną. 7 | Wszystkie uprawnienia są wymagane aby aplikacja działała poprawnie 8 | Uprawnienia 9 | Wyślij 10 | Anuluj 11 | Powiadomienie o awariach 12 | Ostrzeżenie! 13 | 14 | 15 | Znaleziono nową wersję 16 | Nie można znaleźć żadnej aplikacji do otwarcia tego linku 17 | Nie można otworzyć linku 18 | 19 | 20 | Włącz 21 | 22 | 23 | Zezwól na uprawnienia do wyświetlania i nagrywania połączeń 24 | 25 | 26 | Pomocnik nagrywania połączeń 27 | Ta usługa jest używana do dostępu do dźwięku połączenia. Żadne połączenie nie może być nagrywane, gdy jest wyłączone. W celu ograniczenia dostępu do Twoich danych, aplikacja ogranicza obserwację do własnych wydarzeń i nie wymaga żadnych możliwości\n\nNiektóre telefony mogą zgłaszać usługę jako niedziałającą. Spróbuj wyłączyć i włączyć ponownie, jeśli tak się stanie. 28 | Dotknij tutaj, aby włączyć pomocnika nagrywania połączeń 29 | Proszę włączyć pomocnika nagrywania połączeń na ekranie Usług Dostępności 30 | Pomocnik nagrywania połączeń nie jest włączony 31 | 32 | 33 | Pobierz 34 | 35 | 36 | Aplikacja ACR Phone w sklepie Google Play nie jest zainstalowana lub musi zostać zaktualizowana. Ta aplikacja może być używana tylko razem z kompatybilną wersją telefonu ACR 37 | Zainstaluj 38 | Musi być uruchomiony dla funkcji nagrywania połączeń ACR Phone 39 | Ta aplikacja musi być zaktualizowana aby działać poprawnie 40 | Uprawnienia do nagrywania dźwięku 41 | Powiadomienie 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH travou 6 | Houve um erro que originou um bloqueio da aplicação APH.\nPor favor ajude-nos a corrigir este problema enviando-nos dados do erro, basta clicar em \'OK\' e enviar este relatório por e-mail. 7 | Todas as permissões são necessárias para todas as funcionalidades 8 | Solicitação de permissão 9 | Enviar 10 | Cancelar 11 | Notificação de falha 12 | Atenção! 13 | 14 | 15 | Nova versão encontrada 16 | Não foi possível encontrar um app para abrir este link 17 | Não foi possível analisar este link 18 | 19 | 20 | Ativar 21 | 22 | 23 | Toque em Permitir para ver e conceder as permissões necessárias para a gravação de chamadas 24 | 25 | 26 | Assistente de Gravação 27 | Este serviço é usado para acessar o áudio de chamadas. Nenhum áudio de chamada pode ser gravado quando está desativado. Para limitar o acesso aos seus dados, o aplicativo limita a observação em seus próprios eventos e não demanda nenhum recurso\n\nAlguns telefones podem relatar como não funcionando. Tente desativar e ativar novamente se isto acontecer. 28 | Toque aqui para ativar o Assistente de Gravação de Chamadas 29 | Por favor, ative o Assistente de Gravação de Chamadas na tela de Serviços de Acessibilidade Instalados 30 | O Assistente de Gravação de Chamadas não está ativado 31 | 32 | 33 | Baixar 34 | 35 | 36 | A versão ACR Phone do Google Play Store não está instalada ou precisa ser atualizada. Este app só pode ser usado juntamente com uma versão compatível do ACR Phone 37 | Instalar 38 | Serviço para manter a funcionalidade de gravação de chamadas do ACR Phone 39 | Este aplicativo deve ser atualizado para funcionar corretamente 40 | Permissão de gravação de áudio 41 | Notificação 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-ro/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH s-a oprit 6 | A apărut o eroare ce a dus la blocarea și oprirea APH.\nVă rugăm să ne ajutați să o rezolvăm, trimițându-ne date de eroare, tot ce trebuie să faceți fiind să apăsați „OK“ și să trimiteți acest raport prin e-mail. 7 | Toate permisiunile sunt necesare pentru funcționalitate completă 8 | Solicitare de permisiune 9 | Trimiteți 10 | Anulare 11 | Notificare de eroare 12 | Atenție! 13 | 14 | 15 | Permite 16 | 17 | 18 | Apăsați pe Permiteți pentru a vedea și acorda permisiunile necesare pentru înregistrarea apelurilor 19 | 20 | 21 | Ajutor pentru înregistrarea apelurilor 22 | Acest serviciu este utilizat pentru accesarea sunetului apelului. Niciun sunet de apel nu poate fi înregistrat când este dezactivat. Pentru a limita accesul la datele dvs., aplicația limitează observarea la propriile evenimente și nu solicită nicio capacitate. Unele telefoane pot raporta că serviciul nu funcționează. Încercați să o dezactivați și să o activați din nou dacă se întâmplă acest lucru. 23 | Atingeți aici pentru a activa asistentul pentru înregistrarea apelurilor 24 | Vă rugăm să activați ajutorul pentru înregistrarea apelurilor pe ecranul Servicii de accesibilitate instalate 25 | Asistentul pentru înregistrarea apelurilor nu este activat 26 | 27 | 28 | Descarcare 29 | Notificare 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Программа APH завершена с ошибкой 6 | Произошла ошибка при сбое и остановке работы APH.\nПожалуйста, помогите нам исправить это, отправив нам данные об ошибке. Все что вам надо сделать чтобы отправить этот отчет по электронной почте, только нажать \'OK\'. 7 | Все права необходимы для полной функциональности 8 | Запрос разрешения 9 | Отправить 10 | Отменить 11 | Уведомление об ошибке 12 | Внимание! 13 | 14 | 15 | Найдена новая версия 16 | Не удается найти приложение для открытия ссылки 17 | Не удалось определить ссылку 18 | 19 | 20 | Включить 21 | 22 | 23 | Нажать на кнопку Разрешения для просмотра и предоставления необходимых разрешений для записи звонка 24 | 25 | 26 | Помощник записи вызовов 27 | Этот сервис используется для доступа к звуку вызовов. Звук звонков не может быть записан, если он отключен. Чтобы ограничить доступ к вашим данным, приложение ограничивает наблюдение за собственными событиями и не запрашивает никаких возможностей\n\nНекоторые телефоны могут сообщать об ошибках в работе службы. Попробуйте отключить и снова включить его, если это произойдет. 28 | Нажмите здесь, чтобы включить поддержку записи вызовов 29 | Пожалуйста, включите \"Помощник записи звонков\" на экране \"Услуги специальных возможностей\" 30 | Помощник записи вызовов не включен 31 | 32 | 33 | Загрузить 34 | 35 | 36 | Версия ACR телефона Google Play Store не установлена или должна быть обновлена. Это приложение можно использовать только вместе с совместимой версией ACR Phone 37 | Установить 38 | Выполняется для работы функции записи вызовов ACR телефона 39 | Приложение должно быть обновлено для правильной работы 40 | Права на запись аудио 41 | Уведомление 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-si/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ඒසීආර් පෝන් බිඳ වැටී ඇත 6 | ඒසීආර් පෝන් බිඳ වැටී හා නතර වීමට හේතුවන දෝෂයක් ඇති විය.\nඅපට දෝෂ දත්ත එවීම මගින් මෙය නිවැරදි කිරීමට උදව් කරන්න, ඔබ කළ යුතු දේ නම් \'හරි\' මත තට්ටු කර වි-තැපෑලෙන් මෙම වාර්තාව එවීමයි. 7 | පූර්ණ ක්‍රියාකාරිත්වයට සියළු අවසර ඇවැසිය 8 | අවසරය ඉල්ලීම 9 | යවන්න 10 | අවලංගු 11 | බිඳවැටීමේ දැනුම්දීම 12 | අවවාදයයි! 13 | 14 | 15 | නව අනුවාදයක් හමු විය 16 | මෙම සබැඳිය විවෘත කිරීමට කිසිදු යෙදුමක් හමු නොවිණි 17 | 18 | 19 | සබල කරන්න 20 | 21 | 22 | ඇමතුම් පටිගත කිරීමට ඇවැසි අවසර දැක ලබා දීමට ඉඩදෙන්න මත තට්ටු කරන්න 23 | 24 | 25 | ඇමතුම් පටිගත උපකාරකය 26 | ඇමතුමේ හඬට ප්‍රවේශ වීමට මෙම සේවාව භාවිතා කරයි. එය අබල කර ඇති විට ඇමතුමේ හඬ පටිගත කළ නොහැකිය. ඔබගේ දත්ත වෙත ප්‍රවේශය සීමා කිරීමට, යෙදුම එහිම සිදුවීම් සීමා කරගන්නා අතර කිසිම ශක්‍යතාවක් ඉල්ලන්නේ නැත\n\nසමහර දුරකථන මෙය වැඩ නොකරන බව වාර්තා කළ හැකිය. මෙය සිදුවන්නේ නම් එය අබල කර යළි සබල කිරීමට උත්සාහ කරන්න. 27 | ඇමතුම් පටිගත උපකාරකය සබල කිරීමට මෙහි තට්ටු කරන්න 28 | ස්ථාපිත ප්‍රවේශ්‍යතා සේවා තිරයෙහි ඇමතුම් පටිගත උපකාරකය සබල කරන්න 29 | ඇමතුම් පටිගත උපකාරකය සබල කර නැත 30 | 31 | 32 | බාගන්න 33 | 34 | දැනුම්දීම 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values-sk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aplikácia APH prestala pracovať 6 | Vznikla chyba, ktorá spôsobila, že APH prestala pracovať.\nProsíme pomôžte nám opraviť túto chybu odoslaním dát o chybe. Stačí kliknúť na \'OK\' a odoslať email. 7 | Pre správne fungovanie sú potrebné všetky povolenia 8 | Žiadosť o povolenie 9 | Odoslať 10 | Zrušiť 11 | Upozornenie pri zlyhaní 12 | Upozornenie! 13 | 14 | 15 | Nájdená nová verzia 16 | Na otvorenie tohto odkazu nie je možné nájsť žiadnu aplikáciu 17 | Nie je možné analyzovať tento odkaz 18 | 19 | 20 | Povoliť 21 | 22 | 23 | Kliknutím na povoliť zobrazíte a udelíte požadované povolenie pre nahrávanie hovorov 24 | 25 | 26 | Pomocník pre nahrávanie hovorov 27 | Táto služba sa používa pre prístup k zvukovému hovoru. Pokiaľ je zakázaná, nie je možné zvuk hovoru zaznamenať. Pre omedzenie k prístupu k vašim údajom aplikácia obmedzuje pozorovanie na svoje vlastné udalosti a nevyžaduje žiadnu funkciu\n\nNiektoré telefóny môžu hlásiť, že služba nefunguje. Skúste ju znovu zakázať a povoliť, pokiaľ sa tak stane. 28 | Klepnutím sem povolíte pomocníka pre nahrávanie hovorov 29 | Povoľte prosím pomocníka pre nahrávanie hovorov na obrazovke Služby uľahčenia prístupu 30 | Pomocník pre nahrávanie hovorov nie je povolený 31 | 32 | 33 | Stiahnúť 34 | 35 | 36 | Verzia ACR Phone z obchodu Google Play nie je nainštalovaná, alebo je potrebné ju aktualizovať. Túto aplikáciu je možné používať iba spolu s kompatibilnou verziou ACR Phone. 37 | Inštalovať 38 | Pre funkciu nahrávania hovorov musí byť ACR Phone spustený 39 | Táto aplikácia musí byť aktualizovaná, aby fungovala správne 40 | Povolenie pre nahrávanie zvuku 41 | Oznámenie 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-sq/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kërkesë për Leje 6 | Dërgo 7 | Anullo 8 | Njoftime për avari 9 | Paralajmërim! 10 | 11 | 12 | Shkarko 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-sr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH se srušio 6 | Došlo je do greške zbog čega se APH srušio i zaustavio.\nPomozite nam da to popravimo tako što ćete nam poslati podatke o grešci, potrebno je samo da dodirnete \'OK\' i ovaj izveštaj pošaljete e-poštom. 7 | Sve dozvole su potrebne za punu funkcionalnost 8 | Potrebne dozvole 9 | Pošalji 10 | Otkaži 11 | Obaveštenje o padu aplikacije 12 | Upozorenje! 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH har kraschat 6 | Det uppstod ett fel så APH kraschade.\nHjälp oss gärna genom att trycka på \"OK\" och skicka in rapporten med felloggen till oss via e-post. 7 | Alla behörigheter krävs för full funktionalitet 8 | Behörighetsbegäran 9 | Skicka 10 | Avbryt 11 | Kraschavisering 12 | Varning! 13 | 14 | 15 | Hittade ny version 16 | Kan inte hitta någon app för att öppna den här länken 17 | Det gick inte att analysera den här länken 18 | 19 | 20 | Aktivera 21 | 22 | 23 | Tryck på Tillåt för att bevilja nödvändiga behörigheter för samtalsinspelning 24 | 25 | 26 | Samtalsinspelningshjälpare 27 | Denna tjänst används för att komma åt samtalsljud. Inget samtalsljud kan spelas in när det är inaktiverat.\n\nVissa telefoner kan rapportera att tjänsten inte fungerar. Prova då att inaktivera och aktivera den igen för att se om det hjälper. 28 | Tryck här för att aktivera samtalsinspelningshjälpare 29 | Aktivera samtalsinspelningshjälparen på tillgänglighetstjänstskärmen 30 | Samtalsinspelningshjälpare är inaktiverad 31 | 32 | 33 | Ladda ner 34 | 35 | 36 | Google Play Butikens version av ACR Phone är inte installerad eller måste uppdateras. Appen kan endast användas tillsammans med en kompatibel version av ACR Phone 37 | Installera 38 | Måste hållas igång för samtalsinspelningsfunktion av ACR Phone ska fungera 39 | Appen måste uppdateras för att fungera korrekt 40 | Behörighet för ljudinspelning 41 | Avisering 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH çöktü 6 | Beklenmeyen bir hata APH\'nin çökmesine neden oldu. Lütfen bu hatayı bize bildirerek giderilmesine yardımcı olun 7 | Tam işlevsellik için tüm izinler gereklidir 8 | İzin Talebi 9 | Yolla 10 | İptal 11 | Çökme bildirimi 12 | Uyarı! 13 | 14 | 15 | Yeni versiyon bulundu 16 | Bu bağlantıyı açmak için herhangi bir uygulama bulunamıyor 17 | Bu bağlantı ayrıştırılamıyor 18 | 19 | 20 | Etkinleştir 21 | 22 | 23 | Arama ses kaydı için gerekli izinleri görmek ve vermek için İzin Ver\'e dokunun 24 | 25 | 26 | Arama Kaydı Yardımcısı 27 | Bu hizmet, arama sırasında sese erişmek için kullanılır. Devre dışı bırakıldığında hiçbir arama sesi kaydedilemez. Verilerinize erişimi sınırlandırmak için uygulama, gözlemi kendi etkinlikleriyle sınırlar ve herhangi bir kabiliyet talep etmez\n\nBazı telefonlar hizmetin çalışmadığını bildirebilir. Böyle bir durumda devre dışı bırakıp tekrar etkinleştirmeyi deneyin. 28 | Arama Kaydı Yardımcısı\'nı etkinleştirmek için buraya dokunun 29 | Lütfen Yüklü Erişilebilirlik Hizmetleri ekranında Aram Kaydı Yardımcısı\'nı etkinleştirin 30 | Arama Kaydı Yardımcısı etkin değil 31 | 32 | 33 | İndir 34 | 35 | 36 | ACR Telefon\'un Google Play Store sürümü kurulu değil veya güncellenmesi gerekiyor. Bu uygulama yalnızca uyumlu bir ACR Telefon sürümüyle birlikte kullanılabilir 37 | Install 38 | ACR Phone\'un arama kaydetme işlevi için çalışır durumda tutulmalıdır 39 | Bu uygulamanın düzgün çalışması için güncellenmesi gerekmektedir 40 | Ses kaydı izni 41 | Bildirim 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Додаток APH аварійно завершив роботу 6 | Сталася помилка та робота APH була аварійно завершена.\nБудь ласка, допоможіть нам виправити це, надіславши нам інформацію про помилку, для цього вам лише потрібно натиснути \'Гаразд\' та надіслати цей звіт електронною поштою. 7 | Для повної функціональності необхідні всі дозволи 8 | Запит на дозвіл 9 | Надіслати 10 | Скасувати 11 | Сповіщення про аварію 12 | Попередження! 13 | 14 | 15 | Знайдено нову версію 16 | Не вдається знайти програму, щоб відкрити це посилання 17 | Не вдалося обробити це посилання 18 | 19 | 20 | Увімкнути 21 | 22 | 23 | Натисніть на кнопку \"Дозволити\" і надайте дозвіл для запису дзвінків 24 | 25 | Сповіщення 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values-uz/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH nosozlik yuz berdi 6 | Ruxsat so‘rovi 7 | Yuborish 8 | Bekor qilish 9 | Diqqat! 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-vi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH đã bị sự cố 6 | Đã xảy ra lỗi khiến APH bị sự cố và dừng.\nVui lòng giúp chúng tôi khắc phục điều này bằng cách gửi cho chúng tôi dữ liệu lỗi, tất cả những gì bạn phải làm là nhấn \'OK\' và gửi báo cáo này qua email. 7 | Tất cả các quyền được yêu cầu cho chức năng đầy đủ 8 | Yêu cầu cấp quyền 9 | Gửi 10 | Huỷ 11 | Thông báo sự cố 12 | Cảnh báo! 13 | 14 | 15 | Có phiên bản mới 16 | Không thể tìm thấy bất kỳ ứng dụng nào để mở liên kết này 17 | Không thể phân tích liên kết này 18 | 19 | 20 | Bật 21 | 22 | 23 | Nhấn vào Cho phép để xem và cấp các quyền cần thiết để ghi âm cuộc gọi 24 | 25 | 26 | Trợ giúp Ghi âm Cuộc gọi 27 | Dịch vụ này được sử dụng để truy cập âm thanh cuộc gọi. Không thể ghi âm cuộc gọi khi nó bị tắt. Để hạn chế quyền truy cập vào dữ liệu của bạn, ứng dụng giới hạn khả năng quan sát đối với các sự kiện riêng và không yêu cầu bất kỳ khả năng nào\n\nMột số điện thoại có thể báo cáo dịch vụ là không hoạt động. Hãy thử tắt và bật lại nếu điều này xảy ra. 28 | Nhấn vào đây để bật Trình trợ giúp ghi âm cuộc gọi 29 | Vui lòng bật Trình trợ giúp ghi âm cuộc gọi trên màn hình Dịch vụ trợ năng đã cài đặt 30 | Trình trợ giúp ghi âm cuộc gọi chưa được bật 31 | 32 | 33 | Tải xuống 34 | 35 | 36 | Phiên bản Cửa hàng Google Play của ACR Điện thoại chưa được cài đặt hoặc phải được cập nhật. Ứng dụng này chỉ có thể được sử dụng cùng với phiên bản ACR Phone tương thích 37 | Cài đặt 38 | Phải tiếp tục chạy cho chức năng ghi âm cuộc gọi của ACR Phone 39 | Ứng dụng này phải được cập nhật để hoạt động chính xác 40 | Quyền ghi âm thanh 41 | Thông báo 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH 已崩溃 6 | 发生错误,导致 APH 崩溃并停止。请通过向我们发送错误数据来帮助我们解决此问题,您只需点击“确定”并通过电子邮件发送此报告。 7 | 完整功能需要所有权限 8 | 权限请求 9 | 发送 10 | 取消 11 | 崩溃通知 12 | 警告! 13 | 14 | 15 | 发现新版本 16 | 找不到任何应用程序可以打开此链接 17 | 无法解析此链接 18 | 19 | 20 | 启用 21 | 22 | 23 | 点击允许查看并授予所需的通话记录权限 24 | 25 | 26 | 通话记录助手 27 | 此服务用于访问呼叫音频。禁用时,无法录制通话音频。为了限制对您数据的访问,应用程序将观察仅限于其自身的事件,并且不要求任何功能。某些手机可能会报告服务不起作用。如果发生这种情况,请尝试禁用并再次启用它。 28 | 点按此处启用通话记录助手 29 | 请在“已安装的辅助功能”屏幕上启用呼叫记录助手 30 | 呼叫记录助手未启用 31 | 32 | 33 | 下载 34 | 通知 35 | 确定 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APH 已經程式崩潰 6 | APH已出錯導致程序崩潰和停止運作。\n請幫我們回報造成錯誤的資訊,您僅需點擊「OK」按鈕並將報錯資料用電子郵件傳送 7 | 完整的功能需要所有權限 8 | 要求權限 9 | 發送 10 | 取消 11 | 通知崩潰 12 | 警告! 13 | 14 | 15 | 發現新版本 16 | 找不到可開啟此連結的應用程式 17 | 無法解析此連結 18 | 19 | 20 | 啟用 21 | 22 | 23 | 點擊允許以查看並授予所需的通話記錄權限 24 | 25 | 26 | 通話記錄助手 27 | 此服務用於訪問呼叫音頻。禁用時,無法錄製通話音頻。為了限制對您數據的訪問,應用程式將監視僅限於其自身的事件,並且不要求任何功能。某些手機可能會報告服務無法正常工作。如果發生這種情況,請嘗試禁用並再次啟用它。 28 | 點按此處啟用通話記錄助手 29 | 請在“已安裝的輔助功能”屏幕上啟用呼叫記錄幫助器 30 | 呼叫記錄助手未啟用 31 | 32 | 33 | 下載 34 | 通知 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values/app_helper_known_certs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AB0539F2CC3149FE986EEF9B46525BF117972FF4BA6592749296D21078A552BD 5 | 6 | 56158AAD73625E513FBFEEEBE1C4F04D7A992822D8C4916A8257EE19B4ACD8B5 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24dp 4 | 16dp 5 | 8dp 6 | 12dp 7 | 0dp 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/xml/helper_call_recording_accessibility_service.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/nllStore/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = true 32 | override fun canLinkToGooglePlayStore()= true 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-nll-app-store.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = false 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/nllStore/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | import com.nll.helper.util.Util 12 | 13 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 14 | private const val logTag = "DownloadUrlOpenerImpl" 15 | override fun getOpenDownloadUrlIntent( 16 | context: Context, 17 | remoteAppVersion: RemoteAppVersion 18 | ): Intent { 19 | 20 | val isNllAppStoreInstalled = Util.isAppInstalled(context, "com.nll.store") 21 | val urlToOpenString = if (isNllAppStoreInstalled) { 22 | remoteAppVersion.downloadUrl 23 | } else { 24 | "https://acr.app" 25 | } 26 | 27 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 28 | 29 | return Intent(Intent.ACTION_VIEW, Uri.parse(urlToOpenString)).apply { 30 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 31 | } 32 | } 33 | 34 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 35 | val isNllAppStoreInstalled = Util.isAppInstalled(context, "com.nll.store") 36 | val urlToOpenString = if (isNllAppStoreInstalled) { 37 | remoteAppVersion.downloadUrl 38 | } else { 39 | "https://acr.app" 40 | } 41 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion, isNllAppStoreInstalled: $isNllAppStoreInstalled, urlToOpenString: $urlToOpenString") 42 | 43 | try { 44 | val urlToOpen = Uri.parse(urlToOpenString) 45 | try { 46 | /** 47 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 48 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 49 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 50 | */ 51 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 52 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 53 | } 54 | context.startActivity(openIntent) 55 | } catch (e: Exception) { 56 | CLog.logPrintStackTrace(e) 57 | try { 58 | 59 | } catch (e: Exception) { 60 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 61 | } 62 | 63 | } 64 | } catch (e: Exception) { 65 | CLog.logPrintStackTrace(e) 66 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/oppoAppMarket/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = false 32 | override fun canLinkToGooglePlayStore() = false 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-oppo-appmarket.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = true 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/oppoAppMarket/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 13 | private const val logTag = "DownloadUrlOpenerImpl" 14 | override fun getOpenDownloadUrlIntent( 15 | context: Context, 16 | remoteAppVersion: RemoteAppVersion 17 | ): Intent { 18 | 19 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 20 | 21 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 22 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 23 | } 24 | } 25 | 26 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 27 | 28 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 29 | 30 | 31 | try { 32 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 33 | try { 34 | /** 35 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 36 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 37 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 38 | */ 39 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 40 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 41 | } 42 | context.startActivity(openIntent) 43 | } catch (e: Exception) { 44 | CLog.logPrintStackTrace(e) 45 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 46 | } 47 | } catch (e: Exception) { 48 | CLog.logPrintStackTrace(e) 49 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/vivoAppStore/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = true 32 | override fun canLinkToGooglePlayStore() = false 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-vivo-app-store.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = true 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/vivoAppStore/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 13 | private const val logTag = "DownloadUrlOpenerImpl" 14 | override fun getOpenDownloadUrlIntent( 15 | context: Context, 16 | remoteAppVersion: RemoteAppVersion 17 | ): Intent { 18 | 19 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 20 | 21 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 22 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 23 | } 24 | } 25 | 26 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 27 | 28 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 29 | 30 | 31 | try { 32 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 33 | try { 34 | /** 35 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 36 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 37 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 38 | */ 39 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 40 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 41 | } 42 | context.startActivity(openIntent) 43 | } catch (e: Exception) { 44 | CLog.logPrintStackTrace(e) 45 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 46 | } 47 | } catch (e: Exception) { 48 | CLog.logPrintStackTrace(e) 49 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/xiaomiGetApps/java/com/nll/helper/StoreConfigImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | 8 | object StoreConfigImpl : IStoreConfig { 9 | override fun openACRPhoneDownloadLink(context: Context, packageName: String) = try { 10 | Intent( 11 | Intent.ACTION_VIEW, 12 | Uri.parse("market://details?id=$packageName") 13 | ) 14 | .let(context::startActivity) 15 | true 16 | 17 | } catch (ignored: ActivityNotFoundException) { 18 | try { 19 | Intent( 20 | Intent.ACTION_VIEW, 21 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName") 22 | ) 23 | .let(context::startActivity) 24 | true 25 | } catch (e: ActivityNotFoundException) { 26 | e.printStackTrace() 27 | false 28 | } 29 | } 30 | 31 | override fun canLinkToWebSite() = false 32 | override fun canLinkToGooglePlayStore() = false 33 | override fun getUpdateCheckUrl() = "https://acr.app/version-xiaomi-get-apps.json" 34 | override fun requiresProminentPrivacyPolicyDisplay() = true 35 | override fun getPrivacyPolicyUrl()= "https://acr.app/policy.htm" 36 | } -------------------------------------------------------------------------------- /app/src/xiaomiGetApps/java/com/nll/helper/update/DownloadUrlOpenerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.nll.helper.update 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.nll.helper.R 8 | import com.nll.helper.recorder.CLog 9 | import com.nll.helper.update.contract.IDownloadUrlOpener 10 | import com.nll.helper.update.version.RemoteAppVersion 11 | 12 | object DownloadUrlOpenerImpl : IDownloadUrlOpener { 13 | private const val logTag = "DownloadUrlOpenerImpl" 14 | override fun getOpenDownloadUrlIntent( 15 | context: Context, 16 | remoteAppVersion: RemoteAppVersion 17 | ): Intent { 18 | 19 | CLog.log(logTag, "getOpenDownloadUrlIntent -> remoteAppVersion: $remoteAppVersion") 20 | 21 | return Intent(Intent.ACTION_VIEW, Uri.parse(remoteAppVersion.downloadUrl)).apply { 22 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 23 | } 24 | } 25 | 26 | override fun openDownloadUrl(context: Context, remoteAppVersion: RemoteAppVersion) { 27 | 28 | CLog.log(logTag, "openDownloadUrl -> remoteAppVersion: $remoteAppVersion") 29 | 30 | 31 | try { 32 | val urlToOpen = Uri.parse(remoteAppVersion.downloadUrl) 33 | try { 34 | /** 35 | * TODO Do we need FLAG_ACTIVITY_NEW_DOCUMENT 36 | * An activity that handles documents can use this attribute so that with every document you open you launch a separate instance of the same activity. 37 | * If you check your recent apps, then you will see various screens of the same activity of your app, each using a different document. 38 | */ 39 | val openIntent = Intent(Intent.ACTION_VIEW, urlToOpen).apply { 40 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 41 | } 42 | context.startActivity(openIntent) 43 | } catch (e: Exception) { 44 | CLog.logPrintStackTrace(e) 45 | Toast.makeText(context, R.string.no_url_handle, Toast.LENGTH_LONG).show() 46 | } 47 | } catch (e: Exception) { 48 | CLog.logPrintStackTrace(e) 49 | Toast.makeText(context, R.string.url_error, Toast.LENGTH_LONG).show() 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | commonValues = [ 4 | appBaseNameSpace : 'com.nll.helper', 5 | releaseMinifyEnabled : true, 6 | debugMinifyEnabled : true, 7 | releaseShrinkResourcesEnabled: false, 8 | debugShrinkResourcesEnabled : false 9 | ] 10 | 11 | } 12 | repositories { 13 | google() 14 | mavenCentral() 15 | 16 | } 17 | dependencies { 18 | classpath "com.android.tools.build:gradle:${libs.versions.androidGradleVersion.get()}" 19 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlinVersion.get()}" 20 | 21 | } 22 | 23 | 24 | } 25 | 26 | 27 | plugins { 28 | alias(libs.plugins.android.application) apply(false) 29 | alias(libs.plugins.android.library) apply(false) 30 | alias(libs.plugins.kotlin.android) apply(false) 31 | alias(libs.plugins.banes.versions) 32 | } 33 | 34 | allprojects { 35 | layout.buildDirectory = "${System.properties['user.home']}${File.separator}.build${File.separator}${rootProject.name}${File.separator}${project.name}" 36 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.viewbinding=true 25 | # Keep this as long as you can. R8 full mode broke many things 26 | android.enableR8.fullMode=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLLAPPS/ACRPhoneHelper/411741baf4d507ddab907d5cf26c17ea7fd7407b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 17 15:38:18 BST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven {url= "https://jitpack.io"} 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven {url= "https://jitpack.io"} 15 | } 16 | } 17 | rootProject.name = "ACRPhoneHelper" 18 | include ':app' --------------------------------------------------------------------------------