├── .gitignore ├── .hgignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── Screenshot_2016-05-14-16-07-40.png ├── app ├── .gitignore ├── app-release.apk ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── jlcsoftware │ │ └── callrecorder │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── jlcsoftware │ │ │ ├── callrecorder │ │ │ ├── AppPreferences.java │ │ │ ├── LocalBroadcastActions.java │ │ │ ├── MainActivity.java │ │ │ ├── MyContactsAdapter.java │ │ │ ├── MyRecordingRecyclerViewAdapter.java │ │ │ ├── MyWhitelistItemRecyclerViewAdapter.java │ │ │ ├── PhoneCallRecord.java │ │ │ ├── RecordingFragment.java │ │ │ ├── SettingsActivity.java │ │ │ ├── WhitelistActivity.java │ │ │ ├── WhitelistFragment.java │ │ │ └── WhitelistRecord.java │ │ │ ├── database │ │ │ ├── CallLog.java │ │ │ ├── Database.java │ │ │ └── Whitelist.java │ │ │ ├── listeners │ │ │ └── PhoneListener.java │ │ │ ├── receivers │ │ │ ├── MyAlarmReceiver.java │ │ │ ├── MyCallReceiver.java │ │ │ └── MyLocalBroadcastReceiver.java │ │ │ ├── services │ │ │ ├── CleanupService.java │ │ │ └── RecordCallService.java │ │ │ └── widget │ │ │ └── adapter │ │ │ └── SectionedRecyclerViewAdapter.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_info_black_24dp.png │ │ ├── ic_notifications_black_24dp.png │ │ ├── ic_sync_black_24dp.png │ │ ├── switch_off.9.png │ │ └── switch_on.9.png │ │ ├── drawable-mdpi │ │ ├── ic_info_black_24dp.png │ │ ├── ic_notifications_black_24dp.png │ │ ├── ic_sync_black_24dp.png │ │ ├── switch_off.9.png │ │ └── switch_on.9.png │ │ ├── drawable-v21 │ │ ├── ic_info_black_24dp.xml │ │ ├── ic_notifications_black_24dp.xml │ │ └── ic_sync_black_24dp.xml │ │ ├── drawable-xhdpi │ │ ├── ic_info_black_24dp.png │ │ ├── ic_notifications_black_24dp.png │ │ ├── ic_sync_black_24dp.png │ │ ├── switch_off.9.png │ │ └── switch_on.9.png │ │ ├── drawable-xxhdpi │ │ ├── ic_info_black_24dp.png │ │ ├── ic_notifications_black_24dp.png │ │ ├── ic_sync_black_24dp.png │ │ ├── switch_off.9.png │ │ └── switch_on.9.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_info_black_24dp.png │ │ ├── ic_notifications_black_24dp.png │ │ ├── ic_sync_black_24dp.png │ │ ├── switch_off.9.png │ │ └── switch_on.9.png │ │ ├── drawable │ │ ├── clock_circular.xml │ │ ├── day.xml │ │ ├── ic_add_box_white_24dp.xml │ │ ├── ic_cards_black_24.xml │ │ ├── ic_delete_black_24dp.xml │ │ ├── ic_delete_white_24dp.xml │ │ ├── ic_folder_black_24dp.xml │ │ ├── ic_lock_black_24dp.xml │ │ ├── ic_play_arrow_black_36dp.xml │ │ ├── ic_recording_conversation_white_24.xml │ │ ├── ic_share_black_24dp.xml │ │ ├── ic_share_black_48dp.xml │ │ ├── ic_share_white_24dp.xml │ │ ├── ic_unknown.xml │ │ ├── ic_user.xml │ │ ├── phone_in.xml │ │ ├── phone_out.xml │ │ ├── round_button.xml │ │ ├── selected_list_item.xml │ │ └── toggle_switch.xml │ │ ├── layout │ │ ├── actionbar_toggle.xml │ │ ├── actionbar_toggle2.xml │ │ ├── activity_main.xml │ │ ├── activity_settings.xml │ │ ├── activity_whitelist.xml │ │ ├── fragment_recording.xml │ │ ├── fragment_recording_list.xml │ │ ├── fragment_whitelist_item.xml │ │ ├── fragment_whitelist_item_list.xml │ │ └── item_contact.xml │ │ ├── menu │ │ ├── menu_main.xml │ │ ├── menu_main_popup.xml │ │ ├── menu_whitelist.xml │ │ └── menu_whitelist_popup.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── jlcsoftware │ └── callrecorder │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── helpers ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── jlcsoftware │ │ └── helpers │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── jlcsoftware │ │ │ └── helpers │ │ │ ├── AboutDialog.java │ │ │ ├── HtmlTagHandler.java │ │ │ ├── PackageUtils.java │ │ │ └── RateMeNowDialog.java │ └── res │ │ ├── drawable │ │ └── jeffsavatar.png │ │ ├── layout │ │ ├── dialog_about.xml │ │ └── dialog_about_me.xml │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── jlcsoftware │ └── helpers │ └── ExampleUnitTest.java ├── settings.gradle └── web_hi_res_512.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | syntax: glob 3 | 4 | ### JetBrains template 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 6 | 7 | *.iml 8 | 9 | ## Directory-based project format: 10 | .idea/ 11 | # if you remove the above rule, at least ignore the following: 12 | 13 | # User-specific stuff: 14 | # .idea/workspace.xml 15 | # .idea/tasks.xml 16 | # .idea/dictionaries 17 | 18 | # Sensitive or high-churn files: 19 | # .idea/dataSources.ids 20 | # .idea/dataSources.xml 21 | # .idea/sqlDataSources.xml 22 | # .idea/dynamic.xml 23 | # .idea/uiDesigner.xml 24 | 25 | # Gradle: 26 | # .idea/gradle.xml 27 | # .idea/libraries 28 | 29 | # Mongo Explorer plugin: 30 | # .idea/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.ipr 34 | *.iws 35 | 36 | ## Plugin-specific files: 37 | 38 | # IntelliJ 39 | /out/ 40 | 41 | # mpeltonen/sbt-idea plugin 42 | .idea_modules/ 43 | 44 | # JIRA plugin 45 | atlassian-ide-plugin.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | ### Gradle template 52 | .gradle 53 | build/ 54 | 55 | # Ignore Gradle GUI config 56 | gradle-app.setting 57 | 58 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 59 | !gradle-wrapper.jar 60 | ### Android template 61 | # Built application files 62 | *.apk 63 | *.ap_ 64 | 65 | # Files for the Dalvik VM 66 | *.dex 67 | 68 | # Java class files 69 | *.class 70 | 71 | # Generated files 72 | bin/ 73 | gen/ 74 | 75 | # Gradle files 76 | .gradle/ 77 | build/ 78 | 79 | # Local configuration file (sdk path, etc) 80 | local.properties 81 | 82 | # Proguard folder generated by Eclipse 83 | proguard/ 84 | 85 | # Log Files 86 | *.log 87 | 88 | # Android Studio Navigation editor temp files 89 | .navigation/ 90 | ### Java template 91 | /play_licensing/build 92 | *.class 93 | 94 | # Mobile Tools for Java (J2ME) 95 | .mtj.tmp/ 96 | 97 | # Package Files # 98 | *.jar 99 | *.war 100 | *.ear 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | hs_err_pid* 104 | 105 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CallRecorder -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CallRecorder - Unsupported - Use at your own risk 2 | Automatic Phone Call Recorder for Android 3 | 4 | An automatic phone call recorder, records ALL calls your phone gets. You can choose to not record some of your contacts, or you can record every call. Schedule 5 | recording cleanup and mark recordings to never be deleted (by the cleanup schedule). 6 | 7 | It's up to you to determine if this is legal in your jurisdiction. Not every phone supports call recording, so your mileage may very. 8 | 9 | ### Demo 10 | Full Version can be downloaded from [Google Play] 11 | (https://play.google.com/store/apps/details?id=com.jlcsoftware.callrecorder) 12 | 13 | Screen Shot:
14 | 15 | 16 | ### Copyright and License 17 | You are free to use the code as sample code and/or include portions of it in your own projects. Just don't fork it and release it as yours. (ie: At least put SOME effort into making it YOUR app. ) It would be nice if you credit me and where you got it from, but that's not necessary. 18 | 19 | 20 | Note: Icons are licensed as follows: 21 | * [Icons made by Freepik] (http://www.freepik.com) from [Flaticon] (http://www.flaticon.com) is licensed by [Creative Commons BY 3.0] (http://creativecommons.org/licenses/by/3.0/) 22 | * [Icons made by SimpleIcon] (http://www.flaticon.com/authors/simpleicon) from [Flaticon] (http://www.flaticon.com) is licensed by [Creative Commons BY 3.0] (http://creativecommons.org/licenses/by/3.0/) 23 | * [Icons made by Dave Gandy] (http://www.flaticon.com/authors/dave-gandy) from [Flaticon] (http://www.flaticon.com) is licensed by [Creative Commons BY 3.0] (http://creativecommons.org/licenses/by/3.0/) 24 | -------------------------------------------------------------------------------- /Screenshot_2016-05-14-16-07-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/Screenshot_2016-05-14-16-07-40.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/app-release.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | defaultConfig { 7 | applicationId "com.jlcsoftware.callrecorder" 8 | minSdkVersion 15 9 | targetSdkVersion 23 10 | versionCode 4 11 | versionName '1.1' 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | applicationVariants.all { variant -> 18 | variant.outputs.each { output -> 19 | def newName = output.outputFile.name 20 | newName = newName.replace("app", "$defaultConfig.applicationId") 21 | output.outputFile = new File(output.outputFile.parent, newName) 22 | } 23 | } 24 | } 25 | } 26 | productFlavors { 27 | } 28 | } 29 | 30 | dependencies { 31 | compile fileTree(include: ['*.jar'], dir: 'libs') 32 | testCompile 'junit:junit:4.12' 33 | compile 'com.android.support:appcompat-v7:24.0.0-alpha2' 34 | compile 'com.android.support:design:24.0.0-alpha2' 35 | compile 'com.android.support:support-v4:24.0.0-alpha2' 36 | compile 'com.android.support:recyclerview-v7:24.0.0-alpha2' 37 | compile 'com.google.android.gms:play-services-ads:8.4.0' 38 | compile project(path: ':helpers') 39 | } 40 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in d:\DevTools\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/jlcsoftware/callrecorder/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 61 | 64 | 65 | 68 | 69 | 73 | 76 | 77 | 78 | 81 | 85 | 86 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/AppPreferences.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.os.Environment; 6 | 7 | import java.io.File; 8 | 9 | /** 10 | * Created by Jeff on 01-May-16. 11 | * A Singleton 12 | * Wrapper around the default SharedPreferences so we can easily set and read our settings 13 | */ 14 | public class AppPreferences { 15 | 16 | private static AppPreferences instance = null; 17 | 18 | /** 19 | * Must be called once on app startup 20 | * 21 | * @param context - application context 22 | * @return this 23 | */ 24 | public static AppPreferences getInstance(Context context) { 25 | if (instance == null) { 26 | if (context == null) { 27 | throw new IllegalStateException(AppPreferences.class.getSimpleName() + 28 | " is not initialized, call getInstance(Context) with a VALID Context first."); 29 | } 30 | instance = new AppPreferences(context.getApplicationContext()); 31 | } 32 | return instance; 33 | } 34 | 35 | 36 | private SharedPreferences preferences; 37 | 38 | private AppPreferences(Context context) { 39 | preferences = context.getSharedPreferences(context.getPackageName() + "_preferences", Context.MODE_PRIVATE); // From Android sources for getDefaultSharedPreferences 40 | } 41 | 42 | /** 43 | * Is Recording Enabled 44 | * 45 | * @return true/false 46 | */ 47 | public boolean isRecordingEnabled() { 48 | return preferences.getBoolean("RecordingEnabled", true); 49 | } 50 | 51 | /** 52 | * Set Recording is Enabled 53 | * 54 | * @param enabled true/false 55 | */ 56 | public void setRecordingEnabled(boolean enabled) { 57 | preferences.edit().putBoolean("RecordingEnabled", enabled).commit(); 58 | } 59 | 60 | 61 | /** 62 | * Record incoming calls if {@link #isRecordingEnabled() isRecordingEnabled} 63 | * 64 | * @return true/false 65 | */ 66 | public boolean isRecordingIncomingEnabled() { 67 | return preferences.getBoolean("RecordingIncomingEnabled", true); 68 | } 69 | 70 | /** 71 | * Set Recording is Enabled for incoming calls 72 | * 73 | * @param enabled true/false 74 | */ 75 | public void setRecordingIncomingEnabled(boolean enabled) { 76 | preferences.edit().putBoolean("RecordingIncomingEnabled", enabled).commit(); 77 | } 78 | 79 | /** 80 | * Record outgoing calls if {@link #isRecordingEnabled() isRecordingEnabled} 81 | * 82 | * @return true/false 83 | */ 84 | public boolean isRecordingOutgoingEnabled() { 85 | return preferences.getBoolean("RecordingOutgoingEnabled", true); 86 | } 87 | 88 | /** 89 | * Set Recording is Enabled for outgoing calls 90 | * 91 | * @param enabled true/false 92 | */ 93 | public void setRecordingOutgoingEnabled(boolean enabled) { 94 | preferences.edit().putBoolean("RecordingOutgoingEnabled", enabled).commit(); 95 | } 96 | 97 | /** 98 | * Get the location to store the recordings too... 99 | * TODO: make configurable 100 | * 101 | * @return directory location 102 | */ 103 | public File getFilesDirectory() { 104 | // might make this configurable.... 105 | String filesDir = (new StringBuilder()).append(Environment.getExternalStorageDirectory().getAbsolutePath()).append("/").append("calls").append("/").toString(); 106 | filesDir = preferences.getString("FilesDirectory", filesDir); 107 | File myDir = new File(filesDir); 108 | if (!myDir.exists()) { 109 | myDir.mkdirs(); 110 | } 111 | return myDir; 112 | } 113 | 114 | public void setFilesDirectory(File file) { 115 | setFilesDirectory(file.getAbsoluteFile()); 116 | } 117 | 118 | public void setFilesDirectory(String path) { 119 | if (!path.endsWith("/calls/")) { 120 | path += "/calls/"; 121 | } 122 | preferences.edit().putString("FilesDirectory", path).commit(); 123 | } 124 | 125 | public enum OlderThan { 126 | NEVER, 127 | DAILY, 128 | THREE_DAYS, 129 | WEEKLY, 130 | MONTHLY, 131 | QUARTERLY, 132 | YEARLY 133 | } 134 | 135 | 136 | /** 137 | * get the Older Than Days for recording cleanup 138 | * 139 | * @return 0 if not set 140 | */ 141 | 142 | public OlderThan getOlderThan() { 143 | String name = preferences.getString("OlderThan", OlderThan.NEVER.name()); 144 | return OlderThan.valueOf(name); 145 | } 146 | 147 | /** 148 | * Set the Older Than date for recording cleanup 149 | * 150 | * @param olderThan NEVER default 151 | */ 152 | 153 | public void setOlderThan(OlderThan olderThan) { 154 | preferences.edit().putString("OlderThan", olderThan.name()).commit(); 155 | } 156 | 157 | 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/LocalBroadcastActions.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | /** 4 | * Created by Jeff on 04-May-16. 5 | * 6 | * Defines our INTERNAL Broadcast Actions 7 | */ 8 | public final class LocalBroadcastActions { 9 | public static String NEW_RECORDING_BROADCAST = "com.jlcsoftware.NEW_RECORDING_ACTION"; 10 | public static String RECORDING_DELETED_BROADCAST = "com.jlcsoftware.RECORDING_DELETED_BROADCAST"; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/MyContactsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.ContentUris; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Handler; 11 | import android.os.Message; 12 | import android.provider.ContactsContract; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.BaseAdapter; 17 | import android.widget.ImageView; 18 | import android.widget.TextView; 19 | 20 | import java.io.InputStream; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * Created by Jeff on 13-May-16. 26 | */ 27 | public class MyContactsAdapter extends BaseAdapter implements Handler.Callback { 28 | private List mValues; 29 | private Context context; 30 | 31 | private Handler handler; 32 | 33 | @Override 34 | public boolean handleMessage(Message msg) { 35 | // Add to the values 36 | if (null != msg.obj) { 37 | mValues.add((ContactRecord) msg.obj); 38 | notifyDataSetChanged(); 39 | } 40 | return false; 41 | } 42 | 43 | public MyContactsAdapter(Context context) { 44 | handler = new Handler(this); // Attach to this thread 45 | this.context = context; 46 | loadAdapter(context); 47 | } 48 | 49 | public class ContactRecord { 50 | String name; 51 | Drawable image; 52 | String contactId; 53 | } 54 | 55 | private void loadAdapter(final Context context) { 56 | mValues = new ArrayList(); 57 | Runnable runnable = new Runnable() { 58 | @Override 59 | public void run() { 60 | 61 | InputStream input = null; 62 | 63 | // query time 64 | Cursor cursor = context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); 65 | 66 | while (cursor.moveToNext()) { 67 | if (Integer.parseInt(cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0) { 68 | ContactRecord contactRecord = new ContactRecord(); 69 | // Get values from contacts database: 70 | contactRecord.name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); 71 | contactRecord.contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID)); 72 | // Get photo of contactId as input stream: 73 | Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactRecord.contactId)); 74 | input = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri); 75 | if (null != input) { 76 | BitmapDrawable drawable = new BitmapDrawable(context.getResources(), input); 77 | contactRecord.image = drawable; 78 | } else { 79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 80 | contactRecord.image = context.getResources().getDrawable(R.drawable.ic_user, null); 81 | } else { 82 | contactRecord.image = context.getResources().getDrawable(R.drawable.ic_user); 83 | } 84 | } 85 | 86 | // Send to the main thread... 87 | Message msg = Message.obtain(); 88 | msg.obj = contactRecord; 89 | handler.sendMessage(msg); 90 | } 91 | } 92 | } 93 | 94 | }; 95 | new Thread(runnable).start(); // run on a different thread 96 | } 97 | 98 | 99 | @Override 100 | public int getCount() { 101 | return mValues.size(); 102 | } 103 | 104 | @Override 105 | public Object getItem(int position) { 106 | return mValues.get(position); 107 | } 108 | 109 | @Override 110 | public long getItemId(int position) { 111 | return position; 112 | } 113 | 114 | @Override 115 | public View getView(int position, View convertView, ViewGroup parent) { 116 | ContactRecord record = mValues.get(position); 117 | if (null == convertView) { 118 | convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false); 119 | } 120 | ImageView imageView = (ImageView) convertView.findViewById(R.id.imageView); 121 | imageView.setImageDrawable(record.image); 122 | TextView textView = (TextView) convertView.findViewById(R.id.textView); 123 | textView.setText(record.name); 124 | return convertView; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/MyWhitelistItemRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.ContentUris; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Handler; 10 | import android.os.Message; 11 | import android.provider.ContactsContract; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.util.SparseBooleanArray; 14 | import android.view.LayoutInflater; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.ImageView; 19 | import android.widget.TextView; 20 | 21 | import com.jlcsoftware.callrecorder.WhitelistFragment.OnListFragmentInteractionListener; 22 | import com.jlcsoftware.database.Database; 23 | import com.jlcsoftware.database.Whitelist; 24 | 25 | import java.io.InputStream; 26 | import java.util.ArrayList; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | /** 32 | * {@link RecyclerView.Adapter} that can display a {@link WhitelistRecord} and makes a call to the 33 | * specified {@link OnListFragmentInteractionListener}. 34 | */ 35 | public class MyWhitelistItemRecyclerViewAdapter extends RecyclerView.Adapter implements Handler.Callback { 36 | 37 | public interface OnListInteractionListener { 38 | void onListInteraction(WhitelistRecord[] items); 39 | boolean onListLongClick(View v,WhitelistRecord item); 40 | } 41 | 42 | private static int WHAT_NOTIFY_CHANGES = 1; 43 | 44 | private List mValues; 45 | private final OnListInteractionListener mListener; 46 | private SparseBooleanArray selectedItems; 47 | 48 | public boolean isSelected(int pos){ 49 | return selectedItems.get(pos, false); 50 | } 51 | 52 | public void toggleSelection(int pos) { 53 | if (selectedItems.get(pos, false)) { 54 | selectedItems.delete(pos); 55 | } 56 | else { 57 | selectedItems.put(pos, true); 58 | } 59 | notifyItemChanged(pos); 60 | } 61 | 62 | public void clearSelections() { 63 | selectedItems.clear(); 64 | notifyDataSetChanged(); 65 | } 66 | 67 | public int getSelectedItemCount() { 68 | return selectedItems.size(); 69 | } 70 | 71 | public List getSelectedItems() { 72 | List items = 73 | new ArrayList(selectedItems.size()); 74 | for (int i = 0; i < selectedItems.size(); i++) { 75 | items.add(selectedItems.keyAt(i)); 76 | } 77 | return items; 78 | } 79 | private Handler handler; 80 | 81 | @Override 82 | public boolean handleMessage(Message msg) { 83 | if(msg.what == WHAT_NOTIFY_CHANGES) { 84 | // Add to the values 85 | if (null != msg.obj) { 86 | mValues.add((WhitelistRecord) msg.obj); 87 | } 88 | notifyDataSetChanged(); 89 | } 90 | return false; 91 | } 92 | 93 | Context context; 94 | 95 | public MyWhitelistItemRecyclerViewAdapter(Context context, OnListInteractionListener listener) { 96 | handler = new Handler(this); 97 | selectedItems = new SparseBooleanArray(); 98 | mListener = listener; 99 | this.context = context; 100 | loadAdapter(context); 101 | } 102 | 103 | private void loadAdapter(final Context context) { 104 | mValues = new ArrayList(); 105 | // Using a Runnable and a separate Thread with Message on the UI thread 106 | // is a nice way to make the list display more interactive 107 | // no need for a progress indicator, since the user can see the list populating 108 | Runnable runnable = new Runnable() { 109 | @Override 110 | public void run() { 111 | final ArrayList all = Database.getInstance(context).getAllWhitelist(); 112 | for (Whitelist whitelist : all) { 113 | WhitelistRecord record = new WhitelistRecord(whitelist); 114 | resolveContactInfo(context, record); 115 | // Send to the main thread... 116 | Message msg = Message.obtain(); 117 | msg.what = WHAT_NOTIFY_CHANGES; 118 | msg.obj = record; 119 | handler.sendMessage(msg); 120 | } 121 | if(all.size()==0){ 122 | Message msg = Message.obtain(); 123 | msg.what = WHAT_NOTIFY_CHANGES; 124 | msg.obj = null; 125 | handler.sendMessage(msg); 126 | } 127 | } 128 | }; 129 | new Thread(runnable).start(); 130 | } 131 | 132 | 133 | @Override 134 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 135 | View view = LayoutInflater.from(parent.getContext()) 136 | .inflate(R.layout.fragment_whitelist_item, parent, false); 137 | return new ViewHolder(view); 138 | } 139 | 140 | @Override 141 | public void onBindViewHolder(final ViewHolder holder, final int position) { 142 | holder.mItem = mValues.get(position); 143 | holder.mImageView.setImageDrawable(mValues.get(position).getImage()); 144 | holder.mNameView.setText(mValues.get(position).getName()); 145 | 146 | 147 | holder.mView.setOnClickListener(new View.OnClickListener() { 148 | @Override 149 | public void onClick(View v) { 150 | if (null != mListener) { 151 | toggleSelection(position); 152 | holder.itemView.setSelected(selectedItems.get(position, false)); 153 | // Notify the active callbacks interface (the activity, if the 154 | // fragment is attached to one) that an item has been selected. 155 | mListener.onListInteraction(getSelectedRecords()); 156 | } 157 | } 158 | }); 159 | holder.mView.setOnLongClickListener(new View.OnLongClickListener() { 160 | @Override 161 | public boolean onLongClick(View v) { 162 | if (null != mListener) { 163 | if(!isSelected(position)) toggleSelection(position); 164 | // Notify the active callbacks interface (the activity, if the 165 | // fragment is attached to one) that an item has been selected. 166 | return mListener.onListLongClick(v,holder.mItem); 167 | } 168 | return false; 169 | } 170 | }); 171 | // Selection State 172 | holder.itemView.setSelected(selectedItems.get(position, false)); 173 | } 174 | 175 | public WhitelistRecord[] getSelectedRecords() { 176 | ArrayList selected = new ArrayList<>(); 177 | List items = getSelectedItems(); 178 | for(Integer pos : items){ 179 | selected.add(mValues.get(pos)); 180 | } 181 | return selected.toArray(new WhitelistRecord[selected.size()]); 182 | } 183 | 184 | @Override 185 | public int getItemCount() { 186 | return mValues.size(); 187 | } 188 | 189 | public class ViewHolder extends RecyclerView.ViewHolder { 190 | public final View mView; 191 | public final ImageView mImageView; 192 | public final TextView mNameView; 193 | public WhitelistRecord mItem; 194 | 195 | public ViewHolder(View view) { 196 | super(view); 197 | mView = view; 198 | mImageView = (ImageView) view.findViewById(R.id.imageView); 199 | mNameView = (TextView) view.findViewById(R.id.textView); 200 | 201 | } 202 | 203 | @Override 204 | public String toString() { 205 | return super.toString() + " '" + mNameView.getText() + "'"; 206 | } 207 | } 208 | 209 | /** 210 | * Get the contact info details from the Contacts Content Provider 211 | * @param context 212 | * @param record 213 | */ 214 | private void resolveContactInfo(Context context, WhitelistRecord record) { 215 | String name = null; 216 | InputStream input = null; 217 | 218 | // define the columns the query should return 219 | String[] projection = new String[]{ContactsContract.Contacts.DISPLAY_NAME}; 220 | // encode the phone number and build the filter URI 221 | Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(record.getContactId())); 222 | // query time 223 | Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); 224 | 225 | if (cursor.moveToFirst()) { 226 | // Get values from contacts database: 227 | name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); 228 | record.setName(name); 229 | 230 | // Get photo of contactId as input stream: 231 | uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(record.getContactId())); 232 | input = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri); 233 | 234 | if (null != input) { 235 | BitmapDrawable drawable = new BitmapDrawable(context.getResources(), input); 236 | record.setImage(drawable); 237 | } else { 238 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 239 | record.setImage(context.getResources().getDrawable(R.drawable.ic_user, null)); 240 | } else { 241 | record.setImage(context.getResources().getDrawable(R.drawable.ic_user)); 242 | } 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * Something changed, reload the adapter 249 | */ 250 | public void refresh() { 251 | clearSelections(); 252 | loadAdapter(context); 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/PhoneCallRecord.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import com.jlcsoftware.database.CallLog; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by Jeff on 02-May-16. 13 | * 14 | * Info about a phone call recording 15 | */ 16 | public class PhoneCallRecord { 17 | 18 | // Cache of Contact Pictures to minimize image memory use... 19 | private static Map synchronizedMap = Collections.synchronizedMap(new HashMap()); 20 | 21 | CallLog phoneCall; 22 | 23 | public PhoneCallRecord(CallLog phoneCall) { 24 | this.phoneCall = phoneCall; 25 | } 26 | 27 | public void setImage(Drawable photo) { 28 | synchronizedMap.put(phoneCall.getPhoneNumber(), photo); 29 | } 30 | 31 | /** 32 | * Get the Contact image from the cache... 33 | * 34 | * @return NULL if there isn't an Image in the cache 35 | */ 36 | public Drawable getImage() { 37 | Drawable drawable = synchronizedMap.get(phoneCall.getPhoneNumber()); 38 | return drawable; 39 | } 40 | 41 | private String contactId; 42 | 43 | public void setContactId(String contactId) { 44 | this.contactId = contactId; 45 | } 46 | 47 | public String getContactId() { 48 | return contactId; 49 | } 50 | 51 | private String name; 52 | 53 | public void setName(String name) { 54 | this.name = name; 55 | } 56 | 57 | public String getName() { 58 | if(null==name){ 59 | return phoneCall.getPhoneNumber(); 60 | } 61 | return name; 62 | } 63 | 64 | CallLog getPhoneCall() { 65 | return phoneCall; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/RecordingFragment.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.Context; 4 | import android.content.IntentFilter; 5 | import android.os.Bundle; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.content.LocalBroadcastManager; 8 | import android.support.v7.widget.GridLayoutManager; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.view.LayoutInflater; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | 16 | import com.google.android.gms.ads.AdView; 17 | import com.jlcsoftware.receivers.MyLocalBroadcastReceiver; 18 | 19 | /** 20 | * A fragment representing a list of Items. 21 | *

