├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── font
│ │ │ │ └── brandy.ttf
│ │ │ ├── drawable
│ │ │ │ ├── empty_logo.png
│ │ │ │ ├── header_bg_day.jpg
│ │ │ │ ├── nav_header_img.png
│ │ │ │ ├── header_bg_night.jpg
│ │ │ │ ├── cat_listening_music.png
│ │ │ │ ├── shape_for_dialog_box.xml
│ │ │ │ ├── border_style.xml
│ │ │ │ ├── done_24.xml
│ │ │ │ ├── arrow_upward.xml
│ │ │ │ ├── arrow_downward.xml
│ │ │ │ ├── delete_24.xml
│ │ │ │ ├── arrow_back_24.xml
│ │ │ │ ├── info.xml
│ │ │ │ ├── play_arrow_24.xml
│ │ │ │ ├── error.xml
│ │ │ │ ├── play_circle_24.xml
│ │ │ │ ├── stop_circle_24.xml
│ │ │ │ ├── pause_24.xml
│ │ │ │ ├── done_icon.xml
│ │ │ │ ├── mode_edit_24.xml
│ │ │ │ ├── keyboard_voice.xml
│ │ │ │ ├── search_24.xml
│ │ │ │ ├── update_24.xml
│ │ │ │ ├── round_transparent_ripple_button.xml
│ │ │ │ ├── select_all_24.xml
│ │ │ │ ├── deselect_24.xml
│ │ │ │ ├── share_24.xml
│ │ │ │ ├── skip_backward.xml
│ │ │ │ ├── skip_forward.xml
│ │ │ │ ├── facebook.xml
│ │ │ │ ├── settings_24.xml
│ │ │ │ ├── telegram.xml
│ │ │ │ ├── github.xml
│ │ │ │ ├── donation.xml
│ │ │ │ └── browser_icon.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── xml
│ │ │ │ ├── provider_paths.xml
│ │ │ │ ├── shortcuts.xml
│ │ │ │ └── root_preferences.xml
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── custom_styles.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── attr.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── settings_activity.xml
│ │ │ │ ├── toolbar.xml
│ │ │ │ ├── sample_recording_item_design.xml
│ │ │ │ ├── dialog_rename_file.xml
│ │ │ │ ├── header_layout.xml
│ │ │ │ ├── dialog_multiple_selected_files_options.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── dialog_single_file_options.xml
│ │ │ │ ├── dialog_audio_player.xml
│ │ │ │ └── dialog_file_properties.xml
│ │ │ ├── menu
│ │ │ │ ├── speed_menu.xml
│ │ │ │ ├── action_bar_menu.xml
│ │ │ │ └── nav_drawer_menu.xml
│ │ │ └── values-night
│ │ │ │ └── themes.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── zoro
│ │ │ │ └── benojir
│ │ │ │ └── callrecorder
│ │ │ │ ├── receivers
│ │ │ │ └── ActionReceiver.java
│ │ │ │ ├── helpers
│ │ │ │ ├── ContactsHelper.java
│ │ │ │ ├── RecorderHelper.java
│ │ │ │ ├── SingleFileOptionsHelper.java
│ │ │ │ └── CustomFunctions.java
│ │ │ │ ├── dialogs
│ │ │ │ ├── MultipleFilesControlDialog.java
│ │ │ │ ├── SingleFileControlDialog.java
│ │ │ │ ├── FileInfoDialog.java
│ │ │ │ └── AudioPlayerDialog.java
│ │ │ │ ├── activities
│ │ │ │ └── SettingsActivity.java
│ │ │ │ └── services
│ │ │ │ └── MyInCallService.java
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── zoro
│ │ │ └── benojir
│ │ │ └── callrecorder
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── zoro
│ │ └── benojir
│ │ └── callrecorder
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── .idea
├── .name
├── .gitignore
├── compiler.xml
├── vcs.xml
├── render.experimental.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
└── other.xml
├── magisk
├── updater-script
├── updates
│ └── release
│ │ ├── changelog.txt
│ │ └── info.json
└── update-binary
├── magisk-module
├── META-INF
│ └── com
│ │ └── google
│ │ └── android
│ │ ├── updater-script
│ │ └── update-binary
├── system
│ ├── priv-app
│ │ └── zoro.benojir.callrecorder
│ │ │ └── app-release.apk
│ ├── etc
│ │ └── permissions
│ │ │ └── privapp-permissions-zoro.benojir.callrecorder.xml
│ └── addon.d
│ │ └── 51-zoro.benojir.callrecorder.sh
└── module.prop
├── README.md
├── screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Call Recorder
--------------------------------------------------------------------------------
/magisk/updater-script:
--------------------------------------------------------------------------------
1 | #MAGISK
2 |
--------------------------------------------------------------------------------
/magisk-module/META-INF/com/google/android/updater-script:
--------------------------------------------------------------------------------
1 | #MAGISK
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/README.md
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/screenshots/6.png
--------------------------------------------------------------------------------
/magisk/updates/release/changelog.txt:
--------------------------------------------------------------------------------
1 | ### Version 1.5
2 |
3 | * Fixed known issues
4 | * Updated UI to material 3
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/font/brandy.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/font/brandy.ttf
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/empty_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/drawable/empty_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/header_bg_day.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/drawable/header_bg_day.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/nav_header_img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/drawable/nav_header_img.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/header_bg_night.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/drawable/header_bg_night.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cat_listening_music.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/drawable/cat_listening_music.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FC2258
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/magisk-module/system/priv-app/zoro.benojir.callrecorder/app-release.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/HEAD/magisk-module/system/priv-app/zoro.benojir.callrecorder/app-release.apk
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Mar 26 11:04:25 IST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_for_dialog_box.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
8 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/magisk-module/module.prop:
--------------------------------------------------------------------------------
1 | id=zoro.benojir.callrecorder
2 | name=Call Recorder
3 | version=v1.5
4 | versionCode=5
5 | author=Benojir Sultana
6 | description=Record calls automatically without announcement.
7 | updateJson=https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/main/magisk/updates/release/info.json
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/magisk/updates/release/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "zipUrl": "https://github.com/Benojir/Android-Call-Recorder/releases/download/1.5/Call-Recorder_v1.5-Magisk.zip",
3 | "changelog": "https://github.com/Benojir/Android-Call-Recorder/raw/master/magisk/updates/release/changelog.txt",
4 | "version": "1.5",
5 | "versionCode": 5
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | zzz.bat
17 | app/release
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/border_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/custom_styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 20dp
4 | 20dp
5 | 10dp
6 | 5dp
7 | 10dp
8 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/done_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_upward.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_downward.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/magisk-module/system/etc/permissions/privapp-permissions-zoro.benojir.callrecorder.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/delete_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/magisk-module/system/addon.d/51-zoro.benojir.callrecorder.sh:
--------------------------------------------------------------------------------
1 | #!/sbin/sh
2 | # ADDOND_VERSION=2
3 |
4 | . /tmp/backuptool.functions
5 |
6 | files="priv-app/zoro.benojir.callrecorder/app-release.apk etc/permissions/privapp-permissions-zoro.benojir.callrecorder.xml"
7 |
8 | case "${1}" in
9 | backup|restore)
10 | for f in ${files}; do
11 | "${1}_file" "${S}/${f}"
12 | done
13 | ;;
14 | esac
--------------------------------------------------------------------------------
/app/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Call Recorder"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/info.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/play_arrow_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/error.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/play_circle_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/stop_circle_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/pause_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/done_icon.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mode_edit_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/test/java/zoro/benojir/callrecorder/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/keyboard_voice.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/settings_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/search_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/update_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_transparent_ripple_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/select_all_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/speed_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/receivers/ActionReceiver.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.receivers;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 |
8 | import zoro.benojir.callrecorder.services.MyInCallService;
9 |
10 | public class ActionReceiver extends BroadcastReceiver {
11 |
12 | @Override
13 | public void onReceive(Context context, Intent intent) {
14 |
15 | Bundle intentExtras = intent.getExtras();
16 |
17 | if (intentExtras.containsKey("stopRecording")){
18 | context.stopService(new Intent(context, MyInCallService.class));
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/deselect_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF3700B3
4 | #FF03DAC5
5 | #04BAA8
6 | #FF018786
7 | #FF000000
8 | #FFFFFFFF
9 |
10 | #007ADC
11 | #1E1E1E
12 |
13 | #F1D8DF
14 | #FFC107
15 | #3E3D3D
16 | #888888
17 | #2196F3
18 | #3AA8FF
19 | #00C708
20 | #E0DEDE
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - Device default
5 | - Dark mode
6 | - Light mode
7 |
8 |
9 |
10 | - device_default
11 | - dark_mode
12 | - light_mode
13 |
14 |
15 |
16 | - Newest
17 | - Oldest
18 | - A-Z (Ascending)
19 | - Z-A (Descending)
20 |
21 |
22 |
23 | - sort_by_new
24 | - sort_by_old
25 | - sort_by_name_ascending
26 | - sort_by_name_descending
27 |
28 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/zoro/benojir/callrecorder/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("com.hiddenpirates.callrecorder", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/action_bar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/skip_backward.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/skip_forward.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/facebook.xml:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/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 | # Please add these rules to your existing keep rules in order to suppress warnings.
24 | # This is generated automatically by the Android Gradle plugin.
25 | -dontwarn javax.annotation.Nullable
26 | -dontwarn javax.annotation.ParametersAreNonnullByDefault
27 | -dontwarn javax.annotation.WillClose
--------------------------------------------------------------------------------
/app/src/main/res/xml/shortcuts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
13 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/magisk/update-binary:
--------------------------------------------------------------------------------
1 | #!/sbin/sh
2 |
3 | OUTFD=${2}
4 | ZIPFILE=${3}
5 |
6 | umask 022
7 |
8 | ui_print() {
9 | printf "ui_print %s\nui_print\n" "${*}" > /proc/self/fd/"${OUTFD}"
10 | }
11 |
12 | if [ -f /sbin/recovery ] || [ -f /system/bin/recovery ]; then
13 | # Installing via recovery. Always do a direct install.
14 | set -exu
15 |
16 | ui_print 'Mounting system'
17 | if mount /system_root; then
18 | mount -o remount,rw /system_root
19 | root_dir=/system_root
20 | else
21 | mount /system
22 | mount -o remount,rw /system
23 | root_dir=/
24 | fi
25 |
26 | ui_print 'Extracting files'
27 |
28 | # Just overwriting isn't sufficient because the apk filenames are different
29 | # between debug and release builds
30 | app_id=$(unzip -p "${ZIPFILE}" module.prop | grep '^id=' | cut -d= -f2)
31 |
32 | # rm on some custom recoveries doesn't exit with 0 on ENOENT, even with -f
33 | rm -rf "${root_dir}/system/priv-app/${app_id}" || :
34 |
35 | unzip -o "${ZIPFILE}" 'system/*' -d "${root_dir}"
36 |
37 | ui_print 'Done!'
38 | else
39 | # Installing via Magisk Manager.
40 |
41 | . /data/adb/magisk/util_functions.sh
42 |
43 | install_module
44 | fi
45 |
--------------------------------------------------------------------------------
/magisk-module/META-INF/com/google/android/update-binary:
--------------------------------------------------------------------------------
1 | #!/sbin/sh
2 |
3 | OUTFD=${2}
4 | ZIPFILE=${3}
5 |
6 | umask 022
7 |
8 | ui_print() {
9 | printf "ui_print %s\nui_print\n" "${*}" > /proc/self/fd/"${OUTFD}"
10 | }
11 |
12 | if [ -f /sbin/recovery ] || [ -f /system/bin/recovery ]; then
13 | # Installing via recovery. Always do a direct install.
14 | set -exu
15 |
16 | ui_print 'Mounting system'
17 | if mount /system_root; then
18 | mount -o remount,rw /system_root
19 | root_dir=/system_root
20 | else
21 | mount /system
22 | mount -o remount,rw /system
23 | root_dir=/
24 | fi
25 |
26 | ui_print 'Extracting files'
27 |
28 | # Just overwriting isn't sufficient because the apk filenames are different
29 | # between debug and release builds
30 | app_id=$(unzip -p "${ZIPFILE}" module.prop | grep '^id=' | cut -d= -f2)
31 |
32 | # rm on some custom recoveries doesn't exit with 0 on ENOENT, even with -f
33 | rm -rf "${root_dir}/system/priv-app/${app_id}" || :
34 |
35 | unzip -o "${ZIPFILE}" 'system/*' -d "${root_dir}"
36 |
37 | ui_print 'Done!'
38 | else
39 | # Installing via Magisk Manager.
40 |
41 | . /data/adb/magisk/util_functions.sh
42 |
43 | install_module
44 | fi
45 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
25 |
--------------------------------------------------------------------------------
/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 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
22 | android.defaults.buildfeatures.buildconfig=true
23 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/app/src/main/res/drawable/telegram.xml:
--------------------------------------------------------------------------------
1 |
7 |
13 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdk 34
7 |
8 | defaultConfig {
9 | applicationId "zoro.benojir.callrecorder"
10 | minSdk 29
11 | //noinspection OldTargetApi
12 | targetSdk 34
13 | versionCode 5
14 | versionName "1.5"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled true
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | buildFeatures {
30 | viewBinding true
31 | buildConfig true
32 | }
33 | namespace 'zoro.benojir.callrecorder'
34 | }
35 |
36 | dependencies {
37 |
38 | implementation 'androidx.appcompat:appcompat:1.7.0'
39 | implementation 'com.google.android.material:material:1.12.0'
40 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
41 | implementation 'androidx.preference:preference:1.2.1'
42 | testImplementation 'junit:junit:4.13.2'
43 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
45 |
46 | implementation 'org.jsoup:jsoup:1.16.1'
47 | implementation 'commons-io:commons-io:2.13.0'
48 |
49 | implementation 'androidx.media3:media3-exoplayer:1.4.0'
50 | implementation 'androidx.media3:media3-exoplayer-dash:1.4.0'
51 | implementation 'androidx.media3:media3-ui:1.4.0'
52 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/nav_drawer_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/root_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/helpers/ContactsHelper.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.helpers;
2 |
3 | import android.Manifest;
4 | import android.content.Context;
5 | import android.content.pm.PackageManager;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.provider.ContactsContract;
9 | import android.telephony.PhoneNumberUtils;
10 | import android.widget.Toast;
11 |
12 | import androidx.core.content.ContextCompat;
13 |
14 | public class ContactsHelper {
15 |
16 | public static String getContactNameByPhoneNumber(Context context, String phoneNumber) {
17 | String contactName = "";
18 |
19 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
20 |
21 | Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
22 | String[] projection = new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER};
23 |
24 | Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
25 |
26 | if (cursor != null) {
27 | while (cursor.moveToNext()) {
28 | String storedNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
29 | if (PhoneNumberUtils.compare(storedNumber, phoneNumber)) {
30 | contactName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
31 | break;
32 | }
33 | }
34 | cursor.close();
35 | }
36 | } else {
37 | Toast.makeText(context, "Please grant contacts permission", Toast.LENGTH_SHORT).show();
38 | }
39 |
40 | if (contactName.isEmpty()){
41 | contactName = "Unknown";
42 | }
43 |
44 | return contactName;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/sample_recording_item_design.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
21 |
22 |
31 |
32 |
39 |
40 |
41 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_rename_file.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
20 |
21 |
34 |
35 |
43 |
44 |
51 |
52 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/dialogs/MultipleFilesControlDialog.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.dialogs;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.view.Window;
6 | import android.view.WindowManager;
7 | import android.widget.TextView;
8 |
9 | import java.util.ArrayList;
10 |
11 | import zoro.benojir.callrecorder.R;
12 | import zoro.benojir.callrecorder.adapters.RecordingsListAdapter;
13 |
14 | public class MultipleFilesControlDialog {
15 | private final Dialog dialog;
16 | private final ArrayList selectedItemsPositionsList;
17 | private final TextView selectAllOption, deselectAllOption, shareAllOption, deleteAllOption;
18 |
19 | public MultipleFilesControlDialog(Activity activity, ArrayList selectedItemsPositionsList) {
20 | this.selectedItemsPositionsList = selectedItemsPositionsList;
21 |
22 | dialog = new Dialog(activity);
23 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
24 | dialog.setCancelable(true);
25 | dialog.setContentView(R.layout.dialog_multiple_selected_files_options);
26 | dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
27 |
28 | selectAllOption = dialog.findViewById(R.id.selectAllOption);
29 | deselectAllOption = dialog.findViewById(R.id.deselectAllOption);
30 | shareAllOption = dialog.findViewById(R.id.shareAllOption);
31 | deleteAllOption = dialog.findViewById(R.id.deleteAllOption);
32 | }
33 |
34 | public void show(RecordingsListAdapter.OnMultipleItemsLongClickListener listener) {
35 | actionsOnDialog(listener);
36 | dialog.show();
37 | }
38 |
39 | private void actionsOnDialog(RecordingsListAdapter.OnMultipleItemsLongClickListener listener) {
40 | selectAllOption.setOnClickListener(view -> {
41 | listener.onSelectAllOptionClicked();
42 | dialog.dismiss();
43 | });
44 |
45 | deselectAllOption.setOnClickListener(view -> {
46 | listener.onDeselectAllOptionClicked(selectedItemsPositionsList);
47 | dialog.dismiss();
48 | });
49 |
50 | shareAllOption.setOnClickListener(view -> {
51 | listener.onShareAllOptionClicked(selectedItemsPositionsList);
52 | dialog.dismiss();
53 | });
54 |
55 | deleteAllOption.setOnClickListener(view -> {
56 | listener.onDeleteAllOptionClicked();
57 | dialog.dismiss();
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/activities/SettingsActivity.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.activities;
2 |
3 | import android.content.SharedPreferences;
4 | import android.os.Bundle;
5 |
6 | import androidx.appcompat.app.AppCompatActivity;
7 | import androidx.appcompat.app.AppCompatDelegate;
8 | import androidx.appcompat.content.res.AppCompatResources;
9 | import androidx.preference.ListPreference;
10 | import androidx.preference.PreferenceFragmentCompat;
11 | import androidx.preference.PreferenceManager;
12 |
13 | import com.google.android.material.appbar.MaterialToolbar;
14 |
15 | import zoro.benojir.callrecorder.R;
16 |
17 | public class SettingsActivity extends AppCompatActivity {
18 |
19 | @Override
20 | protected void onCreate(Bundle savedInstanceState) {
21 | super.onCreate(savedInstanceState);
22 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
23 |
24 | if (preferences.getString("appearance", "device_default").equals("dark_mode")) {
25 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
26 | } else if (preferences.getString("appearance", "device_default").equals("light_mode")) {
27 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
28 | } else {
29 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
30 | }
31 | setContentView(R.layout.settings_activity);
32 | if (savedInstanceState == null) {
33 | getSupportFragmentManager()
34 | .beginTransaction()
35 | .replace(R.id.settings, new SettingsFragment())
36 | .commit();
37 | }
38 |
39 | MaterialToolbar toolbar = findViewById(R.id.toolbar_include);
40 | setSupportActionBar(toolbar);
41 | if (getSupportActionBar() != null) {
42 | getSupportActionBar().setTitle("Settings");
43 | }
44 | toolbar.setNavigationIcon(AppCompatResources.getDrawable(this, R.drawable.arrow_back_24));
45 | toolbar.setNavigationOnClickListener(v -> onBackPressed());
46 | }
47 |
48 | public static class SettingsFragment extends PreferenceFragmentCompat {
49 | @Override
50 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
51 | setPreferencesFromResource(R.xml.root_preferences, rootKey);
52 |
53 | // Add a listener to restart the activity when the theme is changed
54 | ListPreference appearancePreference = findPreference("appearance");
55 | if (appearancePreference != null) {
56 | appearancePreference.setOnPreferenceChangeListener((preference, newValue) -> {
57 | getActivity().recreate();
58 | return true;
59 | });
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/header_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
24 |
25 |
38 |
39 |
46 |
47 |
57 |
58 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_multiple_selected_files_options.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
24 |
25 |
40 |
41 |
56 |
57 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/dialogs/SingleFileControlDialog.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.dialogs;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.view.Window;
6 | import android.view.WindowManager;
7 | import android.widget.TextView;
8 |
9 | import org.json.JSONException;
10 | import org.json.JSONObject;
11 |
12 | import zoro.benojir.callrecorder.R;
13 | import zoro.benojir.callrecorder.adapters.RecordingsListAdapter;
14 |
15 | public class SingleFileControlDialog {
16 |
17 | private final JSONObject fileInfo;
18 | private final Dialog dialog;
19 | private final TextView fileNameTV, selectOption, playOption, shareOption, renameOption, deleteOption, fileInfoOption;
20 | private final int itemPosition;
21 |
22 | public SingleFileControlDialog(Activity activity, JSONObject fileInfo, int position) {
23 | this.fileInfo = fileInfo;
24 | this.itemPosition = position;
25 |
26 | dialog = new Dialog(activity);
27 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
28 | dialog.setCancelable(true);
29 | dialog.setContentView(R.layout.dialog_single_file_options);
30 | dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
31 |
32 | fileNameTV = dialog.findViewById(R.id.fileNameTV);
33 |
34 | selectOption = dialog.findViewById(R.id.selectOption);
35 | playOption = dialog.findViewById(R.id.playOption);
36 | shareOption = dialog.findViewById(R.id.shareOption);
37 | renameOption = dialog.findViewById(R.id.renameOption);
38 | deleteOption = dialog.findViewById(R.id.deleteOption);
39 | fileInfoOption = dialog.findViewById(R.id.showFileInfoOption);
40 | }
41 |
42 | public void show(RecordingsListAdapter.OnSingleItemLongClickListener singleItemLongClickListener) throws JSONException {
43 | String fileName = fileInfo.getString("file_name");
44 | fileNameTV.setText(fileName);
45 | actionsOnDialog(singleItemLongClickListener);
46 | dialog.show();
47 | }
48 |
49 | private void actionsOnDialog(RecordingsListAdapter.OnSingleItemLongClickListener singleItemLongClickListener) {
50 |
51 | selectOption.setOnClickListener(view -> {
52 | singleItemLongClickListener.onSelectOptionClicked(itemPosition);
53 | dialog.dismiss();
54 | });
55 |
56 | playOption.setOnClickListener(view -> {
57 | singleItemLongClickListener.onPlayWithOptionClicked(itemPosition);
58 | dialog.dismiss();
59 | });
60 |
61 | shareOption.setOnClickListener(view -> {
62 | singleItemLongClickListener.onShareOptionClicked(itemPosition);
63 | dialog.dismiss();
64 | });
65 |
66 | renameOption.setOnClickListener(view -> {
67 | singleItemLongClickListener.onRenameOptionClicked(itemPosition);
68 | dialog.dismiss();
69 | });
70 |
71 | deleteOption.setOnClickListener(view -> {
72 | singleItemLongClickListener.onDeleteOptionClicked(itemPosition);
73 | dialog.dismiss();
74 | });
75 |
76 | fileInfoOption.setOnClickListener(view -> {
77 | singleItemLongClickListener.onShowFileInfoOptionClicked(itemPosition);
78 | dialog.dismiss();
79 | });
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Call Recorder
3 | App is not a system app.
4 | After Android 9 or greater, it is not possible to record calls by using third party apps. So that\'s why you have to manually make this app as a system app. If you don\'t know how to make a third party app to system app, then you should read this tutorial post.
5 | https://www.droidwin.com/how-to-convert-any-android-app-to-system-app/
6 | Call Recorder
7 | "Best automatic call recorder ever. Download it from GitHub now. "
8 | Something went wrong. Please report this issue.
9 | https://github.com/Benojir/Android-Call-Recorder/releases
10 | https://github.com/Benojir/Android-Call-Recorder/issues/
11 | https://play.google.com/store/search?q=pub: Creative Papa&c=apps
12 | https://benojir.github.io/portfolio
13 | https://github.com/Benojir
14 | https://telegram.me/CreativePapa
15 | Failed to open application setting info page. Please open app info page and allow all permissions manually from settings.
16 | Select
17 | Play with
18 | Share
19 | Rename
20 | Delete
21 | Info
22 | Properties
23 | Select All
24 | Deselect All
25 | Share
26 | Delete
27 | Unknown
28 | "Version: "
29 | SettingsActivity
30 | Call recording
31 | Calls will be recorded automatically
32 | Calls will not be recorded automatically
33 | General
34 | Start toast
35 | A toast will be shown when recording will be started
36 | No toast will be shown when recording started
37 | Saved toast
38 | A toast will be shown when recording saved
39 | No toast will be shown when recording saved
40 | Appearance
41 | Theme
42 | Sort by
43 | Recorder
44 | https://raw.githubusercontent.com/Benojir/Android-Call-Recorder/main/magisk/updates/release/info.json
45 | Settings
46 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
35 |
39 |
40 |
45 |
48 |
49 |
50 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/services/MyInCallService.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.services;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.content.Context;
7 | import android.content.SharedPreferences;
8 | import android.content.pm.ServiceInfo;
9 | import android.os.Build;
10 | import android.telecom.Call;
11 | import android.telecom.InCallService;
12 | import android.util.Log;
13 |
14 | import androidx.core.app.NotificationCompat;
15 | import androidx.preference.PreferenceManager;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 |
20 | import zoro.benojir.callrecorder.BuildConfig;
21 | import zoro.benojir.callrecorder.R;
22 | import zoro.benojir.callrecorder.helpers.RecorderHelper;
23 |
24 | public class MyInCallService extends InCallService {
25 |
26 | public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID;
27 | private final Map activeRecorders = new HashMap<>();
28 | private Context sContext;
29 |
30 | @Override
31 | public void onCallAdded(Call call) {
32 | super.onCallAdded(call);
33 | sContext = this;
34 |
35 | call.registerCallback(new Call.Callback() {
36 | @Override
37 | public void onStateChanged(Call call, int state) {
38 | super.onStateChanged(call, state);
39 | Log.d("MyInCallService", "Call state changed: " + state);
40 |
41 | if (state == Call.STATE_ACTIVE) {
42 | startRecording(call);
43 | } else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
44 | stopRecording(call);
45 | }
46 | }
47 | });
48 | }
49 |
50 | @Override
51 | public void onCallRemoved(Call call) {
52 | super.onCallRemoved(call);
53 | stopRecording(call);
54 | }
55 |
56 | private void startRecording(Call call) {
57 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(sContext);
58 | if (preferences.getBoolean("is_call_recording_enabled", false)) {
59 | createNotificationChannel();
60 |
61 | NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(sContext, CHANNEL_ID)
62 | .setContentTitle("Call Recorder")
63 | .setContentText("Call recording in progress")
64 | .setSmallIcon(R.drawable.keyboard_voice)
65 | .setOngoing(true);
66 |
67 | Notification notification = notificationBuilder.build();
68 |
69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
70 | startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
71 | } else {
72 | startForeground(1, notification);
73 | }
74 |
75 | RecorderHelper recorderHelper = new RecorderHelper(sContext, call.getDetails().getHandle().getSchemeSpecificPart());
76 | recorderHelper.startRecoding();
77 | activeRecorders.put(call, recorderHelper);
78 | }
79 | }
80 |
81 | private void stopRecording(Call call) {
82 | RecorderHelper recorderHelper = activeRecorders.remove(call);
83 | if (recorderHelper != null) {
84 | recorderHelper.stopVoiceRecoding();
85 | }
86 |
87 | if (activeRecorders.isEmpty()) {
88 | stopForeground(true);
89 | }
90 | }
91 |
92 | private void createNotificationChannel() {
93 | NotificationChannel serviceChannel = new NotificationChannel(
94 | CHANNEL_ID,
95 | "Call Recorder Service Channel",
96 | NotificationManager.IMPORTANCE_NONE
97 | );
98 | NotificationManager manager = getSystemService(NotificationManager.class);
99 | manager.createNotificationChannel(serviceChannel);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/dialogs/FileInfoDialog.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.dialogs;
2 |
3 | import android.app.Dialog;
4 | import android.content.Context;
5 | import android.net.Uri;
6 | import android.view.Window;
7 | import android.view.WindowManager;
8 | import android.widget.TextView;
9 | import android.widget.Toast;
10 |
11 | import androidx.annotation.NonNull;
12 | import androidx.media3.common.MediaItem;
13 | import androidx.media3.common.PlaybackException;
14 | import androidx.media3.common.Player;
15 | import androidx.media3.exoplayer.ExoPlayer;
16 |
17 | import org.json.JSONException;
18 | import org.json.JSONObject;
19 |
20 | import java.io.File;
21 |
22 | import zoro.benojir.callrecorder.R;
23 | import zoro.benojir.callrecorder.helpers.CustomFunctions;
24 |
25 | public class FileInfoDialog {
26 |
27 | private final Context context;
28 |
29 | public FileInfoDialog(Context context, JSONObject fileInfoJObj) {
30 | this.context = context;
31 |
32 | final Dialog dialog = new Dialog(context);
33 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
34 | dialog.setCancelable(true);
35 | dialog.setContentView(R.layout.dialog_file_properties);
36 | dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
37 |
38 | TextView fileSizeTV, fileLastModifiedTV, fileNameTV, filePathTV, fileDurationTV;
39 |
40 | fileSizeTV = dialog.findViewById(R.id.fileSizeTV);
41 | fileLastModifiedTV = dialog.findViewById(R.id.lastModifiedTV);
42 | fileNameTV = dialog.findViewById(R.id.fileNameTV);
43 | filePathTV = dialog.findViewById(R.id.filePathTV);
44 | fileDurationTV = dialog.findViewById(R.id.callDurationTV);
45 |
46 | try {
47 | fileNameTV.setText(fileInfoJObj.getString("file_name"));
48 | fileSizeTV.setText(fileInfoJObj.get("size").toString());
49 | fileLastModifiedTV.setText(fileInfoJObj.getString("modified_date"));
50 | filePathTV.setText(fileInfoJObj.getString("absolute_path"));
51 |
52 | // Use ExoPlayer to get the duration
53 | File audioFile = new File(fileInfoJObj.getString("absolute_path"));
54 | getAudioDuration(audioFile, fileDurationTV);
55 |
56 | fileNameTV.setOnClickListener(view -> CustomFunctions.copyTextToClipboard(context, fileNameTV.getText().toString()));
57 | filePathTV.setOnClickListener(view -> CustomFunctions.copyTextToClipboard(context, filePathTV.getText().toString()));
58 | }
59 | catch (JSONException e) {
60 | dialog.dismiss();
61 | Toast.makeText(context, context.getString(R.string.report_issue_text), Toast.LENGTH_SHORT).show();
62 | }
63 |
64 | dialog.show();
65 | }
66 |
67 | private void getAudioDuration(File audioFile, TextView fileDurationTV) {
68 | ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
69 | MediaItem mediaItem = MediaItem.fromUri(Uri.fromFile(audioFile));
70 | exoPlayer.setMediaItem(mediaItem);
71 | exoPlayer.prepare();
72 |
73 | exoPlayer.addListener(new Player.Listener() {
74 | @Override
75 | public void onPlaybackStateChanged(int playbackState) {
76 | if (playbackState == Player.STATE_READY) {
77 | long duration = exoPlayer.getDuration();
78 | fileDurationTV.setText(CustomFunctions.formatDuration(duration));
79 | exoPlayer.release();
80 | } else if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {
81 | fileDurationTV.setText(context.getString(R.string.unknown));
82 | exoPlayer.release();
83 | }
84 | }
85 |
86 | @Override
87 | public void onPlayerError(@NonNull PlaybackException error) {
88 | fileDurationTV.setText(context.getString(R.string.unknown));
89 | exoPlayer.release();
90 | }
91 | });
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/helpers/RecorderHelper.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.helpers;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.media.MediaRecorder;
6 | import android.os.Handler;
7 | import android.os.Looper;
8 | import android.util.Log;
9 | import android.widget.Toast;
10 |
11 | import androidx.preference.PreferenceManager;
12 |
13 | import java.io.File;
14 | import java.text.DateFormat;
15 | import java.util.Calendar;
16 |
17 | public class RecorderHelper {
18 |
19 | private static final String TAG = "MADARA";
20 | private final Context context;
21 | public static MediaRecorder recorder;
22 | private final String phoneNumber;
23 | private final SharedPreferences preferences;
24 |
25 | //__________________________________________________________________________________________________
26 |
27 | public RecorderHelper(Context context, String phoneNumber) {
28 | this.context = context;
29 | this.phoneNumber = phoneNumber;
30 | preferences = PreferenceManager.getDefaultSharedPreferences(context);
31 | }
32 | //__________________________________________________________________________________________________
33 |
34 | public void startRecoding() {
35 | File directory = context.getExternalFilesDir("/recordings/");
36 |
37 | if (!directory.exists()) {
38 | if (!directory.mkdirs()) {
39 | Toast.makeText(context, "Failed to create directory.", Toast.LENGTH_SHORT).show();
40 | }
41 | }
42 |
43 |
44 | String fileName = directory.getAbsolutePath() + "/" + getFileName();
45 |
46 | recorder = new MediaRecorder();
47 | recorder.reset();
48 | recorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
49 | recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
50 | recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
51 | recorder.setAudioEncodingBitRate(16);
52 | recorder.setAudioSamplingRate(44100);
53 | recorder.setOutputFile(fileName);
54 |
55 | try {
56 | recorder.prepare();
57 | recorder.start();
58 |
59 | if (preferences.getBoolean("start_toast", false)) {
60 | new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, "Recording started!", Toast.LENGTH_SHORT).show());
61 | }
62 | } catch (Exception e) {
63 | Log.e(TAG, "startVoiceRecoding: ", e);
64 | recorder = null;
65 | new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, "Recording start failed!", Toast.LENGTH_SHORT).show());
66 | }
67 | }
68 | //--------------------------------------------------------------------------------------------------
69 |
70 | public void stopVoiceRecoding() {
71 |
72 | try {
73 | recorder.stop();
74 | recorder.reset();
75 | recorder.release();
76 | recorder = null;
77 |
78 | if (preferences.getBoolean("saved_toast", false)) {
79 | new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, "Recording saved successfully!", Toast.LENGTH_SHORT).show());
80 | }
81 | } catch (Exception e) {
82 | Log.e(TAG, "stopVoiceRecoding:", e);
83 | recorder = null;
84 |
85 | if (preferences.getBoolean("saved_toast", false)) {
86 | new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, "Recording saved!", Toast.LENGTH_SHORT).show());
87 | }
88 | }
89 | }
90 |
91 | // ----------------------------------------------------------------------------------------------
92 |
93 | private String getFileName() {
94 | return ContactsHelper.getContactNameByPhoneNumber(context, phoneNumber)
95 | + "_("
96 | + phoneNumber
97 | + ")_"
98 | + DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime())
99 | + ".m4a";
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/donation.xml:
--------------------------------------------------------------------------------
1 |
3 |
5 |
7 |
9 |
11 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
28 |
29 |
37 |
38 |
46 |
47 |
53 |
54 |
55 |
63 |
64 |
69 |
70 |
78 |
79 |
80 |
96 |
97 |
98 |
99 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_single_file_options.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
19 |
20 |
25 |
26 |
41 |
42 |
57 |
58 |
73 |
74 |
89 |
90 |
105 |
106 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/browser_icon.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_audio_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
23 |
24 |
36 |
37 |
43 |
44 |
57 |
58 |
75 |
76 |
77 |
78 |
87 |
88 |
94 |
95 |
107 |
108 |
120 |
121 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
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 | # https://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 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_file_properties.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
26 |
27 |
34 |
35 |
48 |
49 |
50 |
56 |
57 |
64 |
65 |
78 |
79 |
80 |
86 |
87 |
94 |
95 |
108 |
109 |
110 |
116 |
117 |
124 |
125 |
138 |
139 |
140 |
146 |
147 |
154 |
155 |
169 |
170 |
171 |
177 |
178 |
185 |
186 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/helpers/SingleFileOptionsHelper.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.helpers;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.Intent;
6 | import android.content.res.ColorStateList;
7 | import android.net.Uri;
8 | import android.text.InputType;
9 | import android.util.Log;
10 | import android.view.ViewGroup;
11 | import android.widget.EditText;
12 | import android.widget.FrameLayout;
13 | import android.widget.Toast;
14 |
15 | import androidx.core.content.FileProvider;
16 |
17 | import org.json.JSONException;
18 | import org.json.JSONObject;
19 |
20 | import java.io.File;
21 |
22 | import zoro.benojir.callrecorder.R;
23 |
24 | public class SingleFileOptionsHelper {
25 |
26 | private static final String TAG = "MADARA";
27 | private final File file;
28 | private final Activity activity;
29 |
30 | public SingleFileOptionsHelper(Activity activity, File file) {
31 | this.activity = activity;
32 | this.file = file;
33 | }
34 |
35 | // ----------------------------------------------------------------------------------------------
36 |
37 | public void playRecording() {
38 | try {
39 | Intent intent = new Intent(Intent.ACTION_VIEW);
40 | Uri uri = FileProvider.getUriForFile(activity, activity.getApplicationContext().getPackageName() + ".provider", file);
41 | intent.setDataAndType(uri, "audio/*");
42 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
43 |
44 | // Create a chooser intent
45 | Intent chooserIntent = Intent.createChooser(intent, "Choose an app to play audio");
46 |
47 | // Verify that there are applications available to handle the intent
48 | if (intent.resolveActivity(activity.getPackageManager()) != null) {
49 | activity.startActivity(chooserIntent);
50 | } else {
51 | // Handle the case where no suitable app is installed
52 | Toast.makeText(activity, "No app available to play audio", Toast.LENGTH_SHORT).show();
53 | }
54 | } catch (Exception e) {
55 | Log.e(TAG, "Error playing audio: ", e);
56 | }
57 | }
58 |
59 | // ----------------------------------------------------------------------------------------------
60 |
61 | public void shareSingleRecordingFile() {
62 | try {
63 | Intent intent = new Intent(Intent.ACTION_SEND);
64 | Uri uri = FileProvider.getUriForFile(activity, activity.getApplicationContext().getPackageName() + ".provider", file);
65 | intent.setDataAndType(uri, "audio/mpeg");
66 | intent.putExtra(Intent.EXTRA_STREAM, uri);
67 | activity.startActivity(Intent.createChooser(intent, "Share via"));
68 | } catch (Exception e) {
69 | Log.e(TAG, "onBindViewHolder: ", e);
70 | }
71 | }
72 |
73 | // ----------------------------------------------------------------------------------------------
74 |
75 | public void renameFile(OnFileRenamedListener listener) {
76 | String oldName = file.getName();
77 | String filePath = file.getAbsolutePath();
78 |
79 | EditText inputField = new EditText(activity);
80 | inputField.setHint("Enter new file name...");
81 | inputField.setText(oldName.substring(0, oldName.length() - 4));
82 | inputField.setSingleLine(true);
83 | inputField.setInputType(InputType.TYPE_CLASS_TEXT);
84 | if (CustomFunctions.isDarkModeOn(activity)) {
85 | inputField.setBackgroundTintList(ColorStateList.valueOf(activity.getColor(R.color.theme_color_dark)));
86 | } else {
87 | inputField.setBackgroundTintList(ColorStateList.valueOf(activity.getColor(R.color.theme_color_light)));
88 | }
89 | inputField.setPadding(
90 | activity.getResources().getDimensionPixelSize(R.dimen.input_box_padding_lr),
91 | activity.getResources().getDimensionPixelSize(R.dimen.input_box_padding_tb),
92 | activity.getResources().getDimensionPixelSize(R.dimen.input_box_padding_lr),
93 | activity.getResources().getDimensionPixelSize(R.dimen.input_box_padding_tb)
94 | );
95 |
96 | FrameLayout container = new FrameLayout(activity);
97 |
98 | FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
99 | params.leftMargin = activity.getResources().getDimensionPixelSize(R.dimen.dialog_margin_left);
100 | params.rightMargin = activity.getResources().getDimensionPixelSize(R.dimen.dialog_margin_right);
101 | params.topMargin = activity.getResources().getDimensionPixelSize(R.dimen.dialog_margin_top);
102 |
103 | inputField.setLayoutParams(params);
104 |
105 | container.addView(inputField);
106 |
107 | AlertDialog dialog = new AlertDialog.Builder(activity)
108 | .setTitle("Rename to:")
109 | .setView(container)
110 | .setCancelable(false)
111 | .setIcon(R.drawable.mode_edit_24)
112 | .setPositiveButton("Rename", (dialogInterface, i) -> {
113 |
114 | String newName = inputField.getText().toString();
115 |
116 | if (!newName.equalsIgnoreCase("")) {
117 |
118 | newName += ".m4a";
119 |
120 | File oldNameFile = new File(filePath);
121 | File newNameFile = new File(oldNameFile.getParent().concat("/" + newName));
122 |
123 | JSONObject tempFileJObj = new JSONObject();
124 |
125 | if (oldNameFile.renameTo(newNameFile)) {
126 |
127 | Toast.makeText(activity, "File renamed.", Toast.LENGTH_SHORT).show();
128 |
129 | try {
130 | tempFileJObj.put("file_name", newName);
131 | tempFileJObj.put("size", CustomFunctions.fileSizeFormatter(newNameFile.length()));
132 | tempFileJObj.put("modified_date", CustomFunctions.timeFormatter(newNameFile.lastModified()));
133 | tempFileJObj.put("absolute_path", newNameFile.getAbsolutePath());
134 | listener.onComplete(tempFileJObj, true);
135 | } catch (JSONException e) {
136 | try {
137 | listener.onComplete(tempFileJObj, false);
138 | } catch (JSONException ex) {
139 | Log.e(TAG, "renameFile: ", e);
140 | }
141 | Log.e(TAG, "onBindViewHolder: ", e);
142 | }
143 | } else {
144 | try {
145 | listener.onComplete(tempFileJObj, false);
146 | } catch (JSONException e) {
147 | Log.e(TAG, "renameFile: ", e);
148 | }
149 | Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
150 | }
151 |
152 | dialogInterface.dismiss();
153 | } else {
154 | Toast.makeText(activity, "Please provide a valid name.", Toast.LENGTH_SHORT).show();
155 | }
156 | })
157 | .setNegativeButton("Cancel", null)
158 | .create();
159 |
160 | dialog.show();
161 | }
162 |
163 | // ----------------------------------------------------------------------------------------------
164 |
165 | public void deleteFile(OnFileDeletedListener listener){
166 |
167 | AlertDialog.Builder builder = new AlertDialog.Builder(activity);
168 | builder.setCancelable(true);
169 | builder.setTitle("Delete");
170 | builder.setMessage("Do you really want to delete?");
171 | builder.setIcon(R.drawable.delete_24);
172 | builder.setPositiveButton("Delete", (dialogInterface, i) -> {
173 |
174 | if (file.delete()) {
175 | Toast.makeText(activity, "Recorded file deleted successfully.", Toast.LENGTH_SHORT).show();
176 | listener.onComplete(true, file);
177 | } else {
178 | Toast.makeText(activity, "Failed to delete file.", Toast.LENGTH_SHORT).show();
179 | listener.onComplete(false, file);
180 | }
181 | });
182 | builder.setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.dismiss());
183 | builder.show();
184 | }
185 |
186 | // ----------------------------------------------------------------------------------------------
187 |
188 | public interface OnFileRenamedListener {
189 | void onComplete(JSONObject fileInfo, boolean success) throws JSONException;
190 | }
191 | public interface OnFileDeletedListener {
192 | void onComplete(boolean success, File file);
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/helpers/CustomFunctions.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.helpers;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.content.ClipData;
6 | import android.content.ClipboardManager;
7 | import android.content.Context;
8 | import android.content.pm.ApplicationInfo;
9 | import android.content.pm.PackageManager;
10 | import android.content.res.Configuration;
11 | import android.graphics.drawable.Drawable;
12 | import android.os.Handler;
13 | import android.os.Looper;
14 | import android.util.Log;
15 | import android.view.View;
16 | import android.view.inputmethod.InputMethodManager;
17 | import android.widget.Toast;
18 |
19 | import androidx.appcompat.app.AlertDialog;
20 |
21 | import zoro.benojir.callrecorder.BuildConfig;
22 | import zoro.benojir.callrecorder.R;
23 |
24 | import org.apache.commons.io.comparator.LastModifiedFileComparator;
25 | import org.apache.commons.io.comparator.NameFileComparator;
26 | import org.json.JSONObject;
27 | import org.jsoup.Jsoup;
28 |
29 | import java.io.File;
30 | import java.text.DateFormat;
31 | import java.text.DecimalFormat;
32 | import java.text.SimpleDateFormat;
33 | import java.util.Arrays;
34 | import java.util.List;
35 | import java.util.Locale;
36 | import java.util.concurrent.TimeUnit;
37 |
38 | public class CustomFunctions {
39 |
40 | private static final String TAG = "MADARA";
41 |
42 | //__________________________________________________________________________________________________
43 | public static void hideKeyboard(Context context, View view) {
44 | InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
45 | inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
46 | }
47 | //__________________________________________________________________________________________________
48 |
49 | public static boolean isDarkModeOn(Context context) {
50 | int nightModeFlags = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
51 | return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
52 | }
53 | //__________________________________________________________________________________________________
54 |
55 | public static String formatDuration(long durationMs) {
56 | long hours = TimeUnit.MILLISECONDS.toHours(durationMs);
57 | long minutes = TimeUnit.MILLISECONDS.toMinutes(durationMs) % 60;
58 | long seconds = TimeUnit.MILLISECONDS.toSeconds(durationMs) % 60;
59 |
60 | if (hours > 0) {
61 | return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
62 | } else {
63 | return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
64 | }
65 | }
66 |
67 | //__________________________________________________________________________________________________
68 |
69 | public static boolean isSystemApp(Context context) {
70 |
71 | boolean isSystemApp = false;
72 |
73 | PackageManager pm = context.getPackageManager();
74 | List installedApps = pm.getInstalledApplications(0);
75 |
76 | for (ApplicationInfo ai : installedApps) {
77 |
78 | if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
79 |
80 | if (ai.packageName.equalsIgnoreCase(context.getPackageName())) {
81 | isSystemApp = true;
82 | }
83 | }
84 | }
85 |
86 | return isSystemApp;
87 | }
88 | //__________________________________________________________________________________________________
89 |
90 | public static void sortOldestFilesFirst(File[] files) {
91 | new Thread(() -> Arrays.sort(files, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR)).start();
92 | // Arrays.sort(files, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
93 | }
94 |
95 | public static void sortNewestFilesFirst(File[] files) {
96 | Arrays.sort(files, LastModifiedFileComparator.LASTMODIFIED_REVERSE);
97 | // new Thread(() -> Arrays.sort(files, LastModifiedFileComparator.LASTMODIFIED_REVERSE)).start();
98 | }
99 |
100 | public static void sortFilesByNameAscending(File[] files) {
101 | Arrays.sort(files, NameFileComparator.NAME_COMPARATOR);
102 | }
103 |
104 | public static void sortFilesByNameDescending(File[] files) {
105 | Arrays.sort(files, NameFileComparator.NAME_REVERSE);
106 | }
107 |
108 | //__________________________________________________________________________________________________
109 | public static String fileSizeFormatter(long size) {
110 | if (size <= 0) return "0 Bytes";
111 | final String[] units = new String[]{"Bytes", "KB", "MB", "GB", "TB"};
112 | int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
113 | return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
114 | }
115 | //__________________________________________________________________________________________________
116 |
117 | public static String timeFormatter(Long time) {
118 | @SuppressLint("SimpleDateFormat")
119 | DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy - hh:mm:ss");
120 | return dateFormat.format(time);
121 | }
122 | //__________________________________________________________________________________________________
123 |
124 | public static double round(double value, int places) {
125 | if (places < 0) throw new IllegalArgumentException();
126 |
127 | DecimalFormat deciFormat = new DecimalFormat();
128 | deciFormat.setMaximumFractionDigits(places);
129 | return Double.parseDouble(deciFormat.format(value));
130 | }
131 | //__________________________________________________________________________________________________
132 |
133 | public static String timeFormatterFromSeconds(int seconds) {
134 |
135 | int hoursInt = 0, minutesInt = 0, secondsInt;
136 | String hoursStr = "00", minutesStr = "00", secondsStr;
137 |
138 | if (seconds < 60) {
139 | secondsInt = seconds;
140 | secondsStr = String.valueOf(seconds);
141 | } else {
142 | secondsInt = seconds % 60;
143 | secondsStr = secondsInt + "";
144 | minutesInt = (seconds - secondsInt) / 60;
145 |
146 | if (minutesInt > 60) {
147 |
148 | hoursInt = (minutesInt - (minutesInt % 60)) / 60;
149 | minutesInt = minutesInt % 60;
150 |
151 | hoursStr = String.valueOf(hoursInt);
152 | minutesStr = String.valueOf(minutesInt);
153 | }
154 | }
155 |
156 | if (secondsInt < 10) {
157 | secondsStr = "0" + secondsStr;
158 | }
159 | if (minutesInt < 10) {
160 | if (minutesInt > 0) {
161 | minutesStr = "0" + minutesInt;
162 | }
163 | }
164 | if (hoursInt < 10) {
165 | if (hoursInt > 0) {
166 | hoursStr = "0" + hoursInt;
167 | }
168 | }
169 |
170 | return hoursStr + ":" + minutesStr + ":" + secondsStr;
171 | }
172 | //__________________________________________________________________________________________________
173 |
174 | public static void checkForUpdateOnStartApp(Context context, View button) {
175 |
176 | new Thread(() -> {
177 |
178 | try {
179 | String versionJson = Jsoup.connect(context.getString(R.string.version_check_url))
180 | .timeout(30000)
181 | .get()
182 | .body()
183 | .text();
184 |
185 | JSONObject versionObject = new JSONObject(versionJson);
186 |
187 | if (BuildConfig.VERSION_CODE < Integer.parseInt(versionObject.getString("versionCode"))) {
188 | new Handler(Looper.getMainLooper()).post(() -> button.setVisibility(View.VISIBLE));
189 | }
190 | } catch (Exception e) {
191 | Log.e(TAG, "error: ", e);
192 | }
193 | }).start();
194 | }
195 |
196 | // ----------------------------------------------------------------------------------------------
197 |
198 | public static void simpleAlert(Context context, String title, String message, String dismissBtn, Drawable drawableIcon) {
199 |
200 | AlertDialog.Builder builder = new AlertDialog.Builder(context);
201 |
202 | if (title != null) {
203 | builder.setTitle(title);
204 | }
205 |
206 | if (message != null) {
207 | builder.setMessage(message);
208 | }
209 |
210 | if (drawableIcon != null) {
211 | builder.setIcon(drawableIcon);
212 | }
213 |
214 | builder.setCancelable(false);
215 |
216 | builder.setPositiveButton(dismissBtn, (dialog, which) -> dialog.dismiss());
217 |
218 | AlertDialog alert = builder.create();
219 |
220 | alert.show();
221 | }
222 | // ----------------------------------------------------------------------------------------------
223 |
224 | public static void copyTextToClipboard(Context context, String text) {
225 |
226 | ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
227 | ClipData clipData = ClipData.newPlainText(null, text);
228 |
229 | if (clipboardManager != null) {
230 |
231 | clipboardManager.setPrimaryClip(clipData);
232 | Toast.makeText(context, "Copied to clipboard.", Toast.LENGTH_SHORT).show();
233 | } else {
234 | Toast.makeText(context, context.getString(R.string.report_issue_text), Toast.LENGTH_SHORT).show();
235 | }
236 |
237 | }
238 | // ----------------------------------------------------------------------------------------------
239 | }
240 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/java/zoro/benojir/callrecorder/dialogs/AudioPlayerDialog.java:
--------------------------------------------------------------------------------
1 | package zoro.benojir.callrecorder.dialogs;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.net.Uri;
6 | import android.os.Handler;
7 | import android.os.Looper;
8 | import android.util.Log;
9 | import android.view.MenuInflater;
10 | import android.view.Window;
11 | import android.view.WindowManager;
12 | import android.widget.ImageButton;
13 | import android.widget.SeekBar;
14 | import android.widget.TextView;
15 | import android.widget.Toast;
16 |
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.OptIn;
19 | import androidx.appcompat.widget.PopupMenu;
20 | import androidx.media3.common.MediaItem;
21 | import androidx.media3.common.PlaybackException;
22 | import androidx.media3.common.PlaybackParameters;
23 | import androidx.media3.common.Player;
24 | import androidx.media3.common.util.UnstableApi;
25 | import androidx.media3.exoplayer.ExoPlayer;
26 |
27 | import java.io.File;
28 |
29 | import zoro.benojir.callrecorder.R;
30 | import zoro.benojir.callrecorder.helpers.CustomFunctions;
31 |
32 | @OptIn(markerClass = UnstableApi.class)
33 | public class AudioPlayerDialog {
34 |
35 | private static final String TAG = "MADARA";
36 | private final Activity activity;
37 | private final File file;
38 | private final Dialog dialog;
39 | private final TextView durationTV;
40 | private final TextView speedOptionSelectorTV;
41 | private final SeekBar seekBar;
42 | private final ImageButton playOrPause, skipBackward, skipForward;
43 | private ExoPlayer exoPlayer;
44 | private String totalAudioDuration = "00:00:00";
45 | private boolean isUserSeeking;
46 | private boolean playingFinished;
47 |
48 | private final Handler handler = new Handler(Looper.getMainLooper());
49 | private final Runnable updateSeekBar = new Runnable() {
50 | @Override
51 | public void run() {
52 | if (!isUserSeeking && exoPlayer.isPlaying()) {
53 | seekBar.setProgress((int) exoPlayer.getCurrentPosition());
54 | updateUIWhilePlaying(exoPlayer.getCurrentPosition());
55 | }
56 | handler.postDelayed(this, 1000);
57 | }
58 | };
59 |
60 | // ------------------------------------------------------------------------------------------
61 | public AudioPlayerDialog(Activity activity, File file) {
62 | this.activity = activity;
63 | this.file = file;
64 | dialog = new Dialog(activity);
65 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
66 | dialog.setCancelable(true);
67 | dialog.setContentView(R.layout.dialog_audio_player);
68 | dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
69 |
70 | TextView fileNameTV = dialog.findViewById(R.id.fileNameTV);
71 | durationTV = dialog.findViewById(R.id.duration_timer);
72 | speedOptionSelectorTV = dialog.findViewById(R.id.speed_option_selector);
73 |
74 | seekBar = dialog.findViewById(R.id.seekBar);
75 |
76 | playOrPause = dialog.findViewById(R.id.playOrPause);
77 | skipBackward = dialog.findViewById(R.id.skipBackward);
78 | skipForward = dialog.findViewById(R.id.skipForward);
79 |
80 | String fileName = file.getName();
81 | fileNameTV.setText(fileName);
82 |
83 | initializeExoPlayer();
84 |
85 | //--------------------------------------------------------------------------------------
86 |
87 | seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
88 | @Override
89 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
90 | if (fromUser) {
91 | long shouldSeekTo = progress * 1000L;
92 | exoPlayer.seekTo(shouldSeekTo);
93 |
94 | String formatted = CustomFunctions.formatDuration(shouldSeekTo) + " · " + totalAudioDuration;
95 | durationTV.setText(formatted);
96 | }
97 |
98 | if (progress < seekBar.getMax()){
99 | playingFinished = false;
100 | }
101 | }
102 |
103 | @Override
104 | public void onStartTrackingTouch(SeekBar seekBar) {
105 | isUserSeeking = true;
106 |
107 | if (playingFinished) {
108 | exoPlayer.pause();
109 | playOrPause.setImageResource(R.drawable.play_arrow_24);
110 | }
111 | }
112 |
113 | @Override
114 | public void onStopTrackingTouch(SeekBar seekBar) {
115 | isUserSeeking = false;
116 | long shouldSeekTo = seekBar.getProgress() * 1000L;
117 | exoPlayer.seekTo(shouldSeekTo);
118 |
119 | String formatted = CustomFunctions.formatDuration(shouldSeekTo) + " · " + totalAudioDuration;
120 | durationTV.setText(formatted);
121 | }
122 | });
123 |
124 | //--------------------------------------------------------------------------------------
125 |
126 | dialog.setOnCancelListener(dialog -> {
127 | if (exoPlayer != null) {
128 | exoPlayer.stop();
129 | exoPlayer.release();
130 | handler.removeCallbacks(updateSeekBar);
131 | }
132 | });
133 |
134 |
135 | actionsOnDialog();
136 | dialog.show();
137 | }
138 |
139 | // ------------------------------------------------------------------------------------------
140 | private void actionsOnDialog() {
141 |
142 | playOrPause.setOnClickListener(view -> {
143 | if (exoPlayer != null) {
144 |
145 | if (playingFinished) {
146 | playingFinished = false;
147 | exoPlayer.seekTo(0);
148 | exoPlayer.play();
149 | seekBar.setProgress(0);
150 | playOrPause.setImageResource(R.drawable.pause_24);
151 | } else{
152 | if (exoPlayer.isPlaying()) {
153 | exoPlayer.pause();
154 | playOrPause.setImageResource(R.drawable.play_arrow_24);
155 | } else {
156 | exoPlayer.play();
157 | playOrPause.setImageResource(R.drawable.pause_24);
158 | }
159 | }
160 | }
161 | });
162 |
163 |
164 | skipBackward.setOnClickListener(view -> {
165 | // If the playback had finished, ensure the player remains paused
166 | if (playingFinished) {
167 | exoPlayer.pause();
168 | playOrPause.setImageResource(R.drawable.play_arrow_24);
169 | }
170 |
171 | long currentPosition = exoPlayer.getCurrentPosition();
172 | long newPosition = Math.max(currentPosition - 5000, 0);
173 | exoPlayer.seekTo(newPosition);
174 |
175 | // Update SeekBar and TextView
176 | seekBar.setProgress((int) (newPosition / 1000));
177 | String formatted = CustomFunctions.formatDuration(newPosition) + " · " + totalAudioDuration;
178 | durationTV.setText(formatted);
179 | updateDurationTextView(newPosition);
180 | });
181 |
182 |
183 | skipForward.setOnClickListener(view -> {
184 | // If the playback had finished, ensure the player remains paused
185 | if (playingFinished) {
186 | exoPlayer.pause();
187 | playOrPause.setImageResource(R.drawable.play_arrow_24);
188 | }
189 |
190 | long currentPosition = exoPlayer.getCurrentPosition();
191 | long newPosition = Math.min(currentPosition + 5000, exoPlayer.getDuration());
192 | exoPlayer.seekTo(newPosition);
193 |
194 | // Update SeekBar and TextView
195 | seekBar.setProgress((int) (newPosition / 1000));
196 | updateDurationTextView(newPosition);
197 | });
198 |
199 |
200 | speedOptionSelectorTV.setOnClickListener(view -> {
201 | PopupMenu speedMenu = new PopupMenu(view.getContext(), view);
202 | MenuInflater inflater = speedMenu.getMenuInflater();
203 | inflater.inflate(R.menu.speed_menu, speedMenu.getMenu());
204 |
205 | speedMenu.setOnMenuItemClickListener(item -> {
206 |
207 | float playbackSpeed;
208 |
209 | if (item.getItemId() == R.id.speed_0_75) {
210 | playbackSpeed = 0.75f;
211 | } else if (item.getItemId() == R.id.speed_1_0) {
212 | playbackSpeed = 1.0f;
213 | } else if (item.getItemId() == R.id.speed_1_25) {
214 | playbackSpeed = 1.25f;
215 | } else if (item.getItemId() == R.id.speed_1_5) {
216 | playbackSpeed = 1.5f;
217 | } else if (item.getItemId() == R.id.speed_2_0) {
218 | playbackSpeed = 2.0f;
219 | } else {
220 | playbackSpeed = 1.0f; // Default speed
221 | }
222 |
223 | // Set the playback speed for ExoPlayer
224 | if (exoPlayer != null) {
225 | exoPlayer.setPlaybackParameters(new PlaybackParameters(playbackSpeed));
226 | }
227 |
228 | // Update the TextView to reflect the selected speed
229 | String speedText = item.getTitle().toString();
230 | speedOptionSelectorTV.setText(speedText);
231 |
232 | return true;
233 | });
234 |
235 | // Show the PopupMenu
236 | speedMenu.show();
237 | });
238 | }
239 |
240 | // ------------------------------------------------------------------------------------------
241 |
242 | private void initializeExoPlayer() {
243 |
244 | exoPlayer = new ExoPlayer.Builder(activity).build();
245 | exoPlayer.setMediaItem(MediaItem.fromUri(Uri.fromFile(file)));
246 | exoPlayer.prepare();
247 | exoPlayer.setPlayWhenReady(true);
248 |
249 | playOrPause.setImageResource(R.drawable.pause_24);
250 |
251 | exoPlayer.addListener(new Player.Listener() {
252 | @Override
253 | public void onPlayerError(@NonNull PlaybackException error) {
254 | Player.Listener.super.onPlayerError(error);
255 | Toast.makeText(activity, "Cannot play this recording.", Toast.LENGTH_SHORT).show();
256 | dialog.dismiss();
257 | Log.e(TAG, "onPlayerError: ", error);
258 | }
259 |
260 | @Override
261 | public void onIsPlayingChanged(boolean isPlaying) {
262 | if (isPlaying) {
263 | startSeekBarUpdate();
264 | } else {
265 | stopSeekBarUpdate();
266 | }
267 | }
268 |
269 | @Override
270 | public void onPlaybackStateChanged(int playbackState) {
271 |
272 | if (playbackState == Player.STATE_READY) { // calling everytime when the player is ready even after the user is seeking or buffering but it is not onPlaying
273 | long audioDuration = exoPlayer.getDuration();
274 | int durationInSecs = (int) (audioDuration / 1000); // Convert to seconds
275 | totalAudioDuration = CustomFunctions.formatDuration(audioDuration);
276 | seekBar.setMax(durationInSecs);
277 | }
278 | if (playbackState == Player.STATE_ENDED) {
279 | playOrPause.setImageResource(R.drawable.play_arrow_24);
280 |
281 | String finishedPlayingDuration = totalAudioDuration + " · " + totalAudioDuration;
282 | durationTV.setText(finishedPlayingDuration);
283 |
284 | seekBar.setProgress(seekBar.getMax());
285 |
286 | playingFinished = true;
287 | }
288 | }
289 | });
290 | }
291 | // ------------------------------------------------------------------------------------------
292 |
293 | private void startSeekBarUpdate() {
294 | handler.post(updateSeekBar);
295 | }
296 |
297 | private void stopSeekBarUpdate() {
298 | handler.removeCallbacks(updateSeekBar);
299 | }
300 |
301 | private void updateUIWhilePlaying(long currentPosition) {
302 | int currentProgressInSecs = (int) (currentPosition / 1000);
303 | String formatted = CustomFunctions.formatDuration(currentPosition) + " · " + totalAudioDuration;
304 | durationTV.setText(formatted);
305 | seekBar.setProgress(currentProgressInSecs);
306 | }
307 |
308 | private void updateDurationTextView(long position) {
309 | String formatted = CustomFunctions.formatDuration(position) + " · " + totalAudioDuration;
310 | durationTV.setText(formatted);
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
--------------------------------------------------------------------------------