-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
- Loading branch information
0 parents
commit e6b1034
Showing
39 changed files
with
2,570 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
*.iml | ||
.gradle | ||
/local.properties | ||
/.idea/ | ||
.DS_Store | ||
/build | ||
/captures | ||
.externalNativeBuild | ||
.cxx | ||
local.properties |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
# Basic Call Recorder | ||
|
||
<img src="app/images/icon.svg" alt="app icon" width="72" /> | ||
|
||
BCR is a simple Android call recording app for rooted devices or devices running custom firmware. Once enabled, it stays out of the way and automatically records incoming and outgoing calls in the background. | ||
|
||
<img src="app/images/light.png" alt="light mode screenshot" width="200" /> <img src="app/images/dark.png" alt="dark mode screenshot" width="200" /> | ||
|
||
### Features | ||
|
||
* Supports Android 10 through 13 | ||
* Records FLAC-encoded lossless audio at the device's native sample rate | ||
* Supports Android's Storage Access Framework (can record to SD cards, USB devices, cloud storage, etc.) | ||
* Quick settings toggle | ||
* Material You dynamic theming | ||
* No persistent notification unless a recording is in progress | ||
* No network access permission | ||
* No third party dependencies | ||
|
||
### Non-features | ||
|
||
As the name alludes, BCR intends to be a basic as possible, with only two configuration options: an on/off switch and the output directory. The project will have succeeded at its goal if the only updates it ever needs are for compatibility with new Android versions. Thus, many potentially useful features will never be implemented, such as: | ||
|
||
* Automatic deletion of old recordings | ||
* Changing the filename format | ||
* Support for other lossless codecs | ||
* Support for lossy audio compression | ||
* Support for old Android versions (support is dropped as soon as maintenance becomes cumbersome) | ||
* Workarounds for [OEM-specific battery optimization and app killing behavior](https://dontkillmyapp.com/) | ||
* Workarounds for devices that don't support the [`VOICE_CALL` audio source](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL) (eg. using microphone + speakerphone) | ||
* Support for direct boot mode (the state before the device is initially unlocked after reboot) | ||
* Support for stock, unrooted firmware | ||
|
||
### Usage | ||
|
||
1. Download the latest version from the [releases page](https://github.com/chenxiaolong/BCR/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section. | ||
|
||
2. Install BCR as a system app. | ||
|
||
**For devices rooted with Magisk**, simply flash the zip as a Magisk module from within the Magisk app. | ||
|
||
**For unrooted custom firmware**, the files from the `system/` folder in the zip will need to be baked into the system image (or otherwise made available on the actual `/system` volume). | ||
|
||
3. Reboot and open BCR. | ||
|
||
4. Enable call recording and pick an output directory. If no output directory is selected or if the output directory is no longer accessible, then recordings will be saved to `/sdcard/Android/data/com.chiller3.bcr/files`. | ||
|
||
5. For future updates, either flash the new version of the Magisk module or simply extract the `.apk` from the zip file and install it directly. | ||
|
||
### How it works | ||
|
||
BCR relies heavily on system app permissions in order to function properly. This is primarily because of two permissions: | ||
|
||
* `CONTROL_INCALL_EXPERIENCE` | ||
|
||
This permission allows Android's telephony service to bind to BCR's `InCallService` without BCR being a wearable companion app, a car UI, or the default dialer. Once bound, the service will receive callbacks for call change events (eg. incoming call in the ringing state). This method is much more reliable than using the `READ_PHONE_STATE` permission and relying on `android.intent.action.PHONE_STATE` broadcasts. | ||
|
||
This method has a couple additional benefits. Due to the way that the telephony service binds to BCR's `InCallService`, the service can bring itself in and out of the foreground as needed when a call is in progress and access the audio stream without hitting Android 12+'s background microphone access limitations. It also does not require the service to be manually started from an `ACTION_BOOT_COMPLETED` broadcast receiver and thus is not affected by that broadcast's delays during initial boot. | ||
|
||
* `CAPTURE_AUDIO_OUTPUT` | ||
|
||
This permission is used to record from the `VOICE_CALL` audio stream. This stream, along with some others, like `VOICE_DOWNLINK` and `VOICE_UPLINK`, cannot be accessed without this system permission. | ||
|
||
With these two permissions, BCR can reliably detect phone calls and record from the call's audio stream. The recording process pulls PCM s16le raw audio at the device's native sample rate and uses `MediaCodec`'s builtin (software) FLAC encoder to create a losslessly compressed recording. | ||
|
||
### Verifying digital signatures | ||
|
||
Both the zip file and the APK contained within are digitally signed. | ||
|
||
To verify the signature of the zip file, first retrieve the public key: `2233C479609BDCEC43BE9232F6A3B19090EFF32C`. This is the same key used to sign the git tags in this repository. | ||
|
||
```bash | ||
gpg --recv-key 2233C479609BDCEC43BE9232F6A3B19090EFF32C | ||
``` | ||
|
||
Then, verify the signature of the zip file. | ||
|
||
```bash | ||
gpg --verify BCR-<version>-release.zip.asc BCR-<version>-release.zip | ||
``` | ||
|
||
The command output should include both `Good signature` and the GPG fingerprint listed above. | ||
|
||
To verify the signature of the APK, extract it from the zip and then run: | ||
|
||
``` | ||
apksigner verify --print-certs system/priv-app/com.chiller3.bcr/app-release.apk | ||
``` | ||
|
||
The SHA-256 digest of the APK signing certificate is: | ||
|
||
``` | ||
d16f9b375df668c58ef4bb855eae959713d6d02e45f7f2c05ce2c27ae944f4f9 | ||
``` | ||
|
||
### Building from source | ||
|
||
BCR can be built like most other Android apps using Android Studio or the gradle command line. | ||
|
||
To build the APK: | ||
|
||
```bash | ||
./gradlew assembleDebug | ||
``` | ||
|
||
To build the Magisk module zip (which automatically runs the `assembleDebug` task if needed): | ||
|
||
```bash | ||
./gradlew zipDebug | ||
``` | ||
|
||
The output file is written to `app/build/distributions/debug/`. The APK will be signed with the default autogenerated debug key. | ||
|
||
To create a release build with a specific signing key, set up the following environment variables: | ||
|
||
```bash | ||
export RELEASE_KEYSTORE=/path/to/keystore.jks | ||
export RELEASE_KEY_ALIAS=alias_name | ||
|
||
read -s RELEASE_KEYSTORE_PASSPHRASE | ||
read -s RELEASE_KEY_PASSPHRASE | ||
export RELEASE_KEYSTORE_PASSPHRASE | ||
export RELEASE_KEY_PASSPHRASE | ||
``` | ||
|
||
and then build the release zip: | ||
|
||
```bash | ||
./gradlew zipRelease | ||
``` | ||
|
||
### Contributing | ||
|
||
Bug fix and translation pull requests are welcome and much appreciated! | ||
|
||
If you are interested in implementing a new feature and would like to see it included in BCR, please open an issue to discuss it first. I intend for BCR to be as simple and low-maintenance as possible, so I am not too inclined to add any new features, but I could be convinced otherwise. | ||
|
||
### License | ||
|
||
BCR is licensed under GPLv3. Please see [`LICENSE`](./LICENSE) for the full license text. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
plugins { | ||
id("com.android.application") | ||
id("org.jetbrains.kotlin.android") | ||
} | ||
|
||
android { | ||
compileSdkPreview = "Tiramisu" | ||
|
||
defaultConfig { | ||
applicationId = "com.chiller3.bcr" | ||
minSdk = 29 | ||
targetSdkPreview = "Tiramisu" | ||
versionCode = 1 | ||
versionName = "1.0" | ||
|
||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
signingConfigs { | ||
create("release") { | ||
val keystore = System.getenv("RELEASE_KEYSTORE") | ||
storeFile = if (keystore != null) { File(keystore) } else { null } | ||
storePassword = System.getenv("RELEASE_KEYSTORE_PASSPHRASE") | ||
keyAlias = System.getenv("RELEASE_KEY_ALIAS") | ||
keyPassword = System.getenv("RELEASE_KEY_PASSPHRASE") | ||
} | ||
} | ||
buildTypes { | ||
getByName("release") { | ||
isMinifyEnabled = false | ||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") | ||
|
||
signingConfig = signingConfigs.getByName("release") | ||
} | ||
} | ||
compileOptions { | ||
sourceCompatibility(JavaVersion.VERSION_1_8) | ||
targetCompatibility(JavaVersion.VERSION_1_8) | ||
} | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation("androidx.activity:activity-ktx:1.4.0") | ||
implementation("androidx.appcompat:appcompat:1.4.1") | ||
implementation("androidx.core:core-ktx:1.7.0") | ||
implementation("androidx.documentfile:documentfile:1.0.1") | ||
implementation("androidx.fragment:fragment-ktx:1.4.1") | ||
implementation("androidx.preference:preference-ktx:1.2.0") | ||
implementation("com.google.android.material:material:1.5.0") | ||
testImplementation("junit:junit:4.13.2") | ||
androidTestImplementation("androidx.test.ext:junit:1.1.3") | ||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") | ||
} | ||
|
||
android.applicationVariants.all { | ||
val variant = this | ||
val capitalized = variant.name.capitalize() | ||
val extraDir = File(buildDir, "extra") | ||
val variantDir = File(extraDir, variant.name) | ||
|
||
val moduleProp = tasks.register("moduleProp${capitalized}") { | ||
val outputFile = File(variantDir, "module.prop") | ||
outputs.file(outputFile) | ||
|
||
doLast { | ||
outputFile.writeText(""" | ||
id=${variant.applicationId} | ||
name=Basic Call Recorder | ||
version=v${variant.versionName} | ||
versionCode=${variant.versionCode} | ||
author=chenxiaolong | ||
description=Basic Call Recorder | ||
""".trimIndent()) | ||
} | ||
} | ||
|
||
val permissionsXml = tasks.register("permissionsXml${capitalized}") { | ||
val outputFile = File(variantDir, "privapp-permissions-${variant.applicationId}.xml") | ||
outputs.file(outputFile) | ||
|
||
doLast { | ||
outputFile.writeText(""" | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<permissions> | ||
<privapp-permissions package="${variant.applicationId}"> | ||
<permission name="android.permission.CAPTURE_AUDIO_OUTPUT" /> | ||
<permission name="android.permission.CONTROL_INCALL_EXPERIENCE" /> | ||
</privapp-permissions> | ||
</permissions> | ||
""".trimIndent()) | ||
} | ||
} | ||
|
||
tasks.register<Zip>("zip${capitalized}") { | ||
archiveFileName.set("BCR-${variant.versionName}-${variant.name}.zip") | ||
destinationDirectory.set(File(destinationDirectory.asFile.get(), variant.name)) | ||
|
||
// Make the zip byte-for-byte reproducible (note that the APK is still not reproducible) | ||
isPreserveFileTimestamps = false | ||
isReproducibleFileOrder = true | ||
|
||
dependsOn.add(variant.assembleProvider) | ||
|
||
from(moduleProp.get().outputs) | ||
from(permissionsXml.get().outputs) { | ||
into("system/etc/permissions") | ||
} | ||
from(variant.outputs.map { it.outputFile }) { | ||
into("system/priv-app/${variant.applicationId}") | ||
} | ||
|
||
val magiskDir = File(projectDir, "magisk") | ||
from(magiskDir) { | ||
into("META-INF/com/google/android") | ||
} | ||
|
||
from(File(rootDir, "LICENSE")) | ||
from(File(rootDir, "README.md")) | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
#!/sbin/sh | ||
|
||
################# | ||
# Initialization | ||
################# | ||
|
||
umask 022 | ||
|
||
# echo before loading util_functions | ||
ui_print() { echo "$1"; } | ||
|
||
require_new_magisk() { | ||
ui_print "*******************************" | ||
ui_print " Please install Magisk v20.4+! " | ||
ui_print "*******************************" | ||
exit 1 | ||
} | ||
|
||
######################### | ||
# Load util_functions.sh | ||
######################### | ||
|
||
OUTFD=$2 | ||
ZIPFILE=$3 | ||
|
||
mount /data 2>/dev/null | ||
|
||
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk | ||
. /data/adb/magisk/util_functions.sh | ||
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk | ||
|
||
install_module | ||
exit 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#MAGISK |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle.kts. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
24 changes: 24 additions & 0 deletions
24
app/src/androidTest/java/com/chiller3/bcr/ExampleInstrumentedTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.chiller3.bcr | ||
|
||
import androidx.test.platform.app.InstrumentationRegistry | ||
import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
|
||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
|
||
import org.junit.Assert.* | ||
|
||
/** | ||
* Instrumented test, which will execute on an Android device. | ||
* | ||
* See [testing documentation](http://d.android.com/tools/testing). | ||
*/ | ||
@RunWith(AndroidJUnit4::class) | ||
class ExampleInstrumentedTest { | ||
@Test | ||
fun useAppContext() { | ||
// Context of the app under test. | ||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext | ||
assertEquals("com.chiller3.bcr", appContext.packageName) | ||
} | ||
} |
Oops, something went wrong.