22 | * Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener} 23 | * interface. 24 | */ 25 | public class RecordingFragment extends Fragment { 26 | 27 | private static final String ARG_COLUMN_COUNT = "ARG_COLUMN_COUNT"; 28 | private static final String ARG_TYPE = "ARG_TYPE"; 29 | 30 | private int mColumnCount = 1; 31 | private OnListFragmentInteractionListener mListener; 32 | 33 | /** 34 | * Mandatory empty constructor for the fragment manager to instantiate the 35 | * fragment (e.g. upon screen orientation changes). 36 | */ 37 | public RecordingFragment() { 38 | } 39 | 40 | public enum SORT_TYPE { 41 | ALL, 42 | INCOMING, 43 | OUTGOING 44 | } 45 | 46 | public static RecordingFragment newInstance(int columnCount, SORT_TYPE type) { 47 | RecordingFragment fragment = new RecordingFragment(); 48 | Bundle args = new Bundle(); 49 | args.putInt(ARG_COLUMN_COUNT, columnCount); 50 | args.putString(ARG_TYPE, type.name()); 51 | fragment.setArguments(args); 52 | return fragment; 53 | } 54 | 55 | SORT_TYPE type; 56 | 57 | @Override 58 | public void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | if (getArguments() != null) { 62 | mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT); 63 | type = SORT_TYPE.valueOf(getArguments().getString(ARG_TYPE)); 64 | } 65 | } 66 | 67 | MyRecordingRecyclerViewAdapter adapter; 68 | MyLocalBroadcastReceiver newRecordingReceiver; 69 | 70 | @Override 71 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 72 | Bundle savedInstanceState) { 73 | View view = inflater.inflate(R.layout.fragment_recording_list, container, false); 74 | 75 | 76 | // Set the adapter 77 | RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); 78 | Context context = view.getContext(); 79 | if (mColumnCount <= 1) { 80 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 81 | } else { 82 | recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); 83 | } 84 | adapter = new MyRecordingRecyclerViewAdapter(context, type, mListener); 85 | newRecordingReceiver = new MyLocalBroadcastReceiver(adapter); 86 | LocalBroadcastManager.getInstance(getContext()).registerReceiver(newRecordingReceiver, 87 | new IntentFilter(LocalBroadcastActions.NEW_RECORDING_BROADCAST)); 88 | LocalBroadcastManager.getInstance(getContext()).registerReceiver(newRecordingReceiver, 89 | new IntentFilter(LocalBroadcastActions.RECORDING_DELETED_BROADCAST)); 90 | 91 | recyclerView.setAdapter(adapter); 92 | recyclerView.setOnTouchListener(new View.OnTouchListener() { 93 | @Override 94 | public boolean onTouch(View v, MotionEvent event) { 95 | if (null != mListener) mListener.onListFragmentInteraction(new PhoneCallRecord[0]); 96 | return false; 97 | } 98 | }); 99 | return view; 100 | } 101 | 102 | @Override 103 | public void onDestroy() { 104 | super.onDestroy(); 105 | if (null != newRecordingReceiver) 106 | LocalBroadcastManager.getInstance(getContext()).unregisterReceiver( 107 | newRecordingReceiver); 108 | } 109 | 110 | @Override 111 | public void onResume() { 112 | super.onResume(); 113 | } 114 | 115 | @Override 116 | public void onAttach(Context context) { 117 | super.onAttach(context); 118 | if (context instanceof OnListFragmentInteractionListener) { 119 | mListener = (OnListFragmentInteractionListener) context; 120 | } else { 121 | throw new RuntimeException(context.toString() 122 | + " must implement OnListFragmentInteractionListener"); 123 | } 124 | } 125 | 126 | @Override 127 | public void onDetach() { 128 | super.onDetach(); 129 | mListener = null; 130 | } 131 | 132 | /** 133 | * This interface must be implemented by activities that contain this 134 | * fragment to allow an interaction in this fragment to be communicated 135 | * to the activity and potentially other fragments contained in that 136 | * activity. 137 | *

138 | * See the Android Training lesson Communicating with Other Fragments for more information. 141 | */ 142 | public interface OnListFragmentInteractionListener { 143 | void onListFragmentInteraction(PhoneCallRecord items[]); 144 | 145 | void onItemPlay(PhoneCallRecord item); 146 | 147 | boolean onListItemLongClick(View v, PhoneCallRecord selectedItem, PhoneCallRecord items[]); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.os.StatFs; 6 | import android.support.v4.content.ContextCompat; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.text.Html; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.AdapterView; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.Button; 14 | import android.widget.CheckBox; 15 | import android.widget.CompoundButton; 16 | import android.widget.Spinner; 17 | import android.widget.TextView; 18 | 19 | import com.jlcsoftware.database.CallLog; 20 | import com.jlcsoftware.database.Database; 21 | import com.jlcsoftware.receivers.MyAlarmReceiver; 22 | import com.jlcsoftware.services.CleanupService; 23 | 24 | import java.io.File; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | /** 29 | * Not your classic Android Settings Activity, since 30 | * we are only supporting a very limited range of options and PreferenceActivity is essentially deprecated 31 | * and all the Fragment stuff is over-kill 32 | * 33 | * a new PreferencesFragment based Activity is overkill and the old PreferenceActivity never made much sense anyway 34 | */ 35 | 36 | public class SettingsActivity extends AppCompatActivity { 37 | 38 | class MyArrayAdapter extends ArrayAdapter { 39 | 40 | ArrayList icons; 41 | 42 | public MyArrayAdapter(Context context, List objects, ArrayList icons) { 43 | super(context, android.R.layout.simple_spinner_item, objects); 44 | this.icons = icons; 45 | } 46 | 47 | @Override 48 | public View getView(int position, View convertView, ViewGroup parent) { 49 | View view = super.getView(position, convertView, parent); 50 | ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(icons.get(position), 0, 0, 0); 51 | return view; 52 | } 53 | } 54 | 55 | @Override 56 | public void onStop() { 57 | final AppPreferences.OlderThan olderThan = AppPreferences.getInstance(this).getOlderThan(); 58 | if (olderThan != AppPreferences.OlderThan.NEVER) { 59 | MyAlarmReceiver.setAlarm(SettingsActivity.this); 60 | } else { 61 | MyAlarmReceiver.cancleAlarm(SettingsActivity.this); 62 | } 63 | super.onStop(); 64 | } 65 | 66 | AppPreferences preferences; 67 | 68 | @Override 69 | protected void onCreate(Bundle savedInstanceState) { 70 | super.onCreate(savedInstanceState); 71 | setContentView(R.layout.activity_settings); 72 | 73 | 74 | preferences = AppPreferences.getInstance(this); 75 | 76 | CheckBox checkBox = (CheckBox) findViewById(R.id.checkBox); 77 | checkBox.setChecked(preferences.isRecordingIncomingEnabled()); 78 | checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 79 | @Override 80 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 81 | preferences.setRecordingIncomingEnabled(isChecked); 82 | } 83 | }); 84 | checkBox = (CheckBox) findViewById(R.id.checkBox2); 85 | checkBox.setChecked(preferences.isRecordingOutgoingEnabled()); 86 | checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 87 | @Override 88 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 89 | preferences.setRecordingOutgoingEnabled(isChecked); 90 | } 91 | }); 92 | 93 | File[] externalFilesDirs = new ContextCompat().getExternalFilesDirs(this, null); 94 | Spinner spinner = (Spinner) findViewById(R.id.spinner); 95 | List list = new ArrayList(); 96 | ArrayList icons = new ArrayList<>(); 97 | 98 | File filesDir = getFilesDir(); 99 | list.add(filesDir.getAbsolutePath()); 100 | icons.add(R.drawable.ic_folder_black_24dp); 101 | 102 | for (File file : externalFilesDirs) { 103 | list.add(file.getAbsolutePath()); 104 | icons.add(R.drawable.ic_cards_black_24); 105 | } 106 | final MyArrayAdapter dataAdapter = new MyArrayAdapter(this, list, icons); 107 | spinner.setAdapter(dataAdapter); 108 | spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 109 | @Override 110 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 111 | String path = dataAdapter.getItem(position); 112 | calcFreeSpace(path); 113 | AppPreferences.getInstance(getApplicationContext()).setFilesDirectory(path); 114 | } 115 | 116 | @Override 117 | public void onNothingSelected(AdapterView parent) { 118 | 119 | } 120 | }); 121 | String path = AppPreferences.getInstance(getApplicationContext()).getFilesDirectory().getAbsolutePath(); 122 | spinner.setSelection(dataAdapter.getPosition(path.replace("/calls/", ""))); 123 | calcFreeSpace(path); 124 | 125 | 126 | // Now, count the recordings 127 | ArrayList allCalls = Database.getInstance(getApplicationContext()).getAllCalls(); 128 | TextView textView = (TextView) findViewById(R.id.textView4); 129 | String str = textView.getText().toString(); 130 | str = String.format(str, allCalls.size()); 131 | textView.setText(Html.fromHtml(str)); 132 | 133 | // Get the length of each file... 134 | long length = 0; 135 | for (CallLog call : allCalls) { 136 | File file = new File(call.getPathToRecording()); 137 | length += file.length(); 138 | } 139 | textView = (TextView) findViewById(R.id.textView5); 140 | str = textView.getText().toString(); 141 | str = String.format(str, length / 1024); 142 | textView.setText(Html.fromHtml(str)); 143 | 144 | spinner = (Spinner) findViewById(R.id.spinner2); 145 | spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 146 | @Override 147 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 148 | // Obviously MUST be in the same order as AppPreferences.OlderThan enum 149 | final AppPreferences.OlderThan olderThan = AppPreferences.OlderThan.values()[position]; 150 | AppPreferences.getInstance(getApplicationContext()).setOlderThan(olderThan); 151 | } 152 | 153 | @Override 154 | public void onNothingSelected(AdapterView parent) { 155 | 156 | } 157 | }); 158 | spinner.setSelection(AppPreferences.getInstance(getApplicationContext()).getOlderThan().ordinal()); 159 | 160 | Button button = (Button) findViewById(R.id.button); 161 | button.setOnClickListener(new View.OnClickListener() { 162 | @Override 163 | public void onClick(View v) { 164 | CleanupService.sartCleaning(SettingsActivity.this); 165 | } 166 | }); 167 | } 168 | 169 | 170 | private void calcFreeSpace(String path) { 171 | // http://stackoverflow.com/questions/3394765/how-to-check-available-space-on-android-device-on-mini-sd-card 172 | StatFs stat = new StatFs(path); 173 | long bytesTotal = 0; 174 | long bytesAvailable = 0; 175 | float megAvailable = 0; 176 | long megTotalAvailable = 0; 177 | 178 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { 179 | bytesTotal = (long) stat.getBlockSizeLong() * (long) stat.getBlockCountLong(); 180 | bytesAvailable = (long) stat.getBlockSizeLong() * (long) stat.getAvailableBlocksLong(); 181 | } else { 182 | bytesTotal = (long) stat.getBlockSize() * (long) stat.getBlockCount(); 183 | bytesAvailable = (long) stat.getBlockSize() * (long) stat.getAvailableBlocks(); 184 | } 185 | megAvailable = bytesAvailable / 1048576; 186 | megTotalAvailable = bytesTotal / 1048576; 187 | 188 | // Free Space 189 | TextView textView = (TextView) findViewById(R.id.textView6); 190 | String str = getString(R.string.pref_folder_total_folder_size); 191 | str = String.format(str, megAvailable); 192 | textView.setText(Html.fromHtml(str)); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/WhitelistActivity.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | 8 | /** 9 | * Manage our Whitelisted Contacts (don't record them) 10 | */ 11 | 12 | public class WhitelistActivity extends AppCompatActivity implements WhitelistFragment.OnListFragmentInteractionListener { 13 | 14 | Menu optionsMenu; 15 | WhitelistFragment fragment; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_whitelist); 21 | } 22 | 23 | @Override 24 | public boolean onCreateOptionsMenu(Menu menu) { 25 | // Inflate the menu; this adds items to the action bar if it is present. 26 | getMenuInflater().inflate(R.menu.menu_whitelist, menu); 27 | fragment = (WhitelistFragment) getSupportFragmentManager().findFragmentById(R.id.fragment); 28 | optionsMenu = menu; 29 | 30 | return true; 31 | } 32 | 33 | @Override 34 | public boolean onOptionsItemSelected(MenuItem item) { 35 | // Handle action bar item clicks here. The action bar will 36 | // automatically handle clicks on the Home/Up button, so long 37 | // as you specify a parent activity in AndroidManifest.xml. 38 | int id = item.getItemId(); 39 | 40 | //noinspection SimplifiableIfStatement 41 | if (id == R.id.action_add) { 42 | fragment.addContact(); 43 | return true; 44 | } 45 | 46 | if (id == R.id.action_delete) { 47 | fragment.removeSelectedContacts(); 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | @Override 55 | public void onListFragmentInteraction(WhitelistRecord[] item) { 56 | MenuItem menuItem = optionsMenu.findItem(R.id.action_delete); 57 | menuItem.setVisible(item.length>0); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/WhitelistFragment.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.os.AsyncTask; 7 | import android.os.Bundle; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v7.app.AlertDialog; 10 | import android.support.v7.widget.GridLayoutManager; 11 | import android.support.v7.widget.LinearLayoutManager; 12 | import android.support.v7.widget.PopupMenu; 13 | import android.support.v7.widget.RecyclerView; 14 | import android.view.LayoutInflater; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.AdapterView; 19 | import android.widget.ListView; 20 | 21 | import com.jlcsoftware.database.Database; 22 | import com.jlcsoftware.database.Whitelist; 23 | 24 | 25 | /** 26 | * A fragment representing a list of Items. 27 | *

28 | * Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener} 29 | * interface. 30 | */ 31 | public class WhitelistFragment extends Fragment implements MyWhitelistItemRecyclerViewAdapter.OnListInteractionListener { 32 | 33 | private static final String ARG_COLUMN_COUNT = "ARG_COLUMN_COUNT"; 34 | private int mColumnCount = 1; 35 | private OnListFragmentInteractionListener mListener; 36 | 37 | /** 38 | * Mandatory empty constructor for the fragment manager to instantiate the 39 | * fragment (e.g. upon screen orientation changes). 40 | */ 41 | public WhitelistFragment() { 42 | } 43 | 44 | public static WhitelistFragment newInstance(int columnCount) { 45 | WhitelistFragment fragment = new WhitelistFragment(); 46 | Bundle args = new Bundle(); 47 | args.putInt(ARG_COLUMN_COUNT, columnCount); 48 | fragment.setArguments(args); 49 | return fragment; 50 | } 51 | 52 | @Override 53 | public void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | 56 | if (getArguments() != null) { 57 | mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT); 58 | } 59 | } 60 | 61 | MyWhitelistItemRecyclerViewAdapter adapter; 62 | 63 | @Override 64 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 65 | Bundle savedInstanceState) { 66 | View view = inflater.inflate(R.layout.fragment_whitelist_item_list, container, false); 67 | 68 | // Set the adapter 69 | if (view instanceof RecyclerView) { 70 | Context context = view.getContext(); 71 | RecyclerView recyclerView = (RecyclerView) view; 72 | if (mColumnCount <= 1) { 73 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 74 | } else { 75 | recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); 76 | } 77 | recyclerView.setAdapter(adapter = new MyWhitelistItemRecyclerViewAdapter(getContext(), this)); 78 | } 79 | return view; 80 | } 81 | 82 | 83 | @Override 84 | public void onAttach(Context context) { 85 | super.onAttach(context); 86 | if (context instanceof OnListFragmentInteractionListener) { 87 | mListener = (OnListFragmentInteractionListener) context; 88 | } else { 89 | throw new RuntimeException(context.toString() 90 | + " must implement OnListFragmentInteractionListener"); 91 | } 92 | } 93 | 94 | @Override 95 | public void onDetach() { 96 | super.onDetach(); 97 | mListener = null; 98 | } 99 | 100 | /** 101 | * This interface must be implemented by activities that contain this 102 | * fragment to allow an interaction in this fragment to be communicated 103 | * to the activity and potentially other fragments contained in that 104 | * activity. 105 | *

106 | * See the Android Training lesson Communicating with Other Fragments for more information. 109 | */ 110 | public interface OnListFragmentInteractionListener { 111 | void onListFragmentInteraction(WhitelistRecord[] item); 112 | } 113 | 114 | 115 | @Override 116 | public void onListInteraction(WhitelistRecord[] item) { 117 | mListener.onListFragmentInteraction(item); 118 | } 119 | 120 | @Override 121 | public boolean onListLongClick(View v, final WhitelistRecord record) { 122 | PopupMenu popupMenu = new PopupMenu(getContext(), v); 123 | popupMenu.getMenuInflater().inflate(R.menu.menu_whitelist_popup, popupMenu.getMenu()); 124 | 125 | popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 126 | @Override 127 | public boolean onMenuItemClick(MenuItem item) { 128 | if (item.getItemId() == R.id.action_delete) { 129 | removeSelectedContacts(); 130 | return true; 131 | } 132 | return false; 133 | } 134 | }); 135 | popupMenu.show(); 136 | return true; // handled 137 | } 138 | 139 | 140 | public void removeSelectedContacts() { 141 | AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); 142 | alert.setTitle(R.string.delete_whitelist_title); 143 | alert.setMessage(R.string.delete_whitelist_subject); 144 | alert.setPositiveButton(R.string.dialog_yes, new DialogInterface.OnClickListener() { 145 | 146 | @Override 147 | public void onClick(DialogInterface dialog, int which) { 148 | 149 | AsyncTask asyncTask = new AsyncTask() { 150 | 151 | 152 | @Override 153 | protected Void doInBackground(Void... params) { 154 | Database whitelistDB = Database.getInstance(getContext()); 155 | 156 | WhitelistRecord[] records = adapter.getSelectedRecords(); 157 | for (WhitelistRecord record : records) { 158 | int id = record.whitelist.getId(); 159 | whitelistDB.removeWhiteList(id); 160 | } 161 | return null; 162 | } 163 | 164 | @Override 165 | protected void onPostExecute(Void aVoid) { 166 | super.onPostExecute(aVoid); 167 | // need to run this on main thread.... 168 | refresh(); 169 | } 170 | }; 171 | asyncTask.execute(); 172 | dialog.dismiss(); 173 | } 174 | 175 | }); 176 | alert.setNegativeButton(R.string.dialog_no, new DialogInterface.OnClickListener() { 177 | 178 | @Override 179 | public void onClick(DialogInterface dialog, int which) { 180 | 181 | dialog.dismiss(); 182 | } 183 | }); 184 | 185 | alert.show(); 186 | 187 | } 188 | 189 | public void addContact() { 190 | ListView listView = new ListView(getContext()); 191 | final MyContactsAdapter adapter = new MyContactsAdapter(getContext()); 192 | listView.setAdapter(adapter); 193 | 194 | 195 | AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); 196 | 197 | builder.setTitle(R.string.add_contact); 198 | builder.setView(listView); 199 | 200 | builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { 201 | 202 | @Override 203 | public void onClick(DialogInterface dialog, int which) { 204 | // Do nothing 205 | dialog.dismiss(); 206 | } 207 | }); 208 | 209 | final Dialog dialog = builder.create(); 210 | listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 211 | @Override 212 | public void onItemClick(AdapterView parent, View view, int position, long id) { 213 | MyContactsAdapter.ContactRecord contactRecord = (MyContactsAdapter.ContactRecord) adapter.getItem(position); 214 | addContact(contactRecord.contactId); 215 | dialog.dismiss(); 216 | } 217 | }); 218 | dialog.show(); 219 | } 220 | 221 | 222 | public void addContact(String contactId) { 223 | Whitelist whitelist = new Whitelist(); 224 | whitelist.setContactId(contactId); 225 | Database.getInstance(getContext()).addWhitelist(whitelist); 226 | refresh(); 227 | } 228 | 229 | 230 | private void refresh() { 231 | adapter.refresh(); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/callrecorder/WhitelistRecord.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.callrecorder; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.graphics.drawable.Drawable; 6 | import android.net.Uri; 7 | import android.provider.ContactsContract; 8 | 9 | import com.jlcsoftware.database.Database; 10 | import com.jlcsoftware.database.Whitelist; 11 | 12 | /** 13 | * Created by Jeff on 06-May-16. 14 | * 15 | * A whilelisted contact 16 | */ 17 | public class WhitelistRecord { 18 | 19 | Whitelist whitelist; 20 | 21 | public WhitelistRecord(Whitelist whitelist) { 22 | this.whitelist = whitelist; 23 | } 24 | 25 | public void setContactId(String contactId) { 26 | whitelist.setContactId(contactId); 27 | } 28 | 29 | public String getContactId() { 30 | return whitelist.getContactId(); 31 | } 32 | 33 | private String name; 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | public void setImage(Drawable photo) { 44 | drawable = photo; 45 | } 46 | 47 | Drawable drawable; 48 | 49 | /** 50 | * Get the Contact image from the cache... 51 | * 52 | * @return NULL if there isn't an Image in the cache 53 | */ 54 | 55 | public Drawable getImage() { 56 | return drawable; 57 | } 58 | 59 | public static boolean recordCaller(Context context, String phoneNumber, boolean defaultValue){ 60 | String contactId = null; 61 | 62 | // define the columns the query should return 63 | String[] projection = new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME, ContactsContract.PhoneLookup._ID}; 64 | // encode the phone number and build the filter URI 65 | Uri contactUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); 66 | // query time 67 | Cursor cursor = context.getContentResolver().query(contactUri, projection, null, null, null); 68 | 69 | if (cursor.moveToFirst()) { 70 | // Get values from contacts database: 71 | contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 72 | Whitelist contact = Database.getInstance(context).getContact(contactId); 73 | if(null!=contact) return false; 74 | } 75 | 76 | return defaultValue; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/database/CallLog.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.database; 2 | 3 | import android.content.ContentValues; 4 | import android.content.Context; 5 | 6 | import java.util.Calendar; 7 | import java.util.GregorianCalendar; 8 | 9 | /** 10 | * Created by Jeff on 01-May-16. 11 | *

12 | * A Call Log record from our database 13 | */ 14 | public class CallLog { 15 | 16 | static final int VERSION = 1; 17 | 18 | boolean isNew = true; 19 | 20 | public ContentValues getContent() { 21 | return content; 22 | } 23 | 24 | private ContentValues content; 25 | 26 | public CallLog() { 27 | content = new ContentValues(); 28 | content.put(Database.CALL_RECORDS_TABLE_OUTGOING, false); // default "outgoing" false (therefore incoming) 29 | content.put(Database.CALL_RECORDS_TABLE_KEEP, false); // default "keep" false (therefore allow automatic deletion) 30 | } 31 | 32 | public CallLog(ContentValues content) { 33 | this.content = content; 34 | } 35 | 36 | public String getPhoneNumber() { 37 | String phone = content.getAsString(Database.CALL_RECORDS_TABLE_PHONE_NUMBER); 38 | if(null==phone) return ""; 39 | return phone; 40 | } 41 | 42 | public void setPhoneNumber(String phoneNumer) { 43 | content.put(Database.CALL_RECORDS_TABLE_PHONE_NUMBER, phoneNumer); 44 | } 45 | 46 | public int getId() { 47 | return content.getAsInteger(Database.CALL_RECORDS_TABLE_ID); 48 | } 49 | 50 | public boolean isOutgoing() { 51 | return content.getAsBoolean(Database.CALL_RECORDS_TABLE_OUTGOING); // Outgoing is "true" Incoming is "false" 52 | } 53 | 54 | public void setOutgoing() { 55 | content.put(Database.CALL_RECORDS_TABLE_OUTGOING, true); 56 | } 57 | 58 | 59 | public Calendar getStartTime() { 60 | Long time = content.getAsLong(Database.CALL_RECORDS_TABLE_START_DATE); 61 | Calendar cal = GregorianCalendar.getInstance(); 62 | if (null != time) cal.setTimeInMillis(time); 63 | return cal; 64 | } 65 | 66 | public void setSartTime(Calendar cal) { 67 | content.put(Database.CALL_RECORDS_TABLE_START_DATE, cal.getTimeInMillis()); 68 | } 69 | 70 | public Calendar getEndTime() { 71 | Long time = content.getAsLong(Database.CALL_RECORDS_TABLE_END_DATE); 72 | Calendar cal = GregorianCalendar.getInstance(); 73 | if (null != time) cal.setTimeInMillis(time); 74 | return cal; 75 | } 76 | 77 | 78 | public void setEndTime(Calendar cal) { 79 | content.put(Database.CALL_RECORDS_TABLE_END_DATE, cal.getTimeInMillis()); 80 | } 81 | 82 | public String getPathToRecording() { 83 | String path = content.getAsString(Database.CALL_RECORDS_TABLE_RECORDING_PATH); 84 | if(path==null) return ""; 85 | return path; 86 | } 87 | 88 | public void setPathToRecording(String path) { 89 | content.put(Database.CALL_RECORDS_TABLE_RECORDING_PATH, path); 90 | } 91 | 92 | public boolean isKept() { 93 | Boolean asBoolean = content.getAsBoolean(Database.CALL_RECORDS_TABLE_KEEP); 94 | if(null==asBoolean) return false; 95 | return asBoolean; 96 | } 97 | 98 | public void setKept(boolean keep){ 99 | content.put(Database.CALL_RECORDS_TABLE_KEEP, keep); 100 | } 101 | 102 | public void save(Context context) { 103 | Database.getInstance(context).addCall(this); 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/database/Database.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.database; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.DatabaseUtils; 6 | import android.database.SQLException; 7 | import android.database.sqlite.SQLiteDatabase; 8 | import android.database.sqlite.SQLiteOpenHelper; 9 | import android.net.Uri; 10 | import android.provider.ContactsContract; 11 | 12 | import java.io.File; 13 | import java.util.ArrayList; 14 | 15 | /** 16 | * Created by Jeff on 06-May-16. 17 | *

18 | * Our SQLlite database 19 | */ 20 | public class Database extends SQLiteOpenHelper { 21 | 22 | public static String NAME = "callRecorder"; 23 | public static int VERSION = 1; 24 | 25 | String CREATE_CALL_RECORDS_TABLE = "CREATE TABLE records(_id INTEGER PRIMARY KEY, phone_number TEXT, outgoing INTEGER, start_date_time INTEGER, end_date_time INTEGER, path_to_recording TEXT, keep INTEGER DEFAULT 0, backup_state INTEGER DEFAULT 0 )"; 26 | public static String CALL_RECORDS_TABLE = "records"; 27 | public static String CALL_RECORDS_TABLE_ID = "_id"; // only because of https://developer.android.com/reference/android/widget/CursorAdapter.html 28 | public static String CALL_RECORDS_TABLE_PHONE_NUMBER = "phone_number"; 29 | public static String CALL_RECORDS_TABLE_OUTGOING = "outgoing"; 30 | public static String CALL_RECORDS_TABLE_START_DATE = "start_date_time"; 31 | public static String CALL_RECORDS_TABLE_END_DATE = "end_date_time"; 32 | public static String CALL_RECORDS_TABLE_RECORDING_PATH = "path_to_recording"; 33 | public static String CALL_RECORDS_TABLE_KEEP = "keep"; 34 | public static String CALL_RECORDS_BACKUP_STATE = "backup_state"; 35 | 36 | public static String CREATE_WHITELIST_TABLE = "CREATE TABLE whitelist( _id INTEGER PRIMARY KEY, contact_id TEXT, record INTEGER )"; 37 | public static String WHITELIST_TABLE = "whitelist"; 38 | public static String WHITELIST_TABLE_ID = "_id"; // only because of https://developer.android.com/reference/android/widget/CursorAdapter.html 39 | public static String WHITELIST_TABLE_CONTACT_ID = "contact_id"; 40 | public static String WHITELIST_TABLE_RECORD = "record"; 41 | 42 | private static Database instance; 43 | 44 | public static synchronized Database getInstance(Context context) { 45 | if (instance == null) { 46 | instance = new Database(context.getApplicationContext()); 47 | } 48 | return instance; 49 | } 50 | 51 | private Database(Context context) { 52 | super(context, Database.NAME, null, Database.VERSION); 53 | } 54 | 55 | @Override 56 | public synchronized void onCreate(SQLiteDatabase db) { 57 | try { 58 | db.execSQL(CREATE_CALL_RECORDS_TABLE); 59 | } catch (SQLException e) { 60 | e.printStackTrace(); 61 | } 62 | try { 63 | db.execSQL(CREATE_WHITELIST_TABLE); 64 | } catch (SQLException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | @Override 70 | public synchronized void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 71 | if (oldVersion < 2) { 72 | } 73 | } 74 | 75 | 76 | private CallLog getCallLogFrom(Cursor cursor) { 77 | CallLog phoneCall = new CallLog(); 78 | phoneCall.isNew = false; 79 | 80 | // String[] columnNames = cursor.getColumnNames(); 81 | 82 | int index = cursor.getColumnIndex(CALL_RECORDS_TABLE_ID); 83 | phoneCall.getContent().put(CALL_RECORDS_TABLE_ID, cursor.getInt(index)); 84 | 85 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_PHONE_NUMBER); 86 | phoneCall.getContent().put(CALL_RECORDS_TABLE_PHONE_NUMBER, cursor.getString(index)); 87 | 88 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_OUTGOING); 89 | phoneCall.getContent().put(CALL_RECORDS_TABLE_OUTGOING, cursor.getInt(index)); 90 | 91 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_START_DATE); 92 | phoneCall.getContent().put(CALL_RECORDS_TABLE_START_DATE, cursor.getLong(index)); 93 | 94 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_END_DATE); 95 | phoneCall.getContent().put(CALL_RECORDS_TABLE_END_DATE, cursor.getLong(index)); 96 | 97 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_RECORDING_PATH); 98 | phoneCall.getContent().put(CALL_RECORDS_TABLE_RECORDING_PATH, cursor.getString(index)); 99 | 100 | index = cursor.getColumnIndex(CALL_RECORDS_TABLE_KEEP); 101 | phoneCall.getContent().put(CALL_RECORDS_TABLE_KEEP, cursor.getInt(index)); 102 | 103 | index = cursor.getColumnIndex(CALL_RECORDS_BACKUP_STATE); 104 | phoneCall.getContent().put(CALL_RECORDS_BACKUP_STATE, cursor.getInt(index)); 105 | 106 | return phoneCall; 107 | } 108 | 109 | 110 | public synchronized ArrayList getAllCalls() { 111 | ArrayList array_list = new ArrayList(); 112 | 113 | SQLiteDatabase db = this.getReadableDatabase(); 114 | try { 115 | Cursor cursor = db.rawQuery("select * from " + Database.CALL_RECORDS_TABLE, null); 116 | cursor.moveToFirst(); 117 | while (cursor.isAfterLast() == false) { 118 | CallLog phoneCall = getCallLogFrom(cursor); 119 | array_list.add(phoneCall); 120 | cursor.moveToNext(); 121 | } 122 | return array_list; 123 | } finally { 124 | db.close(); 125 | } 126 | } 127 | 128 | 129 | public synchronized ArrayList getAllCalls(boolean outgoing) { 130 | ArrayList array_list = new ArrayList(); 131 | SQLiteDatabase db = this.getReadableDatabase(); 132 | 133 | try { 134 | Cursor cursor = db.rawQuery("select * from " + Database.CALL_RECORDS_TABLE + " where " + Database.CALL_RECORDS_TABLE_OUTGOING + "=" + (outgoing ? "1" : "0"), null); 135 | cursor.moveToFirst(); 136 | while (cursor.isAfterLast() == false) { 137 | CallLog phoneCall = getCallLogFrom(cursor); 138 | array_list.add(phoneCall); 139 | cursor.moveToNext(); 140 | } 141 | return array_list; 142 | } finally { 143 | db.close(); 144 | } 145 | } 146 | 147 | public synchronized boolean addCall(CallLog phoneCall) { 148 | SQLiteDatabase db = this.getWritableDatabase(); 149 | try { 150 | if (phoneCall.isNew) { 151 | long rowId = db.insert(Database.CALL_RECORDS_TABLE, null, phoneCall.getContent()); 152 | // rowID is and Alias for _ID see: http://www.sqlite.org/autoinc.html 153 | phoneCall.getContent().put(Database.CALL_RECORDS_TABLE_ID, rowId); 154 | } else { 155 | db.update(Database.CALL_RECORDS_TABLE, phoneCall.getContent(), CALL_RECORDS_TABLE_ID + "=" + phoneCall.getId(), null); 156 | } 157 | return true; 158 | } catch (Exception e) { 159 | e.printStackTrace(); 160 | return false; 161 | } finally { 162 | db.close(); 163 | } 164 | } 165 | 166 | public synchronized boolean updateCall(CallLog phoneCall) { 167 | SQLiteDatabase db = this.getWritableDatabase(); 168 | try { 169 | db.update(Database.CALL_RECORDS_TABLE, phoneCall.getContent(), "id = ?", new String[]{Integer.toString(phoneCall.getId())}); 170 | return true; 171 | } finally { 172 | db.close(); 173 | } 174 | } 175 | 176 | public synchronized int count() { 177 | SQLiteDatabase db = this.getReadableDatabase(); 178 | try { 179 | int numRows = (int) DatabaseUtils.queryNumEntries(db, Database.CALL_RECORDS_TABLE); 180 | return numRows; 181 | } finally { 182 | db.close(); 183 | } 184 | } 185 | 186 | public synchronized CallLog getCall(int id) { 187 | SQLiteDatabase db = this.getReadableDatabase(); 188 | try { 189 | Cursor cursor = db.rawQuery("select * from " + Database.CALL_RECORDS_TABLE + " where " + Database.CALL_RECORDS_TABLE_ID + "=" + id, null); 190 | if (!cursor.moveToFirst()) return null; // does not exist 191 | return getCallLogFrom(cursor); 192 | } finally { 193 | db.close(); 194 | } 195 | } 196 | 197 | public synchronized void removeCall(int id) { 198 | SQLiteDatabase db = this.getReadableDatabase(); 199 | try { 200 | Cursor cursor = db.rawQuery("select * from " + Database.CALL_RECORDS_TABLE + " where " + Database.CALL_RECORDS_TABLE_ID + "=" + id, null); 201 | if (!cursor.moveToFirst()) return; // doesn't exist 202 | CallLog call = getCallLogFrom(cursor); 203 | String path = call.getPathToRecording(); 204 | try { 205 | if (null != path) 206 | new File(path).delete(); 207 | } catch (Exception e) { 208 | 209 | } 210 | db.execSQL("Delete from " + Database.CALL_RECORDS_TABLE + " where " + Database.CALL_RECORDS_TABLE_ID + "=" + id); 211 | } finally { 212 | db.close(); 213 | } 214 | } 215 | 216 | 217 | public synchronized void removeAllCalls(boolean includeKept) { 218 | final ArrayList allCalls = getAllCalls(); 219 | SQLiteDatabase db = this.getWritableDatabase(); 220 | try { 221 | for (CallLog call : allCalls) { 222 | if (includeKept || !call.isKept()) { 223 | try { 224 | new File(call.getPathToRecording()).delete(); 225 | } catch (Exception e) { 226 | } 227 | try { 228 | db.execSQL("Delete from " + Database.CALL_RECORDS_TABLE + " where " + Database.CALL_RECORDS_TABLE_ID + "=" + call.getId()); 229 | } catch (Exception e) { 230 | } 231 | } 232 | } 233 | // db.delete(Database.CALL_RECORDS_TABLE, null, null); 234 | } finally { 235 | db.close(); 236 | } 237 | } 238 | 239 | 240 | private synchronized Whitelist getWhitelistFrom(Cursor cursor) { 241 | Whitelist whitelist = new Whitelist(); 242 | 243 | int index = cursor.getColumnIndex(Database.WHITELIST_TABLE_ID); 244 | whitelist.getContent().put(Database.WHITELIST_TABLE_ID, cursor.getInt(index)); 245 | 246 | index = cursor.getColumnIndex(Database.WHITELIST_TABLE_CONTACT_ID); 247 | whitelist.getContent().put(Database.WHITELIST_TABLE_CONTACT_ID, cursor.getString(index)); 248 | 249 | index = cursor.getColumnIndex(Database.WHITELIST_TABLE_RECORD); 250 | whitelist.getContent().put(Database.WHITELIST_TABLE_RECORD, cursor.getString(index)); 251 | 252 | return whitelist; 253 | } 254 | 255 | 256 | public synchronized ArrayList getAllWhitelist() { 257 | ArrayList array_list = new ArrayList(); 258 | 259 | //hp = new HashMap(); 260 | SQLiteDatabase db = this.getReadableDatabase(); 261 | try { 262 | Cursor cursor = db.rawQuery("select * from " + Database.WHITELIST_TABLE, null); 263 | String[] columns = cursor.getColumnNames(); 264 | 265 | cursor.moveToFirst(); 266 | while (cursor.isAfterLast() == false) { 267 | Whitelist contact = getWhitelistFrom(cursor); 268 | array_list.add(contact); 269 | cursor.moveToNext(); 270 | } 271 | return array_list; 272 | } finally { 273 | db.close(); 274 | } 275 | } 276 | 277 | public synchronized Whitelist getContact(String contactId) { 278 | SQLiteDatabase db = this.getReadableDatabase(); 279 | try { 280 | Cursor cursor = db.rawQuery("select * from " + Database.WHITELIST_TABLE + " where " + Database.WHITELIST_TABLE_CONTACT_ID + " = " + contactId + "", null); 281 | if (!cursor.moveToFirst()) return null; // does not exist 282 | return getWhitelistFrom(cursor); 283 | } finally { 284 | db.close(); 285 | } 286 | } 287 | 288 | 289 | public synchronized boolean addWhitelist(Whitelist contact) { 290 | SQLiteDatabase db = this.getWritableDatabase(); 291 | try { 292 | db.insert(Database.WHITELIST_TABLE, null, contact.getContent()); 293 | return true; 294 | } finally { 295 | db.close(); 296 | } 297 | } 298 | 299 | 300 | public synchronized void removeWhiteList(int id) { 301 | SQLiteDatabase db = this.getReadableDatabase(); 302 | try { 303 | db.execSQL("Delete from " + Database.WHITELIST_TABLE + " where " + Database.WHITELIST_TABLE_ID + "=" + id + ""); 304 | } finally { 305 | db.close(); 306 | } 307 | } 308 | 309 | public synchronized static boolean isWhitelisted(Context context, String phoneNumber) { 310 | // define the columns the query should return 311 | String[] projection = new String[]{ContactsContract.PhoneLookup._ID}; 312 | // encode the phone number and build the filter URI 313 | Uri contactUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); 314 | // query time 315 | Cursor cursor = context.getContentResolver().query(contactUri, projection, null, null, null); 316 | 317 | if (cursor.moveToFirst()) { 318 | String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 319 | // Found the callers... now check the whitelist 320 | Whitelist whitelist = getInstance(context).getContact(contactId); 321 | return (null != whitelist); 322 | } 323 | return false; 324 | } 325 | 326 | 327 | } 328 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/database/Whitelist.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.database; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | 6 | /** 7 | * Created by Jeff on 07-May-16. 8 | * 9 | * A Whitelist record from our database 10 | */ 11 | public class Whitelist { 12 | 13 | boolean isNew = true; 14 | 15 | private ContentValues content; 16 | 17 | public Whitelist(){ 18 | content = new ContentValues(); 19 | content.put(Database.WHITELIST_TABLE_RECORD, false); // default: do NOT record 20 | } 21 | 22 | public String getContactId(){ 23 | return content.getAsString(Database.WHITELIST_TABLE_CONTACT_ID); 24 | } 25 | public void setContactId(String contactId){ 26 | content.put(Database.WHITELIST_TABLE_CONTACT_ID,contactId); 27 | } 28 | 29 | public boolean isRecordable(){ 30 | return content.getAsBoolean(Database.WHITELIST_TABLE_RECORD); 31 | } 32 | 33 | public void setRecordable(boolean enable){ 34 | content.put(Database.WHITELIST_TABLE_RECORD,enable); 35 | } 36 | 37 | public int getId(){ 38 | return content.getAsInteger(Database.WHITELIST_TABLE_ID); 39 | } 40 | 41 | 42 | public ContentValues getContent(){ 43 | return content; 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/listeners/PhoneListener.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.listeners; 2 | 3 | import android.content.Context; 4 | import android.telephony.PhoneStateListener; 5 | import android.telephony.TelephonyManager; 6 | 7 | import com.jlcsoftware.database.CallLog; 8 | import com.jlcsoftware.database.Database; 9 | import com.jlcsoftware.receivers.MyCallReceiver; 10 | import com.jlcsoftware.services.RecordCallService; 11 | 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | 14 | /** 15 | * Created by Jeff on 01-May-16. 16 | *

17 | * The logic is a little odd here... 18 | *

19 | * When a incoming call comes in, we get a CALL_STATE_RINGING that provides the incoming number and all is easy and good... 20 | * on the other hand, a Outgoing call generates a ACTION_NEW_OUTGOING_CALL with the phone number, then an a CALL_STATE_IDLE and then a 21 | * CALL_STATE_OFFHOOK when the call connects - we never get the outgoing number in the PhoneState Change 22 | *

23 | */ 24 | public class PhoneListener extends PhoneStateListener { 25 | 26 | private static PhoneListener instance = null; 27 | 28 | /** 29 | * Must be called once on app startup 30 | * 31 | * @param context - application context 32 | * @return 33 | */ 34 | public static PhoneListener getInstance(Context context) { 35 | if (instance == null) { 36 | instance = new PhoneListener(context); 37 | } 38 | return instance; 39 | } 40 | 41 | public static boolean hasInstance() { 42 | return null != instance; 43 | } 44 | 45 | private final Context context; 46 | private CallLog phoneCall; 47 | 48 | private PhoneListener(Context context) { 49 | this.context = context; 50 | } 51 | 52 | AtomicBoolean isRecording = new AtomicBoolean(); 53 | AtomicBoolean isWhitelisted = new AtomicBoolean(); 54 | 55 | 56 | /** 57 | * Set the outgoing phone number 58 | *

59 | * Called by {@link MyCallReceiver} since that is where the phone number is available in a outgoing call 60 | * 61 | * @param phoneNumber 62 | */ 63 | public void setOutgoing(String phoneNumber) { 64 | if (null == phoneCall) 65 | phoneCall = new CallLog(); 66 | phoneCall.setPhoneNumber(phoneNumber); 67 | phoneCall.setOutgoing(); 68 | // called here so as not to miss recording part of the conversation in TelephonyManager.CALL_STATE_OFFHOOK 69 | isWhitelisted.set(Database.isWhitelisted(context, phoneCall.getPhoneNumber())); 70 | } 71 | 72 | @Override 73 | public void onCallStateChanged(int state, String incomingNumber) { 74 | super.onCallStateChanged(state, incomingNumber); 75 | 76 | switch (state) { 77 | case TelephonyManager.CALL_STATE_IDLE: // Idle... no call 78 | if (isRecording.get()) { 79 | RecordCallService.stopRecording(context); 80 | phoneCall = null; 81 | isRecording.set(false); 82 | } 83 | break; 84 | case TelephonyManager.CALL_STATE_OFFHOOK: // Call answered 85 | if (isWhitelisted.get()) { 86 | isWhitelisted.set(false); 87 | return; 88 | } 89 | if (!isRecording.get()) { 90 | isRecording.set(true); 91 | // start: Probably not ever usefull 92 | if (null == phoneCall) 93 | phoneCall = new CallLog(); 94 | if (!incomingNumber.isEmpty()) { 95 | phoneCall.setPhoneNumber(incomingNumber); 96 | } 97 | // end: Probably not ever usefull 98 | RecordCallService.sartRecording(context, phoneCall); 99 | } 100 | break; 101 | case TelephonyManager.CALL_STATE_RINGING: // Phone ringing 102 | // DO NOT try RECORDING here! Leads to VERY poor quality recordings 103 | // I think something is not fully settled with the Incoming phone call when we get CALL_STATE_RINGING 104 | // a "SystemClock.sleep(1000);" in the code will allow the incoming call to stabilize and produce a good recording...(as proof of above) 105 | if (null == phoneCall) 106 | phoneCall = new CallLog(); 107 | if (!incomingNumber.isEmpty()) { 108 | phoneCall.setPhoneNumber(incomingNumber); 109 | // called here so as not to miss recording part of the conversation in TelephonyManager.CALL_STATE_OFFHOOK 110 | isWhitelisted.set(Database.isWhitelisted(context, phoneCall.getPhoneNumber())); 111 | } 112 | break; 113 | } 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/receivers/MyAlarmReceiver.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.receivers; 2 | 3 | import android.app.AlarmManager; 4 | import android.app.PendingIntent; 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | 9 | import com.jlcsoftware.services.CleanupService; 10 | 11 | /** 12 | * Handles the cleaning of old recordings, on a schedule 13 | */ 14 | 15 | public class MyAlarmReceiver extends BroadcastReceiver { 16 | public MyAlarmReceiver() { 17 | } 18 | 19 | @Override 20 | public void onReceive(Context context, Intent intent) { 21 | CleanupService.sartCleaning(context); 22 | } 23 | 24 | /** 25 | * set the system "Alarm" 26 | * @param context 27 | */ 28 | 29 | public static void setAlarm(Context context) { 30 | AlarmManager alarmMgr; 31 | PendingIntent alarmIntent; 32 | 33 | alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 34 | Intent intent = new Intent(context, MyAlarmReceiver.class); 35 | alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0); 36 | alarmMgr.cancel(alarmIntent); 37 | /* 38 | // Debug - Every 30 seconds 39 | alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, 40 | 30 * 1000, 41 | 30 * 1000, alarmIntent); 42 | */ 43 | 44 | alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, 45 | AlarmManager.INTERVAL_DAY, 46 | AlarmManager.INTERVAL_DAY, alarmIntent); 47 | } 48 | 49 | /** 50 | * cancel the system "Alarm" 51 | * @param context 52 | */ 53 | public static void cancleAlarm(Context context) { 54 | AlarmManager alarmMgr; 55 | PendingIntent alarmIntent; 56 | alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 57 | Intent intent = new Intent(context, MyAlarmReceiver.class); 58 | alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0); 59 | alarmMgr.cancel(alarmIntent); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/receivers/MyCallReceiver.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.receivers; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.telephony.PhoneStateListener; 7 | import android.telephony.TelephonyManager; 8 | import android.util.Log; 9 | 10 | import com.jlcsoftware.callrecorder.AppPreferences; 11 | import com.jlcsoftware.listeners.PhoneListener; 12 | 13 | 14 | /** 15 | * Handle the Phone call related BroadcastActions 16 | * 17 | * 18 | */ 19 | 20 | public class MyCallReceiver extends BroadcastReceiver { 21 | 22 | public MyCallReceiver() { 23 | } 24 | 25 | static TelephonyManager manager; 26 | 27 | @Override 28 | public void onReceive(Context context, Intent intent) { 29 | Log.i("JLCreativeCallRecorder", "MyCallReceiver.onReceive "); 30 | 31 | if (!AppPreferences.getInstance(context).isRecordingEnabled()) { 32 | removeListener(); 33 | return; 34 | } 35 | 36 | if (Intent.ACTION_NEW_OUTGOING_CALL.equals(intent.getAction())) { 37 | if (!AppPreferences.getInstance(context).isRecordingOutgoingEnabled()) { 38 | removeListener(); 39 | return; 40 | } 41 | PhoneListener.getInstance(context).setOutgoing(intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)); 42 | } else { 43 | if (!AppPreferences.getInstance(context).isRecordingIncomingEnabled()) { 44 | removeListener(); 45 | return; 46 | } 47 | } 48 | 49 | // Start Listening to the call.... 50 | if (null == manager) { 51 | manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 52 | } 53 | if (null != manager) 54 | manager.listen(PhoneListener.getInstance(context), PhoneStateListener.LISTEN_CALL_STATE); 55 | } 56 | 57 | private void removeListener() { 58 | if (null != manager) { 59 | if (PhoneListener.hasInstance()) 60 | manager.listen(PhoneListener.getInstance(null), PhoneStateListener.LISTEN_NONE); 61 | } 62 | } 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/receivers/MyLocalBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.receivers; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import com.jlcsoftware.callrecorder.LocalBroadcastActions; 8 | 9 | /** 10 | * Internal BroadcastReceiver to handle 11 | * {@link LocalBroadcastActions} 12 | */ 13 | 14 | public class MyLocalBroadcastReceiver extends BroadcastReceiver { 15 | 16 | public interface OnNewRecordingListener{ 17 | void OnBroadcastReceived(Intent intent); 18 | } 19 | 20 | OnNewRecordingListener listener; 21 | 22 | public MyLocalBroadcastReceiver(OnNewRecordingListener listener) { 23 | this.listener = listener; 24 | } 25 | 26 | public MyLocalBroadcastReceiver() { 27 | } 28 | 29 | public void setListener( OnNewRecordingListener listener){ 30 | this.listener = listener; 31 | } 32 | 33 | @Override 34 | public void onReceive(Context context, Intent intent) { 35 | if(null!=listener) listener.OnBroadcastReceived(intent); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/services/CleanupService.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.services; 2 | 3 | import android.app.Service; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.IBinder; 7 | import android.support.v4.content.LocalBroadcastManager; 8 | 9 | import com.jlcsoftware.callrecorder.AppPreferences; 10 | import com.jlcsoftware.callrecorder.LocalBroadcastActions; 11 | import com.jlcsoftware.database.CallLog; 12 | import com.jlcsoftware.database.Database; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Calendar; 16 | import java.util.GregorianCalendar; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | /** 21 | * Clean up disk usage 22 | */ 23 | public class CleanupService extends Service { 24 | public CleanupService() { 25 | 26 | } 27 | 28 | @Override 29 | public IBinder onBind(Intent intent) { 30 | return null; // not supported 31 | } 32 | 33 | 34 | AtomicBoolean isRunning = new AtomicBoolean(); 35 | 36 | @Override 37 | public int onStartCommand(Intent intent, int flags, int startId) { 38 | super.onStartCommand(intent, flags, startId); 39 | if (!isRunning.get()) { 40 | isRunning.set(true); 41 | 42 | Runnable runnable = new Runnable() { 43 | @Override 44 | public void run() { 45 | try { 46 | AppPreferences instance = AppPreferences.getInstance(getApplicationContext()); 47 | AppPreferences.OlderThan olderThan = instance.getOlderThan(); 48 | if (olderThan != AppPreferences.OlderThan.NEVER) { 49 | int age = 0; 50 | switch (olderThan) { 51 | case DAILY: 52 | age = 1; 53 | break; 54 | case THREE_DAYS: 55 | age = 3; 56 | break; 57 | case WEEKLY: 58 | age = 7; 59 | break; 60 | case MONTHLY: 61 | age = 31; 62 | break; 63 | case QUARTERLY: 64 | age = 92; 65 | break; 66 | case YEARLY: 67 | age = 365; 68 | break; 69 | } 70 | boolean deleted = false; 71 | Calendar now = GregorianCalendar.getInstance(); 72 | Database callLogDB = Database.getInstance(getApplicationContext()); 73 | ArrayList allCalls = callLogDB.getAllCalls(); 74 | for (CallLog call : allCalls) { 75 | if (!call.isKept()) { 76 | if (daysBetween(call.getEndTime(), now) >= age) { 77 | callLogDB.removeCall(call.getId()); 78 | deleted = true; 79 | } 80 | } 81 | } 82 | if(deleted) LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(LocalBroadcastActions.RECORDING_DELETED_BROADCAST)); 83 | } 84 | // TODO: implement cleanup via disk usage... 85 | }catch (Exception e){ 86 | e.printStackTrace(); 87 | }finally { 88 | isRunning.set(false); 89 | } 90 | } 91 | }; 92 | new Thread(runnable).start(); 93 | } 94 | return Service.START_REDELIVER_INTENT; 95 | } 96 | 97 | @Override 98 | public void onDestroy() { 99 | super.onDestroy(); 100 | } 101 | 102 | public final static String ACTION_CLEAN_UP = "com.jlcsoftware.ACTION_CLEAN_UP"; 103 | 104 | 105 | public static void sartCleaning(Context context) { 106 | Intent intent = new Intent(context, CleanupService.class); 107 | intent.setAction(ACTION_CLEAN_UP); 108 | context.startService(intent); 109 | } 110 | 111 | 112 | private long daysBetween(Calendar then, Calendar now) { 113 | long diff = now.getTimeInMillis() - then.getTimeInMillis(); 114 | return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/services/RecordCallService.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.services; 2 | 3 | import android.app.NotificationManager; 4 | import android.app.PendingIntent; 5 | import android.app.Service; 6 | import android.content.ContentValues; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.media.MediaRecorder; 10 | import android.os.IBinder; 11 | import android.support.v4.content.LocalBroadcastManager; 12 | import android.support.v7.app.NotificationCompat; 13 | 14 | import com.jlcsoftware.callrecorder.AppPreferences; 15 | import com.jlcsoftware.callrecorder.LocalBroadcastActions; 16 | import com.jlcsoftware.callrecorder.MainActivity; 17 | import com.jlcsoftware.callrecorder.R; 18 | import com.jlcsoftware.database.CallLog; 19 | 20 | import java.io.File; 21 | import java.util.Calendar; 22 | 23 | /** 24 | * The nitty gritty Service that handles actually recording the conversations 25 | */ 26 | 27 | public class RecordCallService extends Service { 28 | 29 | public final static String ACTION_START_RECORDING = "com.jlcsoftware.ACTION_CLEAN_UP"; 30 | public final static String ACTION_STOP_RECORDING = "com.jlcsoftware.ACTION_STOP_RECORDING"; 31 | public final static String EXTRA_PHONE_CALL = "com.jlcsoftware.EXTRA_PHONE_CALL"; 32 | 33 | public RecordCallService(){ 34 | } 35 | 36 | @Override 37 | public IBinder onBind(Intent intent) { 38 | return null; 39 | } 40 | 41 | 42 | @Override 43 | public int onStartCommand(Intent intent, int flags, int startId) { 44 | ContentValues parcelableExtra = intent.getParcelableExtra(EXTRA_PHONE_CALL); 45 | startRecording(new CallLog(parcelableExtra)); 46 | return START_NOT_STICKY ; 47 | } 48 | 49 | @Override 50 | public void onDestroy() { 51 | stopRecording(); 52 | super.onDestroy(); 53 | } 54 | 55 | private CallLog phoneCall; 56 | 57 | boolean isRecording = false; 58 | 59 | private void stopRecording() { 60 | 61 | if (isRecording) { 62 | try { 63 | phoneCall.setEndTime(Calendar.getInstance()); 64 | mediaRecorder.stop(); 65 | mediaRecorder.reset(); 66 | mediaRecorder.release(); 67 | mediaRecorder = null; 68 | isRecording = false; 69 | 70 | phoneCall.save(getBaseContext()); 71 | displayNotification(phoneCall); 72 | 73 | LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(LocalBroadcastActions.NEW_RECORDING_BROADCAST)); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | phoneCall = null; 79 | } 80 | 81 | 82 | MediaRecorder mediaRecorder; 83 | 84 | 85 | private void startRecording(CallLog phoneCall) { 86 | if (!isRecording) { 87 | isRecording = true; 88 | this.phoneCall = phoneCall; 89 | File file = null; 90 | try { 91 | this.phoneCall.setSartTime(Calendar.getInstance()); 92 | File dir = AppPreferences.getInstance(getApplicationContext()).getFilesDirectory(); 93 | mediaRecorder = new MediaRecorder(); 94 | file = File.createTempFile("record", ".3gp", dir); 95 | this.phoneCall.setPathToRecording(file.getAbsolutePath()); 96 | mediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL); 97 | mediaRecorder.setAudioSamplingRate(8000); 98 | mediaRecorder.setAudioEncodingBitRate(12200); 99 | mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 100 | mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 101 | mediaRecorder.setOutputFile(phoneCall.getPathToRecording()); 102 | mediaRecorder.prepare(); 103 | mediaRecorder.start(); 104 | } catch (Exception e) { 105 | e.printStackTrace(); 106 | isRecording = false; 107 | if (file != null) file.delete(); 108 | this.phoneCall = null; 109 | isRecording = false; 110 | } 111 | } 112 | } 113 | 114 | public void displayNotification(CallLog phoneCall) { 115 | NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 116 | 117 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this); 118 | builder.setSmallIcon(R.drawable.ic_recording_conversation_white_24); 119 | builder.setContentTitle(getApplicationContext().getString(R.string.notification_title)); 120 | builder.setContentText(getApplicationContext().getString(R.string.notification_text)); 121 | builder.setContentInfo(getApplicationContext().getString(R.string.notification_more_text)); 122 | builder.setAutoCancel(true); 123 | 124 | Intent intent = new Intent(getApplicationContext(), MainActivity.class); 125 | intent.setAction(Long.toString(System.currentTimeMillis())); // fake action to force PendingIntent.FLAG_UPDATE_CURRENT 126 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 127 | 128 | intent.putExtra("RecordingId", phoneCall.getId()); 129 | 130 | builder.setContentIntent(PendingIntent.getActivity(this, 0xFeed, intent, PendingIntent.FLAG_UPDATE_CURRENT)); 131 | notificationManager.notify(0xfeed, builder.build()); 132 | } 133 | 134 | 135 | public static void sartRecording(Context context, CallLog phoneCall) { 136 | Intent intent = new Intent(context, RecordCallService.class); 137 | intent.setAction(ACTION_START_RECORDING); 138 | intent.putExtra(EXTRA_PHONE_CALL, phoneCall.getContent()); 139 | context.startService(intent); 140 | } 141 | 142 | 143 | public static void stopRecording(Context context) { 144 | Intent intent = new Intent(context, RecordCallService.class); 145 | intent.setAction(ACTION_STOP_RECORDING); 146 | context.stopService(intent); 147 | } 148 | 149 | 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/jlcsoftware/widget/adapter/SectionedRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jlcsoftware.widget.adapter; 2 | 3 | /** 4 | * Created by Jeff on 19-May-16. 5 | *

6 | * 7 | * Original: https://github.com/afollestad/sectioned-recyclerview 8 | * 9 | * Any modifications are mine 10 | * 11 | * 12 | * Usage: 13 | * Layout Manager 14 | * If you're using a LinearLayoutManager, you're all set. If you're using a GridLayoutManager, you need to tell the adapter: 15 | * GridLayoutManager manager = // ... 16 | * adapter.setLayoutManager(manager); 17 | * This is vital to getting headers to span all columns. 18 | * 19 | * 20 | */ 21 | 22 | import android.support.annotation.IntRange; 23 | import android.support.annotation.Nullable; 24 | import android.support.v4.util.ArrayMap; 25 | import android.support.v7.widget.GridLayoutManager; 26 | import android.support.v7.widget.RecyclerView; 27 | import android.support.v7.widget.StaggeredGridLayoutManager; 28 | import android.view.ViewGroup; 29 | 30 | import java.util.List; 31 | 32 | /** 33 | * @author Aidan Follestad (afollestad) 34 | */ 35 | public abstract class SectionedRecyclerViewAdapter extends RecyclerView.Adapter { 36 | 37 | protected final static int VIEW_TYPE_HEADER = -2; 38 | protected final static int VIEW_TYPE_ITEM = -1; 39 | 40 | private final ArrayMap mHeaderLocationMap; 41 | private GridLayoutManager mLayoutManager; 42 | private ArrayMap mSpanMap; 43 | private boolean mShowHeadersForEmptySections; 44 | 45 | public SectionedRecyclerViewAdapter() { 46 | mHeaderLocationMap = new ArrayMap<>(); 47 | } 48 | 49 | public abstract int getSectionCount(); 50 | 51 | public abstract int getItemCount(int section); 52 | 53 | public abstract void onBindHeaderViewHolder(VH holder, int section); 54 | 55 | /** 56 | * Setup non-header view. 57 | * @param holder 58 | * @param section is section index. 59 | * @param relativePosition is index in this section. 60 | * @param absolutePosition is index out of all non-header items. 61 | */ 62 | 63 | public abstract void onBindViewHolder(VH holder, int section, int relativePosition, int absolutePosition); 64 | 65 | public final boolean isHeader(int position) { 66 | return mHeaderLocationMap.get(position) != null; 67 | } 68 | 69 | /** 70 | * Instructs the list view adapter to whether show headers for empty sections or not. 71 | * 72 | * @param show flag indicating whether headers for empty sections ought to be shown. 73 | */ 74 | public final void shouldShowHeadersForEmptySections(boolean show) { 75 | mShowHeadersForEmptySections = show; 76 | } 77 | 78 | public final void setLayoutManager(@Nullable GridLayoutManager lm) { 79 | mLayoutManager = lm; 80 | if (lm == null) return; 81 | lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 82 | @Override 83 | public int getSpanSize(int position) { 84 | if (isHeader(position)) 85 | return mLayoutManager.getSpanCount(); 86 | final int[] sectionAndPos = getSectionIndexAndRelativePosition(position); 87 | final int absPos = position - (sectionAndPos[0] + 1); 88 | return getRowSpan(mLayoutManager.getSpanCount(), 89 | sectionAndPos[0], sectionAndPos[1], absPos); 90 | } 91 | }); 92 | } 93 | 94 | @SuppressWarnings("UnusedParameters") 95 | protected int getRowSpan(int fullSpanSize, int section, int relativePosition, int absolutePosition) { 96 | return 1; 97 | } 98 | 99 | // returns section along with offsetted position 100 | private int[] getSectionIndexAndRelativePosition(int itemPosition) { 101 | synchronized (mHeaderLocationMap) { 102 | Integer lastSectionIndex = -1; 103 | for (final Integer sectionIndex : mHeaderLocationMap.keySet()) { 104 | if (itemPosition > sectionIndex) { 105 | lastSectionIndex = sectionIndex; 106 | } else { 107 | break; 108 | } 109 | } 110 | return new int[]{mHeaderLocationMap.get(lastSectionIndex), itemPosition - lastSectionIndex - 1}; 111 | } 112 | } 113 | 114 | @Override 115 | public final int getItemCount() { 116 | int count = 0; 117 | mHeaderLocationMap.clear(); 118 | for (int s = 0; s < getSectionCount(); s++) { 119 | int itemCount = getItemCount(s); 120 | if (mShowHeadersForEmptySections || (itemCount > 0)) { 121 | mHeaderLocationMap.put(count, s); 122 | count += itemCount + 1; 123 | } 124 | } 125 | return count; 126 | } 127 | 128 | /** 129 | * @hide 130 | * @deprecated 131 | */ 132 | @Override 133 | @Deprecated 134 | public final int getItemViewType(int position) { 135 | if (isHeader(position)) { 136 | return getHeaderViewType(mHeaderLocationMap.get(position)); 137 | } else { 138 | final int[] sectionAndPos = getSectionIndexAndRelativePosition(position); 139 | return getItemViewType(sectionAndPos[0], 140 | // offset section view positions 141 | sectionAndPos[1], 142 | position - (sectionAndPos[0] + 1)); 143 | } 144 | } 145 | 146 | @SuppressWarnings("UnusedParameters") 147 | @IntRange(from = 0, to = Integer.MAX_VALUE) 148 | public int getHeaderViewType(int section) { 149 | //noinspection ResourceType 150 | return VIEW_TYPE_HEADER; 151 | } 152 | 153 | @SuppressWarnings("UnusedParameters") 154 | @IntRange(from = 0, to = Integer.MAX_VALUE) 155 | public int getItemViewType(int section, int relativePosition, int absolutePosition) { 156 | //noinspection ResourceType 157 | return VIEW_TYPE_ITEM; 158 | } 159 | 160 | /** 161 | * @hide 162 | * @deprecated 163 | */ 164 | @Override 165 | @Deprecated 166 | public final void onBindViewHolder(VH holder, int position) { 167 | StaggeredGridLayoutManager.LayoutParams layoutParams = null; 168 | if (holder.itemView.getLayoutParams() instanceof GridLayoutManager.LayoutParams) 169 | layoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 170 | else if (holder.itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams) 171 | layoutParams = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams(); 172 | if (isHeader(position)) { 173 | if (layoutParams != null) layoutParams.setFullSpan(true); 174 | onBindHeaderViewHolder(holder, mHeaderLocationMap.get(position)); 175 | } else { 176 | if (layoutParams != null) layoutParams.setFullSpan(false); 177 | final int[] sectionAndPos = getSectionIndexAndRelativePosition(position); 178 | final int absPos = position - (sectionAndPos[0] + 1); 179 | onBindViewHolder(holder, sectionAndPos[0], 180 | // offset section view positions 181 | sectionAndPos[1], absPos); 182 | } 183 | if (layoutParams != null) 184 | holder.itemView.setLayoutParams(layoutParams); 185 | } 186 | 187 | /** 188 | * @hide 189 | * @deprecated 190 | */ 191 | @Deprecated 192 | @Override 193 | public final void onBindViewHolder(VH holder, int position, List payloads) { 194 | super.onBindViewHolder(holder, position, payloads); 195 | } 196 | } 197 | 198 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-hdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/switch_off.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-hdpi/switch_off.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/switch_on.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-hdpi/switch_on.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-mdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/switch_off.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-mdpi/switch_off.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/switch_on.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-mdpi/switch_on.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_info_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_sync_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/switch_off.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xhdpi/switch_off.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/switch_on.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xhdpi/switch_on.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/switch_off.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxhdpi/switch_off.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/switch_on.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxhdpi/switch_on.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/switch_off.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxxhdpi/switch_off.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/switch_on.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjeffm/CallRecorder/97a56c1da3d1ad7dbcbb023e47e377efcde2661a/app/src/main/res/drawable-xxxhdpi/switch_on.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/clock_circular.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/day.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_box_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cards_black_24.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lock_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_black_36dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_recording_conversation_white_24.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_unknown.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/phone_in.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/phone_out.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_button.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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selected_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/toggle_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/actionbar_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/actionbar_toggle2.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 15 | 20 | 26 | 27 | 33 | 34 | 39 | 40 | 45 | 46 | 52 | 53 | 58 | 59 | 60 | 66 | 67 | 73 | 74 | 80 | 81 | 87 | 88 | 94 | 95 | 100 | 101 | 107 | 108 | 115 | 116 | 117 |