diff --git a/.gitignore b/.gitignore index 39fb081..34f4fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /build /captures .externalNativeBuild +*/release/* \ No newline at end of file diff --git a/.idea/dictionaries/arjun.xml b/.idea/dictionaries/arjun.xml new file mode 100644 index 0000000..66c5a21 --- /dev/null +++ b/.idea/dictionaries/arjun.xml @@ -0,0 +1,7 @@ + + + + flowable + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3963879..75dac50 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf75cc2 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Android Audio Recorder App +Application to record audio and save it locally in the device. The application also visualizes the audio in wave format. It also supports recording in the background while you can continue using the device for other tasks. + + +GooglePlay Link + + diff --git a/app/build.gradle b/app/build.gradle index 48ddbcd..c7d1032 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,19 +1,32 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 - buildToolsVersion "26.0.1" + compileSdkVersion 25 + buildToolsVersion "25.0.3" defaultConfig { - applicationId "in.arjsna.voicerecorder" - minSdkVersion 15 - targetSdkVersion 26 + applicationId "in.arjsna.audiorecorder" + minSdkVersion 16 + targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } + + repositories { + maven { + url "http://dl.bintray.com/dasar/maven" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildTypes { release { - minifyEnabled false + minifyEnabled true + debuggable false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } @@ -26,9 +39,16 @@ dependencies { androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' - compile 'com.android.support:appcompat-v7:26.0.2' - compile 'com.android.support:cardview-v7:26.0.2' - compile 'com.android.support:recyclerview-v7:26.0.2' - compile 'com.melnykov:floatingactionbutton:1.1.0' - compile 'com.jpardogo.materialtabstrip:library:1.0.6' + compile 'com.android.support:appcompat-v7:25.4.0' + compile 'com.android.support:cardview-v7:25.4.0' + compile 'com.android.support:recyclerview-v7:25.4.0' + compile 'com.android.support:design:25.4.0' + + compile 'io.reactivex.rxjava2:rxjava:2.1.2' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + + compile 'com.orhanobut:hawk:2.0.1' + compile 'uz.shift:colorpicker:0.5@aar' + } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9d6192a..778c1b3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,11 +15,109 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* +-optimizationpasses 5 +-allowaccessmodification +-dontpreverify -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# The remainder of this file is identical to the non-optimized version +# of the Proguard configuration file (except that the other file has +# flags to turn off optimization). -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-verbose + +-keepattributes *Annotation* +-keep public class com.google.vending.licensing.ILicensingService +-keep public class com.android.vending.licensing.ILicensingService + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} + +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} + +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepclassmembers class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator CREATOR; +} + +-keepclassmembers class **.R$* { + public static ; +} + +# The support library contains references to newer platform versions. +# Don't warn about those in case this app is linking against an older +# platform version. We know about them, and they are safe. +-dontwarn android.support.** + +# Understand the @Keep support annotation. +-keep class android.support.annotation.Keep + +-keep @android.support.annotation.Keep class * {*;} + +-keepclasseswithmembers class * { + @android.support.annotation.Keep ; +} + +-keepclasseswithmembers class * { + @android.support.annotation.Keep ; +} + +-keepclasseswithmembers class * { + @android.support.annotation.Keep (...); +} + +-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault,Annotation +-keep class com.google.** { *;} +-keep interface com.google.** { *;} +-dontwarn com.google.** +-keepclassmembers class * { + @com.google.api.client.util.Key ; +} + +# Needed by google-http-client-android when linking against an older platform version + +-dontwarn com.google.api.client.extensions.android.** + +# Needed by google-api-client-android when linking against an older platform version + +-dontwarn com.google.api.client.googleapis.extensions.android.** + +-keepclassmembers enum * { *; } + +-dontwarn javax.xml.** +-dontwarn javax.naming.** +-dontwarn junit.textui.** +-dontwarn java.nio.** +-dontwarn java.lang.** +-dontwarn org.codehaus.** +-dontwarn sun.security.** +-dontwarn org.apache.** +-dontwarn java.awt.** +-dontwarn sun.misc.** + +# Crashlytics +-keepattributes *Annotation* +-keepattributes SourceFile,LineNumberTable +-keep public class * extends java.lang.Exception +-printmapping mapping.txt +-keep class com.crashlytics.** { *; } +-dontwarn com.crashlytics.** \ No newline at end of file diff --git a/app/src/androidTest/java/in/arjsna/voicerecorder/ExampleInstrumentedTest.java b/app/src/androidTest/java/in/arjsna/audiorecorder/ExampleInstrumentedTest.java similarity index 95% rename from app/src/androidTest/java/in/arjsna/voicerecorder/ExampleInstrumentedTest.java rename to app/src/androidTest/java/in/arjsna/audiorecorder/ExampleInstrumentedTest.java index 27fcaeb..481e0e2 100644 --- a/app/src/androidTest/java/in/arjsna/voicerecorder/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/in/arjsna/audiorecorder/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package in.arjsna.voicerecorder; +package in.arjsna.audiorecorder; import android.content.Context; import android.support.test.InstrumentationRegistry; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c448688..0dee1ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,18 +1,22 @@ + package="in.arjsna.audiorecorder"> - - + + + - + android:name=".AudioRecorderApp" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> + @@ -20,24 +24,34 @@ + android:parentActivityName="in.arjsna.audiorecorder.activities.MainActivity" + android:screenOrientation="portrait"> + android:value="in.arjsna.audiorecorder.activities.MainActivity"/> + + android:authorities="in.arjsna.audiorecorder.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + android:resource="@xml/filepaths"/> - + + + + + + \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/AppConstants.java b/app/src/main/java/in/arjsna/audiorecorder/AppConstants.java new file mode 100644 index 0000000..09599a9 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/AppConstants.java @@ -0,0 +1,8 @@ +package in.arjsna.audiorecorder; + +public class AppConstants { + public static final String ACTION_PAUSE = "in.arjsna.audiorecorder.PAUSE"; + public static final String ACTION_RESUME = "in.arjsna.audiorecorder.RESUME"; + public static final String ACTION_STOP = "in.arjsna.audiorecorder.STOP"; + public static final String ACTION_IN_SERVICE = "in.arjsna.audiorecorder.ACTION_IN_SERVICE"; +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/AudioRecorderApp.java b/app/src/main/java/in/arjsna/audiorecorder/AudioRecorderApp.java new file mode 100644 index 0000000..0920fd3 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/AudioRecorderApp.java @@ -0,0 +1,11 @@ +package in.arjsna.audiorecorder; + +import android.app.Application; +import com.orhanobut.hawk.Hawk; + +public class AudioRecorderApp extends Application { + @Override public void onCreate() { + super.onCreate(); + Hawk.init(getApplicationContext()).build(); + } +} diff --git a/app/src/main/java/in/arjsna/voicerecorder/DBHelper.java b/app/src/main/java/in/arjsna/audiorecorder/DBHelper.java similarity index 69% rename from app/src/main/java/in/arjsna/voicerecorder/DBHelper.java rename to app/src/main/java/in/arjsna/audiorecorder/DBHelper.java index c2edbe5..acc61c5 100644 --- a/app/src/main/java/in/arjsna/voicerecorder/DBHelper.java +++ b/app/src/main/java/in/arjsna/audiorecorder/DBHelper.java @@ -1,4 +1,4 @@ -package in.arjsna.voicerecorder; +package in.arjsna.audiorecorder; import android.content.ContentValues; import android.content.Context; @@ -6,26 +6,29 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; -import in.arjsna.voicerecorder.listeners.OnDatabaseChangedListener; +import in.arjsna.audiorecorder.listeners.OnDatabaseChangedListener; +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import java.util.ArrayList; import java.util.Comparator; public class DBHelper extends SQLiteOpenHelper { - private Context mContext; + private final Context mContext; private static final String LOG_TAG = "DBHelper"; - private static OnDatabaseChangedListener mOnDatabaseChangedListener; + private OnDatabaseChangedListener mOnDatabaseChangedListener; - public static final String DATABASE_NAME = "saved_recordings.db"; + private static final String DATABASE_NAME = "saved_recordings.db"; private static final int DATABASE_VERSION = 1; - public static abstract class DBHelperItem implements BaseColumns { - public static final String TABLE_NAME = "saved_recordings"; + static abstract class DBHelperItem implements BaseColumns { + static final String TABLE_NAME = "saved_recordings"; - public static final String COLUMN_NAME_RECORDING_NAME = "recording_name"; - public static final String COLUMN_NAME_RECORDING_FILE_PATH = "file_path"; - public static final String COLUMN_NAME_RECORDING_LENGTH = "length"; - public static final String COLUMN_NAME_TIME_ADDED = "time_added"; + static final String COLUMN_NAME_RECORDING_NAME = "recording_name"; + static final String COLUMN_NAME_RECORDING_FILE_PATH = "file_path"; + static final String COLUMN_NAME_RECORDING_LENGTH = "length"; + static final String COLUMN_NAME_TIME_ADDED = "time_added"; } private static final String TEXT_TYPE = " TEXT"; @@ -65,7 +68,7 @@ public DBHelper(Context context) { mContext = context; } - public static void setOnDatabaseChangedListener(OnDatabaseChangedListener listener) { + public void setOnDatabaseChangedListener(OnDatabaseChangedListener listener) { mOnDatabaseChangedListener = listener; } @@ -90,25 +93,46 @@ public RecordingItem getItemAt(int position) { return null; } + public Observable> getAllRecordings() { + return Observable.fromCallable(() -> { + SQLiteDatabase db = getReadableDatabase(); + ArrayList recordingItems = new ArrayList<>(); + String[] projection = { + DBHelperItem._ID, DBHelperItem.COLUMN_NAME_RECORDING_NAME, + DBHelperItem.COLUMN_NAME_RECORDING_FILE_PATH, DBHelperItem.COLUMN_NAME_RECORDING_LENGTH, + DBHelperItem.COLUMN_NAME_TIME_ADDED + }; + Cursor c = db.query(DBHelperItem.TABLE_NAME, projection, null, null, null, null, null); + while (c.moveToNext()) { + RecordingItem item = new RecordingItem(); + item.setId(c.getInt(c.getColumnIndex(DBHelperItem._ID))); + item.setName(c.getString(c.getColumnIndex(DBHelperItem.COLUMN_NAME_RECORDING_NAME))); + item.setFilePath( + c.getString(c.getColumnIndex(DBHelperItem.COLUMN_NAME_RECORDING_FILE_PATH))); + item.setLength(c.getInt(c.getColumnIndex(DBHelperItem.COLUMN_NAME_RECORDING_LENGTH))); + item.setTime(c.getLong(c.getColumnIndex(DBHelperItem.COLUMN_NAME_TIME_ADDED))); + recordingItems.add(item); + } + c.close(); + return recordingItems; + }).subscribeOn(Schedulers.io()); + } + public void removeItemWithId(int id) { SQLiteDatabase db = getWritableDatabase(); - String[] whereArgs = { String.valueOf(id) }; + String[] whereArgs = {String.valueOf(id)}; db.delete(DBHelperItem.TABLE_NAME, "_ID=?", whereArgs); } public int getCount() { SQLiteDatabase db = getReadableDatabase(); - String[] projection = { DBHelperItem._ID }; + String[] projection = {DBHelperItem._ID}; Cursor c = db.query(DBHelperItem.TABLE_NAME, projection, null, null, null, null, null); int count = c.getCount(); c.close(); return count; } - public Context getContext() { - return mContext; - } - public class RecordingComparator implements Comparator { public int compare(RecordingItem item1, RecordingItem item2) { Long o1 = item1.getTime(); diff --git a/app/src/main/java/in/arjsna/voicerecorder/RecordingItem.java b/app/src/main/java/in/arjsna/audiorecorder/RecordingItem.java similarity index 94% rename from app/src/main/java/in/arjsna/voicerecorder/RecordingItem.java rename to app/src/main/java/in/arjsna/audiorecorder/RecordingItem.java index 17844b4..286124b 100644 --- a/app/src/main/java/in/arjsna/voicerecorder/RecordingItem.java +++ b/app/src/main/java/in/arjsna/audiorecorder/RecordingItem.java @@ -1,4 +1,4 @@ -package in.arjsna.voicerecorder; +package in.arjsna.audiorecorder; import android.os.Parcel; import android.os.Parcelable; @@ -10,10 +10,10 @@ public class RecordingItem implements Parcelable { private int mLength; // length of recording in seconds private long mTime; // date/time of the recording - public RecordingItem() { + RecordingItem() { } - public RecordingItem(Parcel in) { + private RecordingItem(Parcel in) { mName = in.readString(); mFilePath = in.readString(); mId = in.readInt(); diff --git a/app/src/main/java/in/arjsna/audiorecorder/SettingBasic.java b/app/src/main/java/in/arjsna/audiorecorder/SettingBasic.java new file mode 100644 index 0000000..bf50c5f --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/SettingBasic.java @@ -0,0 +1,66 @@ +package in.arjsna.audiorecorder; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.Themed; + +public class SettingBasic extends FrameLayout implements Themed { + private final String iconString; + @StringRes private final int titleRes; + @StringRes private final int captionRes; + TextView title; + TextView caption; + + public SettingBasic(Context context) { + this(context, null); + } + + public SettingBasic(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SettingBasic(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + setBackgroundResource(R.drawable.ripple); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + View rootView = inflater.inflate(R.layout.view_setting_basic, this); + initViews(rootView); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SettingBasic); + iconString = a.getString(R.styleable.SettingBasic_settingIcon); + titleRes = a.getResourceId(R.styleable.SettingBasic_settingTitle, 0); + captionRes = a.getResourceId(R.styleable.SettingBasic_settingCaption, 0); + int minimumApi = a.getInteger(R.styleable.SettingBasic_settingMinApi, 0); + a.recycle(); + + if (Build.VERSION.SDK_INT < minimumApi) setVisibility(GONE); + } + + private void initViews(View rootView) { + title = (TextView) rootView.findViewById(R.id.title); + caption = (TextView) rootView.findViewById(R.id.caption); + } + + @Override + protected void onFinishInflate() { + title.setText(titleRes); + caption.setText(captionRes); + super.onFinishInflate(); + } + + @Override + public void refreshTheme(ThemeHelper themeHelper) { + title.setTextColor(themeHelper.getTextColor()); + caption.setTextColor(themeHelper.getTextColor()); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/SettingWithSwitchView.java b/app/src/main/java/in/arjsna/audiorecorder/SettingWithSwitchView.java new file mode 100644 index 0000000..e03996f --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/SettingWithSwitchView.java @@ -0,0 +1,109 @@ +package in.arjsna.audiorecorder; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v7.widget.SwitchCompat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.Themed; +import in.arjsna.audiorecorder.theme.ThemedActivity; + +public class SettingWithSwitchView extends FrameLayout implements View.OnClickListener, Themed { + private final String preferenceKey; + @StringRes private final int titleRes; + @StringRes private final int captionRes; + private final boolean defaultValue; + TextView title; + TextView caption; + SwitchCompat toggle; + @Nullable private OnClickListener clickListener; + + public SettingWithSwitchView(Context context) { + this(context, null); + } + + public SettingWithSwitchView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SettingWithSwitchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + setBackgroundResource(R.drawable.ripple); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + View rootView = inflater.inflate(R.layout.view_setting_switch, this); + initViews(rootView); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SettingWithSwitchView); + final int prefKeyRes = + a.getResourceId(R.styleable.SettingWithSwitchView_settingPreferenceKey, 0); + if (prefKeyRes == 0) throw new IllegalArgumentException("Invalid preference reference"); + preferenceKey = getResources().getString(prefKeyRes); + titleRes = a.getResourceId(R.styleable.SettingWithSwitchView_settingTitle, 0); + captionRes = a.getResourceId(R.styleable.SettingWithSwitchView_settingCaption, 0); + defaultValue = a.getBoolean(R.styleable.SettingWithSwitchView_settingDefaultValue, false); + int minimumApi = 0;//a.getInteger(R.styleable.SettingWithSwitchView_settingMinApi, 0); + a.recycle(); + + if (Build.VERSION.SDK_INT < minimumApi) setVisibility(GONE); + } + + private void initViews(View rootView) { + title = (TextView) rootView.findViewById(R.id.title); + caption = (TextView) rootView.findViewById(R.id.caption); + toggle = (SwitchCompat) rootView.findViewById(R.id.toggle); + } + + @Override + protected void onFinishInflate() { + title.setText(titleRes); + caption.setText(captionRes); + toggle.setChecked(isChecked()); + super.setOnClickListener(this); + super.onFinishInflate(); + } + + @Override + public void setOnClickListener(@Nullable OnClickListener clickListener) { + this.clickListener = clickListener; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) refreshTheme(((ThemedActivity) getContext()).getThemeHelper()); + } + + @Override + public void onClick(View view) { + toggle(); + refreshTheme(((ThemedActivity) getContext()).getThemeHelper()); + if (clickListener != null) clickListener.onClick(this); + } + + @Override + public void refreshTheme(ThemeHelper themeHelper) { + themeHelper.setSwitchCompactColor(toggle, themeHelper.getAccentColor()); + title.setTextColor(themeHelper.getTextColor()); + caption.setTextColor(themeHelper.getTextColor()); + } + + public boolean isChecked() { + return Hawk.get(preferenceKey, defaultValue); + } + + public boolean toggle() { + Hawk.put(preferenceKey, !isChecked()); + boolean checked = isChecked(); + toggle.setChecked(checked); + return checked; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/activities/MainActivity.java b/app/src/main/java/in/arjsna/audiorecorder/activities/MainActivity.java new file mode 100644 index 0000000..cf77a90 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/activities/MainActivity.java @@ -0,0 +1,104 @@ +package in.arjsna.audiorecorder.activities; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; +import android.view.Menu; +import android.view.MenuItem; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.fragments.RecordFragment; +import in.arjsna.audiorecorder.theme.ThemedActivity; + +public class MainActivity extends ThemedActivity { + + private static final String LOG_TAG = MainActivity.class.getSimpleName(); + private static final int PERMISSION_REQ = 222; + + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.main_container, RecordFragment.newInstance()) + .commit(); + } + getPermissions(); + } + + @Override public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + Intent i = new Intent(this, SettingsActivity.class); + startActivity(i); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @TargetApi(23) + private void getPermissions() { + if (ContextCompat.checkSelfPermission(MainActivity.this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MainActivity.this, + Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + String[] permissions = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO}; + requestPermissions(permissions, PERMISSION_REQ); + } + } + + @TargetApi(23) + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) + || shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + showRationale(false); + } else if (grantResults[0] != PackageManager.PERMISSION_GRANTED + || grantResults[1] != PackageManager.PERMISSION_GRANTED) { + showRationale(true); + } + } + + private void showRationale(boolean openSettings) { + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); + builder.setTitle("Permissions Required") + .setCancelable(false) + .setMessage( + "App requires RECORD_AUDIO permission to access mic and WRITE_EXTERNAL_STORAGE to save recorded audio") + .setPositiveButton(R.string.dialog_action_ok, (dialog, which) -> { + if (openSettings) { + openSettingsPage(); + } else { + getPermissions(); + } + dialog.dismiss(); + }) + .setNegativeButton(R.string.dialog_action_cancel, + (dialog, which) -> onBackPressed()) + .show(); + } + + private void openSettingsPage() { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, PERMISSION_REQ); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + getPermissions(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/activities/PlayListActivity.java b/app/src/main/java/in/arjsna/audiorecorder/activities/PlayListActivity.java new file mode 100644 index 0000000..d3b95fd --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/activities/PlayListActivity.java @@ -0,0 +1,31 @@ +package in.arjsna.audiorecorder.activities; + +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.fragments.PlayListFragment; +import in.arjsna.audiorecorder.theme.ThemedActivity; + +public class PlayListActivity extends ThemedActivity { + + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_record_list); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.tab_title_saved_recordings); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + } + setNavBarColor(); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.record_list_container, PlayListFragment.newInstance()) + .commit(); + } + } +} diff --git a/app/src/main/java/in/arjsna/voicerecorder/activities/SettingsActivity.java b/app/src/main/java/in/arjsna/audiorecorder/activities/SettingsActivity.java similarity index 60% rename from app/src/main/java/in/arjsna/voicerecorder/activities/SettingsActivity.java rename to app/src/main/java/in/arjsna/audiorecorder/activities/SettingsActivity.java index 79b20c4..c18363a 100644 --- a/app/src/main/java/in/arjsna/voicerecorder/activities/SettingsActivity.java +++ b/app/src/main/java/in/arjsna/audiorecorder/activities/SettingsActivity.java @@ -1,20 +1,19 @@ -package in.arjsna.voicerecorder.activities; +package in.arjsna.audiorecorder.activities; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.fragments.SettingsFragment; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.fragments.SettingsFragment; +import in.arjsna.audiorecorder.theme.ThemedActivity; -public class SettingsActivity extends AppCompatActivity { - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { +public class SettingsActivity extends ThemedActivity { + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_preferences); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - toolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { @@ -23,7 +22,7 @@ public class SettingsActivity extends AppCompatActivity { actionBar.setDisplayShowHomeEnabled(true); } - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(R.id.container, new SettingsFragment()) .commit(); } diff --git a/app/src/main/java/in/arjsna/audiorecorder/adapters/PlayListAdapter.java b/app/src/main/java/in/arjsna/audiorecorder/adapters/PlayListAdapter.java new file mode 100644 index 0000000..b3bdb4e --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/adapters/PlayListAdapter.java @@ -0,0 +1,285 @@ +package in.arjsna.audiorecorder.adapters; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import in.arjsna.audiorecorder.DBHelper; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.RecordingItem; +import in.arjsna.audiorecorder.fragments.PlaybackFragment; +import in.arjsna.audiorecorder.listeners.OnDatabaseChangedListener; +import in.arjsna.audiorecorder.recording.Constants; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableSingleObserver; +import io.reactivex.schedulers.Schedulers; +import java.io.File; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +public class PlayListAdapter extends RecyclerView.Adapter + implements OnDatabaseChangedListener { + + private static final String LOG_TAG = "PlayListAdapter"; + + private final DBHelper mDatabase; + + private RecordingItem item; + private Context mContext; + private final LinearLayoutManager llm; + private ArrayList recordingItems; + + public PlayListAdapter(Context context, LinearLayoutManager linearLayoutManager, + ArrayList recordingItems) { + super(); + mContext = context; + mDatabase = new DBHelper(mContext); + mDatabase.setOnDatabaseChangedListener(this); + this.recordingItems = recordingItems; + llm = linearLayoutManager; + } + + @Override public void onBindViewHolder(final RecordingsViewHolder holder, int position) { + + item = recordingItems.get(position); + long itemDuration = item.getLength(); + + long minutes = TimeUnit.MILLISECONDS.toMinutes(itemDuration); + long seconds = + TimeUnit.MILLISECONDS.toSeconds(itemDuration) - TimeUnit.MINUTES.toSeconds(minutes); + + holder.vName.setText(item.getName()); + holder.vLength.setText( + String.format(mContext.getString(R.string.play_time_format), minutes, seconds)); + holder.vDateAdded.setText(DateUtils.formatDateTime(mContext, item.getTime(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + // define an on click listener to open PlaybackFragment + holder.cardView.setOnClickListener(view -> { + try { + PlaybackFragment playbackFragment = + new PlaybackFragment().newInstance(recordingItems.get(holder.getAdapterPosition())); + + FragmentTransaction transaction = + ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction(); + + playbackFragment.show(transaction, "dialog_playback"); + } catch (Exception e) { + Log.e(LOG_TAG, "exception", e); + } + }); + + holder.cardView.setOnLongClickListener(v -> { + + ArrayList entrys = new ArrayList<>(); + entrys.add(mContext.getString(R.string.dialog_file_share)); + entrys.add(mContext.getString(R.string.dialog_file_rename)); + entrys.add(mContext.getString(R.string.dialog_file_delete)); + + final CharSequence[] items = entrys.toArray(new CharSequence[entrys.size()]); + + // File delete confirm + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(mContext.getString(R.string.dialog_title_options)); + builder.setItems(items, (dialog, item) -> { + if (item == 0) { + shareFileDialog(holder.getAdapterPosition()); + } + if (item == 1) { + renameFileDialog(holder.getAdapterPosition()); + } else if (item == 2) { + deleteFileDialog(holder.getAdapterPosition()); + } + }); + builder.setCancelable(true); + builder.setNegativeButton(mContext.getString(R.string.dialog_action_cancel), + (dialog, id) -> dialog.cancel()); + + AlertDialog alert = builder.create(); + alert.show(); + + return false; + }); + } + + @Override public RecordingsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + View itemView = LayoutInflater. + from(parent.getContext()). + inflate(R.layout.card_view, parent, false); + + mContext = parent.getContext(); + + return new RecordingsViewHolder(itemView); + } + + public void addAllAndNotify(ArrayList recordingItems) { + this.recordingItems.addAll(recordingItems); + notifyDataSetChanged(); + } + + static class RecordingsViewHolder extends RecyclerView.ViewHolder { + final TextView vName; + final TextView vLength; + final TextView vDateAdded; + final View cardView; + + RecordingsViewHolder(View v) { + super(v); + vName = (TextView) v.findViewById(R.id.file_name_text); + vLength = (TextView) v.findViewById(R.id.file_length_text); + vDateAdded = (TextView) v.findViewById(R.id.file_date_added_text); + cardView = v.findViewById(R.id.card_view); + } + } + + @Override public int getItemCount() { + return recordingItems.size(); + } + + @Override public void onNewDatabaseEntryAdded() { + //item added to top of the list + notifyItemInserted(getItemCount() - 1); + llm.scrollToPosition(getItemCount() - 1); + } + + @Override + //TODO + public void onDatabaseEntryRenamed() { + + } + + private Single remove(int position) { + return Single.create((SingleOnSubscribe) e -> { + RecordingItem recordingItem = recordingItems.get(position); + File file = new File(recordingItem.getFilePath()); + if (file.delete()) { + mDatabase.removeItemWithId(recordingItem.getId()); + recordingItems.remove(position); + e.onSuccess(recordingItem.getName()); + } else { + e.onError(new Exception("File deletion failed")); + } + }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + //TODO + public void removeOutOfApp(String filePath) { + //user deletes a saved recording out of the application through another application + } + + private Single rename(int position, String name) { + return Single.create((SingleOnSubscribe) e -> { + String mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath(); + mFilePath += "/SoundRecorder/" + name; + File f = new File(mFilePath); + + if (f.exists() && !f.isDirectory()) { + e.onError(new Exception(mContext.getString(R.string.toast_file_exists))); + } else { + RecordingItem currentItem = recordingItems.get(position); + File oldFilePath = new File(currentItem.getFilePath()); + oldFilePath.renameTo(f); + mDatabase.renameItem(currentItem, name, mFilePath); + currentItem.setName(name); + e.onSuccess(position); + } + }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + private void shareFileDialog(int position) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + Uri.fromFile(new File(recordingItems.get(position).getFilePath()))); + shareIntent.setType("audio/mp4"); + mContext.startActivity(Intent.createChooser(shareIntent, mContext.getText(R.string.send_to))); + } + + private void renameFileDialog(final int position) { + // File rename dialog + AlertDialog.Builder renameFileBuilder = new AlertDialog.Builder(mContext); + + LayoutInflater inflater = LayoutInflater.from(mContext); + View view = inflater.inflate(R.layout.dialog_rename_file, null); + + final EditText input = (EditText) view.findViewById(R.id.new_name); + + renameFileBuilder.setTitle(mContext.getString(R.string.dialog_title_rename)); + renameFileBuilder.setCancelable(true); + renameFileBuilder.setPositiveButton(mContext.getString(R.string.dialog_action_ok), + (dialog, id) -> { + String value = + input.getText().toString().trim() + Constants.AUDIO_RECORDER_FILE_EXT_WAV; + rename(position, value).subscribe(new DisposableSingleObserver() { + @Override public void onSuccess(Integer removedPosition) { + notifyItemChanged(removedPosition); + } + + @Override public void onError(Throwable e) { + Toast.makeText(mContext, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + dialog.cancel(); + }); + renameFileBuilder.setNegativeButton(mContext.getString(R.string.dialog_action_cancel), + (dialog, id) -> dialog.cancel()); + + renameFileBuilder.setView(view); + AlertDialog alert = renameFileBuilder.create(); + alert.show(); + } + + private void deleteFileDialog(final int position) { + // File delete confirm + AlertDialog.Builder confirmDelete = new AlertDialog.Builder(mContext); + confirmDelete.setTitle(mContext.getString(R.string.dialog_title_delete)); + confirmDelete.setMessage(mContext.getString(R.string.dialog_text_delete)); + confirmDelete.setCancelable(true); + confirmDelete.setPositiveButton(mContext.getString(R.string.dialog_action_yes), + (dialog, id) -> { + try { + remove(position).subscribe(new DisposableSingleObserver() { + @Override public void onSuccess(String removedFileName) { + Toast.makeText(mContext, + String.format(mContext.getString(R.string.toast_file_delete), + removedFileName), + Toast.LENGTH_SHORT).show(); + notifyItemRemoved(position); + } + + @Override public void onError(Throwable e) { + Toast.makeText(mContext, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + Log.e(LOG_TAG, "exception", e); + } + + dialog.cancel(); + }); + confirmDelete.setNegativeButton(mContext.getString(R.string.dialog_action_no), + (dialog, id) -> dialog.cancel()); + + AlertDialog alert = confirmDelete.create(); + alert.show(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/AudioVisualization.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/AudioVisualization.java new file mode 100644 index 0000000..d972753 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/AudioVisualization.java @@ -0,0 +1,31 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.support.annotation.NonNull; + +/** + * Audio visualization view interface + */ +public interface AudioVisualization { + + /** + * Link view to custom implementation of {@link DbmHandler}. + * + * @param dbmHandler instance of DbmHandler + */ + void linkTo(@NonNull DbmHandler dbmHandler); + + /** + * Resume audio visualization. + */ + void onResume(); + + /** + * Pause audio visualization. + */ + void onPause(); + + /** + * Release resources of audio visualization. + */ + void release(); +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Constants.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Constants.java new file mode 100644 index 0000000..3fd0262 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Constants.java @@ -0,0 +1,100 @@ +package in.arjsna.audiorecorder.audiovisualization; + +/** + * Constants holder class. + */ +final class Constants { + + /** + * Minimum number of waves to display. + */ + public static final int MIN_WAVES_COUNT = 1; + + /** + * Maximum number of waves to display. + */ + public static final int MAX_WAVES_COUNT = 16; + + /** + * Default number of waves to display. + */ + public static final int DEFAULT_WAVES_COUNT = 7; + + /** + * Minimum number of layers to display. + */ + public static final int MIN_LAYERS_COUNT = 1; + + /** + * Maximum number of waves to display. + */ + public static final int MAX_LAYERS_COUNT = 4; + + /** + * Default number of waves to display. + */ + public static final int DEFAULT_LAYERS_COUNT = MAX_LAYERS_COUNT; + + /** + * Minimum wave height (in pixels). + */ + public static final float MIN_WAVE_HEIGHT = 10; + + /** + * Maximum wave height (in pixels). + */ + public static final float MAX_WAVE_HEIGHT = 1920; + + /** + * Default wave height (in pixels). + */ + public static final float DEFAULT_WAVE_HEIGHT = MIN_WAVE_HEIGHT; + + /** + * Minimum bubble size (in pixels). + */ + public static final int MIN_BUBBLE_SIZE = 10; + + /** + * Maximum bubble size (in pixels). + */ + public static final int MAX_BUBBLE_SIZE = 200; + + /** + * Default bubble size (in pixels). + */ + public static final int DEFAULT_BUBBLE_SIZE = 20; + + /** + * Minimum footer height (in pixels). + */ + public static final float MIN_FOOTER_HEIGHT = 20; + + /** + * Maximum footer height (in pixels). + */ + public static final float MAX_FOOTER_HEIGHT = 1080; + + /** + * Default footer height (in pixels). + */ + public static final float DEFAULT_FOOTER_HEIGHT = 640; + + /** + * Default number of bubbles per layer. + */ + public static final int DEFAULT_BUBBLES_PER_LAYER = 8; + + /** + * Minimum number of bubbles per layer. + */ + public static final int DEFAULT_BUBBLES_PER_LAYER_MIN = 1; + + /** + * Maximum number of bubbles per layer. + */ + public static final int DEFAULT_BUBBLES_PER_LAYER_MAX = 36; + + private Constants() { + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/DbmHandler.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/DbmHandler.java new file mode 100644 index 0000000..43b13c8 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/DbmHandler.java @@ -0,0 +1,143 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import in.arjsna.audiorecorder.recording.AudioRecorder; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Abstract class for converting your data to dBm values. + * When you're have new portion of data, call {@link #onDataReceived(Object)} method. + */ +public abstract class DbmHandler { + + private static final long UPDATE_INTERVAL = 16; + private int layersCount; + private InnerAudioVisualization audioVisualization; + private float[] dBmArray; + private float[] ampsArray; + private float[] emptyArray; + private Timer timer; + private boolean isVisualizationSetup = false; + protected AudioRecorder audioRecorder; + + void setUp(@NonNull InnerAudioVisualization audioVisualization, int layersCount) { + isVisualizationSetup = true; + this.audioVisualization = audioVisualization; + this.layersCount = layersCount; + this.dBmArray = new float[layersCount]; + this.ampsArray = new float[layersCount]; + this.emptyArray = new float[layersCount]; + } + + /** + * Call this method when your data is available for conversion. + * + * @param data any data + */ + public final void onDataReceived(TData data) { + onDataReceivedImpl(data, layersCount, dBmArray, ampsArray); + if (isVisualizationSetup) { + audioVisualization.onDataReceived(dBmArray, ampsArray); + startRendering(); + } + } + + /** + * Start rendering thread. + */ + protected final void startRendering() { + cancelTimer(); + audioVisualization.startRendering(); + } + + /** + * Stop rendering thread. + */ + protected final void stopRendering() { + cancelTimer(); + if (audioVisualization != null) { + audioVisualization.stopRendering(); + } + } + + /** + * Post empty values to renderer and stop rendering thread after waves calm down. + */ + protected final void calmDownAndStopRendering() { + if (timer == null) { + timer = new Timer("Stop Rendering Timer"); + timer.scheduleAtFixedRate(new TimerTask() { + @Override public void run() { + if (audioVisualization != null) { + audioVisualization.onDataReceived(emptyArray, emptyArray); + } + } + }, UPDATE_INTERVAL, UPDATE_INTERVAL); + } + if (isVisualizationSetup) { + audioVisualization.calmDownListener(this::stopRendering); + } + } + + /** + * Cancel timer posting empty values. + */ + private void cancelTimer() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + } + + /** + * Called after {@link AudioVisualization#onResume()} call. + */ + public void onResume() { + + } + + /** + * Called after {@link AudioVisualization#onPause()} call. + */ + public void onPause() { + + } + + /** + * Called after {@link AudioVisualization#release()} ()} call. + */ + @CallSuper public void release() { + isVisualizationSetup = false; + dBmArray = null; + ampsArray = null; + audioVisualization = null; + } + + /** + * Implement your own data conversion. + * + * @param data any data + * @param layersCount layers count + * @param dBmArray array of normalized (in range [0..1]) dBm values that should be populated by + * you. + * Array size is equals to {@code layersCount} value. + * @param ampsArray array of amplitude values that should be populated by you. + * Array size is equals to {@code layersCount} value.

+ * This values affect the appearance of bubbles. If new amplitude value is greater + * than previous value and normalized dBm value is greater than 0.25, bubbles will appear on + * screen. + * In case if amplitude is less than previous value, exponential smoothing (Holt - Winters) + * used for smoothing amplitude values. + */ + protected abstract void onDataReceivedImpl(TData data, int layersCount, float[] dBmArray, + float[] ampsArray); + + public void addRecorder(AudioRecorder audioRecorder) { + this.audioRecorder = audioRecorder; + } + + public abstract void startDbmThread(); +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLAudioVisualizationView.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLAudioVisualizationView.java new file mode 100644 index 0000000..0331981 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLAudioVisualizationView.java @@ -0,0 +1,500 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.opengl.GLSurfaceView; +import android.support.annotation.ArrayRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.theme.ColorPalette; + +/** + * Audio visualization view implementation for OpenGL. + */ +public class GLAudioVisualizationView extends GLSurfaceView + implements AudioVisualization, InnerAudioVisualization { + + private static final int EGL_VERSION = 2; + private final GLRenderer renderer; + private DbmHandler dbmHandler; + private final Configuration configuration; + private CalmDownListener innerCalmDownListener; + + private GLAudioVisualizationView(@NonNull Builder builder) { + super(builder.context); + configuration = new Configuration(builder); + renderer = new GLRenderer(getContext(), configuration); + init(); + } + + public GLAudioVisualizationView(Context context, AttributeSet attrs) { + super(context, attrs); + configuration = new Configuration(context, attrs, isInEditMode()); + renderer = new GLRenderer(getContext(), configuration); + init(); + } + + private void init() { + setEGLContextClientVersion(EGL_VERSION); + setRenderer(renderer); + renderer.calmDownListener(() -> { + stopRendering(); + if (innerCalmDownListener != null) { + innerCalmDownListener.onCalmedDown(); + } + }); + } + + @Override public void onResume() { + super.onResume(); + if (dbmHandler != null) { + dbmHandler.onResume(); + } + } + + @Override public void onPause() { + if (dbmHandler != null) { + dbmHandler.onPause(); + } + super.onPause(); + } + + @Override public void linkTo(@NonNull DbmHandler dbmHandler) { + if (this.dbmHandler != null) { + this.dbmHandler.release(); + } + this.dbmHandler = dbmHandler; + this.dbmHandler.setUp(this, configuration.layersCount); + } + + @Override public void release() { + if (dbmHandler != null) { + dbmHandler = null; + calmDownListener(null); + renderer.calmDownListener(null); + } + } + + @Override public void startRendering() { + if (getRenderMode() != RENDERMODE_CONTINUOUSLY) { + setRenderMode(RENDERMODE_CONTINUOUSLY); + } + } + + @Override public void stopRendering() { + if (getRenderMode() != RENDERMODE_WHEN_DIRTY) { + setRenderMode(RENDERMODE_WHEN_DIRTY); + } + } + + @Override public void calmDownListener(@Nullable CalmDownListener calmDownListener) { + innerCalmDownListener = calmDownListener; + } + + @Override public void onDataReceived(float[] dBmArray, float[] ampsArray) { + renderer.onDataReceived(dBmArray, ampsArray); + } + + public void updateConfig(ColorsBuilder colorsBuilder) { + renderer.updateConfiguration(colorsBuilder); + } + + /** + * Configuration holder class. + */ + static class Configuration { + + int wavesCount; + int layersCount; + int bubblesPerLayer; + float bubbleSize; + float waveHeight; + float footerHeight; + boolean randomizeBubbleSize; + float[] backgroundColor; + float[][] layerColors; + + public Configuration(Context context, AttributeSet attrs, boolean isInEditMode) { + TypedArray array = + context.obtainStyledAttributes(attrs, R.styleable.GLAudioVisualizationView); + int[] colors; + int bgColor; + try { + layersCount = array.getInt(R.styleable.GLAudioVisualizationView_av_layersCount, + Constants.DEFAULT_LAYERS_COUNT); + layersCount = + Utils.between(layersCount, Constants.MIN_LAYERS_COUNT, Constants.MAX_LAYERS_COUNT); + wavesCount = array.getInt(R.styleable.GLAudioVisualizationView_av_wavesCount, + Constants.DEFAULT_WAVES_COUNT); + wavesCount = + Utils.between(wavesCount, Constants.MIN_WAVES_COUNT, Constants.MAX_WAVES_COUNT); + waveHeight = + array.getDimensionPixelSize(R.styleable.GLAudioVisualizationView_av_wavesHeight, + (int) Constants.DEFAULT_WAVE_HEIGHT); + waveHeight = + Utils.between(waveHeight, Constants.MIN_WAVE_HEIGHT, Constants.MAX_WAVE_HEIGHT); + bubbleSize = + array.getDimensionPixelSize(R.styleable.GLAudioVisualizationView_av_bubblesSize, + Constants.DEFAULT_BUBBLE_SIZE); + bubbleSize = + Utils.between(bubbleSize, Constants.MIN_BUBBLE_SIZE, Constants.MAX_BUBBLE_SIZE); + randomizeBubbleSize = + array.getBoolean(R.styleable.GLAudioVisualizationView_av_bubblesRandomizeSizes, false); + footerHeight = + array.getDimensionPixelSize(R.styleable.GLAudioVisualizationView_av_wavesFooterHeight, + (int) Constants.DEFAULT_FOOTER_HEIGHT); + footerHeight = + Utils.between(footerHeight, Constants.MIN_FOOTER_HEIGHT, Constants.MAX_FOOTER_HEIGHT); + bubblesPerLayer = array.getInt(R.styleable.GLAudioVisualizationView_av_bubblesPerLayer, + Constants.DEFAULT_BUBBLES_PER_LAYER); + bubblesPerLayer = Utils.between(bubblesPerLayer, Constants.DEFAULT_BUBBLES_PER_LAYER_MIN, + Constants.DEFAULT_BUBBLES_PER_LAYER_MAX); + bgColor = array.getColor(R.styleable.GLAudioVisualizationView_av_backgroundColor, + Color.TRANSPARENT); + if (bgColor == Color.TRANSPARENT) { + bgColor = ContextCompat.getColor(context, R.color.av_color_bg); + } + // TODO: 9/9/17 fix this + //int arrayId = array.getResourceId(R.styleable.GLAudioVisualizationView_av_wavesColors, + // R.array.av_colors); + //if (isInEditMode) { + // colors = new int[layersCount]; + //} else { + // TypedArray colorsArray = array.getResources().obtainTypedArray(arrayId); + // colors = new int[colorsArray.length()]; + // for (int i = 0; i < colorsArray.length(); i++) { + // colors[i] = colorsArray.getColor(i, Color.TRANSPARENT); + // } + // colorsArray.recycle(); + //} + } finally { + array.recycle(); + } + colors = Hawk.get(context.getString(R.string.preference_layer_colors), + ColorPalette.getColors(context, ContextCompat.getColor(context, R.color.av_color5))); + if (colors.length < layersCount) { + throw new IllegalArgumentException("You specified more layers than colors."); + } + + layerColors = new float[colors.length][]; + for (int i = 0; i < colors.length; i++) { + layerColors[i] = Utils.convertColor(colors[i]); + } + backgroundColor = Utils.convertColor(bgColor); + bubbleSize /= context.getResources().getDisplayMetrics().widthPixels; + } + + private Configuration(@NonNull Builder builder) { + this.waveHeight = builder.waveHeight; + waveHeight = Utils.between(waveHeight, Constants.MIN_WAVE_HEIGHT, Constants.MAX_WAVE_HEIGHT); + this.wavesCount = builder.wavesCount; + wavesCount = Utils.between(wavesCount, Constants.MIN_WAVES_COUNT, Constants.MAX_WAVES_COUNT); + this.layerColors = builder.layerColors(); + this.bubbleSize = builder.bubbleSize; + bubbleSize = Utils.between(bubbleSize, Constants.MIN_BUBBLE_SIZE, Constants.MAX_BUBBLE_SIZE); + this.bubbleSize = + this.bubbleSize / builder.context.getResources().getDisplayMetrics().widthPixels; + this.footerHeight = builder.footerHeight; + footerHeight = + Utils.between(footerHeight, Constants.MIN_FOOTER_HEIGHT, Constants.MAX_FOOTER_HEIGHT); + this.randomizeBubbleSize = builder.randomizeBubbleSize; + this.backgroundColor = builder.backgroundColor(); + this.layersCount = builder.layersCount; + this.bubblesPerLayer = builder.bubblesPerLayer; + Utils.between(bubblesPerLayer, Constants.DEFAULT_BUBBLES_PER_LAYER_MIN, + Constants.DEFAULT_BUBBLES_PER_LAYER_MAX); + layersCount = + Utils.between(layersCount, Constants.MIN_LAYERS_COUNT, Constants.MAX_LAYERS_COUNT); + if (layerColors.length < layersCount) { + throw new IllegalArgumentException("You specified more layers than colors."); + } + } + } + + public static class ColorsBuilder { + private float[] backgroundColor; + private float[][] layerColors; + private final Context context; + + public ColorsBuilder(@NonNull Context context) { + this.context = context; + } + + float[][] layerColors() { + return layerColors; + } + + float[] backgroundColor() { + return backgroundColor; + } + + /** + * Set background color + * + * @param backgroundColor background color + */ + public T setBackgroundColor(@ColorInt int backgroundColor) { + this.backgroundColor = Utils.convertColor(backgroundColor); + return getThis(); + } + + /** + * Set layer colors from array resource + * + * @param arrayId array resource + */ + public T setLayerColors(@ArrayRes int arrayId) { + TypedArray colorsArray = context.getResources().obtainTypedArray(arrayId); + int[] colors = new int[colorsArray.length()]; + for (int i = 0; i < colorsArray.length(); i++) { + colors[i] = colorsArray.getColor(i, Color.TRANSPARENT); + } + colorsArray.recycle(); + return setLayerColors(colors); + } + + /** + * Set layer colors. + * + * @param colors array of colors + */ + public T setLayerColors(int[] colors) { + layerColors = new float[colors.length][]; + for (int i = 0; i < colors.length; i++) { + layerColors[i] = Utils.convertColor(colors[i]); + } + return getThis(); + } + + /** + * Set background color from color resource + * + * @param backgroundColor color resource + */ + public T setBackgroundColorRes(@ColorRes int backgroundColor) { + return setBackgroundColor(ContextCompat.getColor(context, backgroundColor)); + } + + protected T getThis() { + //noinspection unchecked + return (T) this; + } + } + + public static class Builder extends ColorsBuilder { + + private Context context; + private int wavesCount; + private int layersCount; + private float bubbleSize; + private float waveHeight; + private float footerHeight; + private boolean randomizeBubbleSize; + private int bubblesPerLayer; + + public Builder(@NonNull Context context) { + super(context); + this.context = context; + } + + @Override protected Builder getThis() { + return this; + } + + /** + * Set waves count + * + * @param wavesCount waves count + */ + public Builder setWavesCount(int wavesCount) { + this.wavesCount = wavesCount; + return this; + } + + /** + * Set layers count + * + * @param layersCount layers count + */ + public Builder setLayersCount(int layersCount) { + this.layersCount = layersCount; + return this; + } + + /** + * Set bubbles size in pixels + * + * @param bubbleSize bubbles size in pixels + */ + public Builder setBubblesSize(float bubbleSize) { + this.bubbleSize = bubbleSize; + return this; + } + + /** + * Set bubble size from dimension resource + * + * @param bubbleSize dimension resource + */ + public Builder setBubblesSize(@DimenRes int bubbleSize) { + return setBubblesSize((float) context.getResources().getDimensionPixelSize(bubbleSize)); + } + + /** + * Set wave height in pixels + * + * @param waveHeight wave height in pixels + */ + public Builder setWavesHeight(float waveHeight) { + this.waveHeight = waveHeight; + return this; + } + + /** + * Set wave height from dimension resource + * + * @param waveHeight dimension resource + */ + public Builder setWavesHeight(@DimenRes int waveHeight) { + return setWavesHeight((float) context.getResources().getDimensionPixelSize(waveHeight)); + } + + /** + * Set footer height in pixels + * + * @param footerHeight footer height in pixels + */ + public Builder setWavesFooterHeight(float footerHeight) { + this.footerHeight = footerHeight; + return this; + } + + /** + * Set footer height from dimension resource + * + * @param footerHeight dimension resource + */ + public Builder setWavesFooterHeight(@DimenRes int footerHeight) { + return setWavesFooterHeight( + (float) context.getResources().getDimensionPixelSize(footerHeight)); + } + + /** + * Set flag indicates that size of bubbles should be randomized + * + * @param randomizeBubbleSize true if size of bubbles should be randomized, false if size of + * bubbles must be the same + */ + public Builder setBubblesRandomizeSize(boolean randomizeBubbleSize) { + this.randomizeBubbleSize = randomizeBubbleSize; + return this; + } + + /** + * Set number of bubbles per layer. + * + * @param bubblesPerLayer number of bubbles per layer + */ + public Builder setBubblesPerLayer(int bubblesPerLayer) { + this.bubblesPerLayer = bubblesPerLayer; + return this; + } + + public GLAudioVisualizationView build() { + return new GLAudioVisualizationView(this); + } + } + + /** + * Renderer builder. + */ + public static class RendererBuilder { + + private final Builder builder; + private GLSurfaceView glSurfaceView; + private DbmHandler handler; + + /** + * Create new renderer using existing Audio Visualization builder. + * + * @param builder instance of Audio Visualization builder + */ + public RendererBuilder(@NonNull Builder builder) { + this.builder = builder; + } + + /** + * Set dBm handler. + * + * @param handler instance of dBm handler + */ + public RendererBuilder handler(DbmHandler handler) { + this.handler = handler; + return this; + } + + /** + * Set OpenGL surface view. + * + * @param glSurfaceView instance of OpenGL surface view + */ + public RendererBuilder glSurfaceView(@NonNull GLSurfaceView glSurfaceView) { + this.glSurfaceView = glSurfaceView; + return this; + } + + /** + * Create new Audio Visualization Renderer. + * + * @return new Audio Visualization Renderer + */ + public AudioVisualizationRenderer build() { + final GLRenderer renderer = new GLRenderer(builder.context, new Configuration(builder)); + final InnerAudioVisualization audioVisualization = new InnerAudioVisualization() { + @Override public void startRendering() { + if (glSurfaceView.getRenderMode() != RENDERMODE_CONTINUOUSLY) { + glSurfaceView.setRenderMode(RENDERMODE_CONTINUOUSLY); + } + } + + @Override public void stopRendering() { + if (glSurfaceView.getRenderMode() != RENDERMODE_WHEN_DIRTY) { + glSurfaceView.setRenderMode(RENDERMODE_WHEN_DIRTY); + } + } + + @Override public void calmDownListener(@Nullable CalmDownListener calmDownListener) { + + } + + @Override public void onDataReceived(float[] dBmArray, float[] ampsArray) { + renderer.onDataReceived(dBmArray, ampsArray); + } + }; + renderer.calmDownListener(audioVisualization::stopRendering); + handler.setUp(audioVisualization, builder.layersCount); + return renderer; + } + } + + /** + * Audio Visualization renderer interface that allows to change waves' colors at runtime. + */ + public interface AudioVisualizationRenderer extends Renderer { + + /** + * Update colors configuration. + * + * @param builder instance of color builder. + */ + void updateConfiguration(@NonNull ColorsBuilder builder); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLBubble.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLBubble.java new file mode 100644 index 0000000..8bf900c --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLBubble.java @@ -0,0 +1,138 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.opengl.GLES20; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.Random; + +/** + * Bubble implementation. + */ +class GLBubble extends GLShape { + + /** + * Duration of bubble movement. + */ + private static final long BUBBLE_ANIMATION_DURATION = 1000; + private static final float BUBBLE_D_ANGLE = (float) (2 * Math.PI / BUBBLE_ANIMATION_DURATION); + + /** + * Number of points for drawing circle. + */ + private static final int POINTS_PER_CIRCLE = 40; + private static final float TOP_Y = 1f; + private final FloatBuffer vertexBuffer; + private final ShortBuffer shortBuffer; + private final Random random; + private float fromY; + private float size; + private float speed; + private float virtualSpeed; + private float centerY = -1; + private float startX; + private float angle; + + public GLBubble(float[] color, float startX, float fromY, float toY, float size, Random random) { + super(color); + this.random = random; + update(startX, fromY, toY, size); + float[] vertices = new float[(POINTS_PER_CIRCLE + 1) * COORDS_PER_VERTEX]; + short[] indices = new short[POINTS_PER_CIRCLE * COORDS_PER_VERTEX]; + int i; + for (i = 0; i < indices.length / COORDS_PER_VERTEX - 1; i++) { + indices[COORDS_PER_VERTEX * i] = 0; + indices[COORDS_PER_VERTEX * i + 1] = (short) (i + 1); + indices[COORDS_PER_VERTEX * i + 2] = (short) (i + 2); + } + // connect first and last elements + indices[COORDS_PER_VERTEX * i] = 0; + indices[COORDS_PER_VERTEX * i + 1] = (short) (i + 1); + indices[COORDS_PER_VERTEX * i + 2] = (short) 1; + ByteBuffer verticesByteBuffer = ByteBuffer.allocateDirect(vertices.length * SIZE_OF_FLOAT); + verticesByteBuffer.order(ByteOrder.nativeOrder()); + vertexBuffer = verticesByteBuffer.asFloatBuffer(); + vertexBuffer.put(vertices); + vertexBuffer.position(0); + ByteBuffer indicesByteBuffer = ByteBuffer.allocateDirect(indices.length * SIZE_OF_SHORT); + indicesByteBuffer.order(ByteOrder.nativeOrder()); + shortBuffer = indicesByteBuffer.asShortBuffer(); + shortBuffer.put(indices); + shortBuffer.position(0); + angle = (float) (random.nextFloat() * 2 * Math.PI); + } + + /** + * Update position of bubble. + * + * @param dt time elapsed from last calculations + * @param ratioY aspect ratio for Y coordinates + */ + public void update(long dt, float ratioY) { + double step = 2 * Math.PI / POINTS_PER_CIRCLE; + angle += dt * BUBBLE_D_ANGLE; + float fromX = startX + (float) (0.05f * Math.sin(angle)); + float toX = fromX + size; + float fromY = this.fromY + dt * speed; + float toY = fromY + size; + centerY += dt * virtualSpeed; + getColor()[3] = (TOP_Y - centerY / TOP_Y); + vertexBuffer.put(0, Utils.normalizeGl(0, fromX, toX)); + vertexBuffer.put(1, Utils.normalizeGl(centerY * ratioY, fromY, toY)); + for (int i = 1; i <= POINTS_PER_CIRCLE; i++) { + vertexBuffer.put(COORDS_PER_VERTEX * i, + Utils.normalizeGl((float) Math.sin(-Math.PI + step * i), fromX, toX)); + vertexBuffer.put(COORDS_PER_VERTEX * i + 1, + Utils.normalizeGl((float) Math.cos(-Math.PI + step * i) * ratioY, fromY, toY)); + } + this.fromY = fromY; + } + + /** + * Draw bubble. + */ + public void draw() { + GLES20.glUseProgram(getProgram()); + int positionHandle = GLES20.glGetAttribLocation(getProgram(), VERTEX_POSITION); + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + COORDS_PER_VERTEX * SIZE_OF_FLOAT, vertexBuffer); + int colorHandle = GLES20.glGetUniformLocation(getProgram(), VERTEX_COLOR); + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + GLES20.glUniform4fv(colorHandle, 1, getColor(), 0); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_FAN, shortBuffer.capacity(), GLES20.GL_UNSIGNED_SHORT, + shortBuffer); + GLES20.glDisableVertexAttribArray(positionHandle); + GLES20.glDisable(GLES20.GL_BLEND); + } + + /** + * Check if bubble is moved out of specified area. + * + * @return true if bubble is outside of specified area, false otherwise + */ + public boolean isOffScreen() { + return centerY > TOP_Y; + } + + /** + * Update bubble's area of movement. + * + * @param startX start X position + * @param fromY start Y position + * @param toY end Y position + * @param size size of bubble + */ + public void update(float startX, float fromY, float toY, float size) { + this.fromY = fromY; + this.size = size; + this.startX = startX; + this.centerY = -1; + float coef = 0.4f + random.nextFloat() * 0.8f; // randomize speed of movement + this.speed = (toY - fromY) / BUBBLE_ANIMATION_DURATION * coef; + this.virtualSpeed = 2f / BUBBLE_ANIMATION_DURATION * coef; + getColor()[3] = 1f; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRectangle.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRectangle.java new file mode 100644 index 0000000..9ee720a --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRectangle.java @@ -0,0 +1,53 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.opengl.GLES20; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + +/** + * Rectangle implementation. + */ +class GLRectangle extends GLShape { + + private final FloatBuffer vertexBuffer; + private final ShortBuffer shortBuffer; + + public GLRectangle(float[] color, float fromX, float toX, float fromY, float toY) { + super(color); + final float[] vertices = { + Utils.normalizeGl(-1, fromX, toX), Utils.normalizeGl(1, fromY, toY), 0, + Utils.normalizeGl(-1, fromX, toX), Utils.normalizeGl(-1, fromY, toY), 0, + Utils.normalizeGl(1, fromX, toX), Utils.normalizeGl(-1, fromY, toY), 0, + Utils.normalizeGl(1, fromX, toX), Utils.normalizeGl(1, fromY, toY), 0 + }; + ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * SIZE_OF_FLOAT); + vertexByteBuffer.order(ByteOrder.nativeOrder()); + vertexBuffer = vertexByteBuffer.asFloatBuffer(); + vertexBuffer.put(vertices); + vertexBuffer.position(0); + final short[] indices = { 0, 1, 2, 0, 2, 3 }; + ByteBuffer indicesByteBuffer = ByteBuffer.allocateDirect(indices.length * SIZE_OF_SHORT); + indicesByteBuffer.order(ByteOrder.nativeOrder()); + shortBuffer = indicesByteBuffer.asShortBuffer(); + shortBuffer.put(indices); + shortBuffer.position(0); + } + + /** + * Draw rectangle. + */ + public void draw() { + GLES20.glUseProgram(getProgram()); + int positionHandle = GLES20.glGetAttribLocation(getProgram(), VERTEX_POSITION); + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + COORDS_PER_VERTEX * SIZE_OF_FLOAT, vertexBuffer); + int colorHandle = GLES20.glGetUniformLocation(getProgram(), VERTEX_COLOR); + GLES20.glUniform4fv(colorHandle, 1, getColor(), 0); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_FAN, shortBuffer.capacity(), GLES20.GL_UNSIGNED_SHORT, + shortBuffer); + GLES20.glDisableVertexAttribArray(positionHandle); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRenderer.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRenderer.java new file mode 100644 index 0000000..2e32d52 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLRenderer.java @@ -0,0 +1,128 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.content.Context; +import android.opengl.GLES20; +import android.support.annotation.NonNull; +import java.util.Random; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * OpenGL renderer implementation. + */ +class GLRenderer implements GLAudioVisualizationView.AudioVisualizationRenderer { + + private static final long ANIMATION_TIME = 400; + private static final float D_ANGLE = (float) (2 * Math.PI / ANIMATION_TIME); + + private final GLAudioVisualizationView.Configuration configuration; + private GLWaveLayer[] layers; + private long startTime; + private final float height; + private final Random random; + private float ratioY = 1; + private InnerAudioVisualization.CalmDownListener calmDownListener; + boolean bgUpdated; + + public GLRenderer(@NonNull Context context, + GLAudioVisualizationView.Configuration configuration) { + this.configuration = configuration; + this.random = new Random(); + startTime = System.currentTimeMillis(); + height = context.getResources().getDisplayMetrics().heightPixels; + } + + public GLRenderer calmDownListener(InnerAudioVisualization.CalmDownListener calmDownListener) { + this.calmDownListener = calmDownListener; + return this; + } + + @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { + float[] backgroundColor = configuration.backgroundColor; + GLES20.glClearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], + backgroundColor[3]); + layers = new GLWaveLayer[configuration.layersCount]; + float layerHeightPerc = (configuration.footerHeight + configuration.waveHeight) / height; + float waveHeightPerc = configuration.waveHeight / height * 2; + for (int i = 0; i < layers.length; i++) { + int reverseI = layers.length - i - 1; + float fromY = -1 + reverseI * waveHeightPerc * 2; + float toY = fromY + layerHeightPerc * 2; + layers[i] = new GLWaveLayer(configuration, configuration.layerColors[i], fromY, toY, random); + } + } + + @Override public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + ratioY = (float) width / height; + } + + @Override public void onDrawFrame(GL10 gl) { + if (bgUpdated) { + float[] backgroundColor = configuration.backgroundColor; + GLES20.glClearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], + backgroundColor[3]); + bgUpdated = false; + } else { + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + } + long endTime = System.currentTimeMillis(); + long dt = endTime - startTime; + startTime = endTime; + int i = 0; + boolean isCalmedDown = true; + for (GLWaveLayer layer : layers) { + // slow down speed of wave from top to bottom of screen + float speedCoef = (1 - 1f * i / (layers.length) * 0.8f); + layer.update(dt, D_ANGLE * speedCoef, ratioY); + isCalmedDown &= layer.isCalmedDown(); + i++; + } + for (GLWaveLayer layer : layers) { + layer.draw(); + } + if (isCalmedDown && calmDownListener != null) { + calmDownListener.onCalmedDown(); + } + } + + public final void onDataReceived(float[] dBmArray, float[] ampsArray) { + if (layers == null) return; + for (int i = 0; i < layers.length; i++) { + if (layers[i] == null) return; + layers[i].updateData(dBmArray[i], ampsArray[i]); + } + } + + /** + * Utility method for compiling a OpenGL shader. + * + * @param type - Vertex or fragment shader type. + * @param shaderCode - String containing the shader code. + * @return - Returns an id for the shader. + */ + public static int loadShader(int type, String shaderCode) { + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + @Override + public void updateConfiguration(@NonNull GLAudioVisualizationView.ColorsBuilder builder) { + float[] bgColor = configuration.backgroundColor; + float[] backgroundColor = builder.backgroundColor(); + bgUpdated = false; + for (int i = 0; i < 4; i++) { + bgUpdated |= Float.compare(bgColor[i], backgroundColor[i]) != 0; + } + if (bgUpdated) { + configuration.backgroundColor = builder.backgroundColor(); + } + if (layers == null) return; + float[][] colors = builder.layerColors(); + for (int i = 0; i < layers.length; i++) { + layers[i].setColor(colors[i]); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLShape.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLShape.java new file mode 100644 index 0000000..c0f45e2 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLShape.java @@ -0,0 +1,64 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.opengl.GLES20; + +/** + * Abstract shape implementation. + */ +abstract class GLShape { + + protected static final String VERTEX_POSITION = "vPosition"; + protected static final String VERTEX_COLOR = "vColor"; + private static final String VERTEX_SHADER_CODE = "attribute vec4 " + + VERTEX_POSITION + + ";" + + "void main() {" + + " gl_Position = " + + VERTEX_POSITION + + ";" + + "}"; + private static final String FRAGMENT_SHADER_CODE = "precision mediump float;" + + "uniform vec4 " + + VERTEX_COLOR + + ";" + + "void main() {" + + " gl_FragColor = " + + VERTEX_COLOR + + ";" + + "}"; + protected static final int COORDS_PER_VERTEX = 3; + protected static final int SIZE_OF_FLOAT = 4; + protected static final int SIZE_OF_SHORT = 2; + + /** + * Shape color. + */ + private final float color[]; + + /** + * Program associated with shape. + */ + private final int program; + + public GLShape(float[] color) { + this.color = color; + int vertexShader = GLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE); + int fragmentShader = GLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE); + program = GLES20.glCreateProgram(); + GLES20.glAttachShader(program, vertexShader); + GLES20.glAttachShader(program, fragmentShader); + GLES20.glLinkProgram(program); + } + + protected float[] getColor() { + return color; + } + + protected int getProgram() { + return program; + } + + public void setColor(float[] color) { + System.arraycopy(color, 0, this.color, 0, this.color.length); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWave.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWave.java new file mode 100644 index 0000000..656aeb5 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWave.java @@ -0,0 +1,177 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.opengl.GLES20; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.Random; + +/** + * Single wave implementation. + */ +class GLWave extends GLShape { + + /** + * Wave movement from bottom to top. + */ + public static final byte DIRECTION_UP = 0; + /** + * Wave movement from top to bottom. + */ + public static final byte DIRECTION_DOWN = 1; + /** + * Smooth coefficient for {@link Utils#smooth(float, float, float)} method. + */ + private static final float SMOOTH_A = 0.35f; + + /** + * Number of points used for drawing Bezier curve. + */ + private static final int POINTS_PER_WAVE = 40; + + /** + * Number of additional points used for drawing wave: center, lb, lt, rt, rb. + */ + private static final int ADDITIONAL_POINTS = 5; + + /** + * Number of points to skip for getting proper index for Bezier curve points. + */ + private static final int SKIP = (int) Math.ceil(ADDITIONAL_POINTS / 2f) * COORDS_PER_VERTEX; + + private FloatBuffer vertexBuffer; + private ShortBuffer shortBuffer; + private final Random random; + private final float fromX, toX; + private final float fromY, toY; + private float[] vertices; + private float currentAngle; + private float coefficient; + private float latestCoefficient; + private float prevVal; + + public GLWave(float[] color, float fromX, float toX, float fromY, float toY, byte direction, + Random random) { + super(color); + this.fromX = fromX; + this.toX = toX; + this.fromY = fromY; + this.toY = toY; + this.random = random; + currentAngle = direction == DIRECTION_UP ? 0 : (float) Math.PI; + initVertices(); + initIndices(); + } + + private void initIndices() { + short[] indices = new short[(POINTS_PER_WAVE + ADDITIONAL_POINTS - 2) * COORDS_PER_VERTEX]; + for (int i = 0; i < indices.length / COORDS_PER_VERTEX; i++) { + indices[COORDS_PER_VERTEX * i] = 0; + indices[COORDS_PER_VERTEX * i + 1] = (short) (i + 1); + indices[COORDS_PER_VERTEX * i + 2] = (short) (i + 2); + } + ByteBuffer indicesByteBuffer = ByteBuffer.allocateDirect(indices.length * SIZE_OF_SHORT); + indicesByteBuffer.order(ByteOrder.nativeOrder()); + shortBuffer = indicesByteBuffer.asShortBuffer(); + shortBuffer.put(indices); + shortBuffer.position(0); + } + + private void initVertices() { + int items = POINTS_PER_WAVE + ADDITIONAL_POINTS; + int size = items * COORDS_PER_VERTEX; + vertices = new float[size]; + + // center + vertices[0] = Utils.normalizeGl(0f, fromX, toX); + vertices[1] = Utils.normalizeGl(-1f, fromY, toY); + + // left bottom footer + vertices[3] = Utils.normalizeGl(-1f, fromX, toX); + vertices[4] = Utils.normalizeGl(-1f, fromY, toY); + + // left top footer + vertices[6] = vertices[3]; + vertices[7] = Utils.normalizeGl(0f, fromY, toY); + + // right top footer + vertices[vertices.length - 6] = Utils.normalizeGl(1f, fromX, toX); + vertices[vertices.length - 5] = vertices[7]; + + // right bottom footer + vertices[vertices.length - 3] = vertices[vertices.length - 6]; + vertices[vertices.length - 2] = vertices[4]; + } + + float waveX = 0; + + /** + * Update wave position. + * + * @param dAngle delta angle + */ + public void update(float dAngle) { + if (vertexBuffer == null) { + ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * SIZE_OF_FLOAT); + vertexByteBuffer.order(ByteOrder.nativeOrder()); + vertexBuffer = vertexByteBuffer.asFloatBuffer(); + vertexBuffer.put(vertices); + vertexBuffer.position(0); + } + float angle = currentAngle; + angle += dAngle; + currentAngle = angle; + if (coefficient == 0 && latestCoefficient > 0) { + coefficient = Utils.smooth(0, latestCoefficient, SMOOTH_A); + } + + float val = (float) Math.sin(angle) * coefficient; + if (prevVal > 0 && val <= 0 || prevVal < 0 && val >= 0) { + coefficient = Utils.smooth(coefficient, latestCoefficient, SMOOTH_A); + waveX = random.nextFloat() * 0.3f * (random.nextBoolean() ? 1 : -1); + } + prevVal = val; + int i = 0; + double step = 1.0 / POINTS_PER_WAVE; + float posX = Utils.normalizeGl(waveX, fromX, toX); + float posY = Utils.normalizeGl(val, fromY, toY); + for (float time = 0; time < 1 - step / 2; time += step) { + vertices[COORDS_PER_VERTEX * i + 1 + SKIP] = angle; + vertexBuffer.put(COORDS_PER_VERTEX * i + SKIP, + Utils.quad(time, vertices[6], posX, vertices[vertices.length - 6])); + vertexBuffer.put(COORDS_PER_VERTEX * i + 1 + SKIP, + Utils.quad(time, vertices[7], posY, vertices[vertices.length - 5])); + i++; + } + } + + public boolean isCalmedDown() { + return Math.abs(prevVal) < 0.001f; + } + + /** + * Set wave height coefficient. + * + * @param coefficient wave height coefficient + */ + public void setCoefficient(float coefficient) { + this.latestCoefficient = coefficient; + } + + /** + * Draw wave. + */ + public void draw() { + GLES20.glUseProgram(getProgram()); + int positionHandle = GLES20.glGetAttribLocation(getProgram(), VERTEX_POSITION); + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + COORDS_PER_VERTEX * SIZE_OF_FLOAT, vertexBuffer); + int colorHandle = GLES20.glGetUniformLocation(getProgram(), VERTEX_COLOR); + GLES20.glUniform4fv(colorHandle, 1, getColor(), 0); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_FAN, shortBuffer.capacity(), GLES20.GL_UNSIGNED_SHORT, + shortBuffer); + GLES20.glDisableVertexAttribArray(positionHandle); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWaveLayer.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWaveLayer.java new file mode 100644 index 0000000..e081e8b --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/GLWaveLayer.java @@ -0,0 +1,196 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Wave layer implementation. + */ +class GLWaveLayer { + + private final GLAudioVisualizationView.Configuration configuration; + private final GLWave[] waves; + private final GLRectangle rectangle; + private final Random random; + private final float bubbleFromY; + private final float bubbleToY; + private float amplitude; + + private final Set usedBubbles; + private final Queue unusedBubbles; + private final Set producedBubbles; + private boolean isCalmedDown; + private final GLBubble[] allBubbles; + + public GLWaveLayer(GLAudioVisualizationView.Configuration configuration, float[] color, + float fromY, float toY, Random random) { + this.configuration = configuration; + this.random = random; + this.waves = new GLWave[configuration.wavesCount]; + float footerToY = fromY + + configuration.footerHeight / (configuration.footerHeight + configuration.waveHeight * 2) + * (toY - fromY); + this.rectangle = new GLRectangle(color, -1, 1, fromY, footerToY); + float waveWidth = 2f / configuration.wavesCount; + float[] points = randomPoints(this.random, configuration.wavesCount, waveWidth, 0.15f); + this.bubbleFromY = footerToY; + this.bubbleToY = toY; + for (int i = 0; i < configuration.wavesCount; i++) { + byte direction = i % 2 == 0 ? GLWave.DIRECTION_UP : GLWave.DIRECTION_DOWN; + waves[i] = new GLWave(color, points[i], points[i + 1], footerToY, toY, direction, random); + } + this.usedBubbles = Collections.newSetFromMap(new ConcurrentHashMap()); + this.producedBubbles = Collections.newSetFromMap(new ConcurrentHashMap()); + this.unusedBubbles = new LinkedList<>(); + allBubbles = generateBubbles(color, configuration.bubblesPerLayer); + Collections.addAll(unusedBubbles, allBubbles); + } + + /** + * Generate random points for wave. + * + * @param random instance of Random + * @param wavesCount number of waves + * @param width width of single wave + * @param shiftCoef shift coefficient + * @return generated points for waves + */ + private static float[] randomPoints(Random random, int wavesCount, float width, float shiftCoef) { + float shift; + float[] points = new float[wavesCount + 1]; + for (int i = 0; i < points.length; i++) { + if (i == 0) { + points[i] = -1; + } else if (i == points.length - 1) { + points[i] = 1; + } else { + shift = random.nextFloat() * shiftCoef * width; + shift *= random.nextBoolean() ? 1 : -1; + points[i] = -1 + i * width + shift; + } + } + return points; + } + + /** + * Update waves and bubbles positions. + * + * @param dt time elapsed from last calculations + * @param dAngle delta angle + * @param ratioY aspect ratio for Y coordinates + */ + public void update(long dt, float dAngle, float ratioY) { + float d = dt * dAngle; + isCalmedDown = true; + for (GLWave wave : waves) { + wave.update(d); + isCalmedDown &= wave.isCalmedDown(); + } + usedBubbles.addAll(producedBubbles); + producedBubbles.clear(); + Iterator iterator = usedBubbles.iterator(); + while (iterator.hasNext()) { + GLBubble bubble = iterator.next(); + bubble.update(dt, ratioY); + if (bubble.isOffScreen()) { + unusedBubbles.add(bubble); + iterator.remove(); + } + } + } + + public boolean isCalmedDown() { + return isCalmedDown; + } + + /** + * Draw whole wave layer. + */ + public void draw() { + for (GLWave wave : waves) { + wave.draw(); + } + rectangle.draw(); + for (GLBubble bubble : usedBubbles) { + bubble.draw(); + } + } + + /** + * Update waves data. + * + * @param heightCoefficient wave height's coefficient + * @param amplitude amplitude + */ + public void updateData(float heightCoefficient, float amplitude) { + for (GLWave wave : waves) { + wave.setCoefficient(Utils.randomize(heightCoefficient, random)); + } + if (amplitude > this.amplitude) { + this.amplitude = amplitude; + if (heightCoefficient > 0.25f) { + produceBubbles(); + } + } else { + this.amplitude = Utils.smooth(this.amplitude, amplitude, 0.8f); + } + } + + /** + * Produce new bubbles. + */ + private void produceBubbles() { + int bubblesCount = random.nextInt(3); + for (int i = 0; i < bubblesCount; i++) { + GLBubble bubble = unusedBubbles.poll(); + if (bubble != null) { + float shift = random.nextFloat() * 0.1f * (random.nextBoolean() ? 1 : -1); + float size = configuration.bubbleSize; + if (configuration.randomizeBubbleSize) { + size *= 0.5f + random.nextFloat() * 0.8f; + } + bubble.update(-1 + random.nextFloat() * 2, bubbleFromY + shift, bubbleToY, size); + producedBubbles.add(bubble); + } + } + } + + /** + * Generate bubbles. + * + * @param color color of bubbles + * @param count number of bubbles to generate + * @return generated bubbles + */ + private GLBubble[] generateBubbles(float[] color, int count) { + GLBubble[] bubbles = new GLBubble[count]; + for (int i = 0; i < count; i++) { + float size = configuration.bubbleSize; + if (configuration.randomizeBubbleSize) { + size *= 0.5f + random.nextFloat() * 0.8f; + } + float shift = random.nextFloat() * 0.1f * (random.nextBoolean() ? 1 : -1); + float[] col = new float[color.length]; + System.arraycopy(color, 0, col, 0, col.length); + bubbles[i] = + new GLBubble(col, -1 + random.nextFloat() * 2, bubbleFromY + shift, bubbleToY, size, + random); + } + return bubbles; + } + + public void setColor(float[] color) { + rectangle.setColor(color); + for (GLWave wave : waves) { + wave.setColor(color); + } + for (GLBubble bubble : allBubbles) { + bubble.setColor(color); + } + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/InnerAudioVisualization.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/InnerAudioVisualization.java new file mode 100644 index 0000000..69a3aa2 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/InnerAudioVisualization.java @@ -0,0 +1,45 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.support.annotation.Nullable; + +/** + * Inner audio visualization interface. + */ +interface InnerAudioVisualization { + + /** + * Start rendering of data. + */ + void startRendering(); + + /** + * Stop rendering of data. + */ + void stopRendering(); + + /** + * Set calm down listener. + * + * @param calmDownListener calm down listener or null + */ + void calmDownListener(@Nullable CalmDownListener calmDownListener); + + /** + * Called when data received. + * + * @param dBmArray normalized dBm values for every layer + * @param ampsArray amplitude values for every layer + */ + void onDataReceived(float[] dBmArray, float[] ampsArray); + + /** + * Listener that notifies about waves calm down. + */ + interface CalmDownListener { + + /** + * Called when all waves calm down. + */ + void onCalmedDown(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Utils.java b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Utils.java new file mode 100644 index 0000000..9072619 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/audiovisualization/Utils.java @@ -0,0 +1,160 @@ +package in.arjsna.audiorecorder.audiovisualization; + +import android.graphics.Color; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import java.util.Random; + +/** + * Helpful utils. + */ +class Utils { + private Utils() { + } + + /** + * Check if value within allowed range. + * + * @param val some value + * @param min minimum value + * @param max maximum value + */ + public static float between(float val, float min, float max) { + return Math.max(Math.min(val, max), min); + } + + /** + * Check if value within allowed range. + * + * @param val some value + * @param min minimum value + * @param max maximum value + */ + public static int between(int val, int min, int max) { + return Math.max(Math.min(val, max), min); + } + + /** + * Convert color into OpenGL color format. + * + * @param color some color + * @return array of floats: [red, green, blue, alpha] + */ + public static float[] convertColor(@ColorInt int color) { + return new float[] { + Color.red(color) / 255f, Color.green(color) / 255f, Color.blue(color) / 255f, + Color.alpha(color) / 255f + }; + } + + public static float normalize(float val, float from, float to) { + if (val < from) return 0; + if (val > to) return 1; + return val / (to - from); + } + + public static float normalizeGl(float val, float newFromVal, float newToVal) { + return normalizeGl(val, -1, 1, newFromVal, newToVal); + } + + public static float normalizeGl(float val, float fromVal, float toVal, float newFromVal, + float newToVal) { + float perc = (val - fromVal) / (toVal - fromVal); + return newFromVal + perc * (newToVal - newFromVal); + } + + /** + * Convert square of magnitude to decibels + * + * @param squareMag square of magnitude + * @return decibels + */ + public static float magnitudeToDb(float squareMag) { + if (squareMag == 0) return 0; + return (float) (20 * Math.log10(squareMag)); + } + + /** + * Exponential smoothing (Holt - Winters). + * + * @param prevValue previous values in series X[i-1] + * @param newValue new value in series X[i] + * @param a smooth coefficient + * @return smoothed value + */ + public static float smooth(float prevValue, float newValue, float a) { + return a * newValue + (1 - a) * prevValue; + } + + /** + * Quadratic Bezier curve. + * + * @param t time + * @param p0 start point + * @param p1 control point + * @param p2 end point + * @return point on Bezier curve at some time t + */ + public static float quad(float t, float p0, float p1, float p2) { + return (float) (p0 * Math.pow(1 - t, 2) + p1 * 2 * t * (1 - t) + p2 * t * t); + } + + public static float randomize(float value, Random random) { + float perc = between((random.nextInt(100) + 70) / 100, 0.7f, 1.3f); + return perc * value; + } + + /** + * Check if all elements are null + * + * @param array some array + * @return true if all elements are null, false otherwise + */ + public static boolean allElementsAreNull(T[] array) { + for (T element : array) { + if (element != null) return false; + } + return true; + } + + /** + * Get index of object in array. + * + * @param array some array + * @param object some object + * @return index of object in array or -1 + */ + public static int indexOf(@NonNull T[] array, @Nullable T object) { + for (int i = 0; i < array.length; i++) { + if (array[i] == object) return i; + } + return -1; + } + + /** + * Check if all array elements are false + * + * @param array some array + * @return true if all elements are equals to false + */ + public static boolean allElementsAreFalse(@NonNull boolean[] array) { + for (boolean wavesWorkingState : array) { + if (wavesWorkingState) return false; + } + return true; + } + + /** + * Check if all array elements equal to zero + * + * @param array some array + * @return true if all elements equal to zero + */ + public static boolean allElementsAreZero(byte[] array) { + for (byte b : array) { + if (b != 0) return false; + } + return true; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/fragments/PlayListFragment.java b/app/src/main/java/in/arjsna/audiorecorder/fragments/PlayListFragment.java new file mode 100644 index 0000000..6db14c5 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/fragments/PlayListFragment.java @@ -0,0 +1,109 @@ +package in.arjsna.audiorecorder.fragments; + +import android.os.Bundle; +import android.os.FileObserver; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import in.arjsna.audiorecorder.DBHelper; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.adapters.PlayListAdapter; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.ThemedFragment; +import io.reactivex.android.schedulers.AndroidSchedulers; +import java.util.ArrayList; + +public class PlayListFragment extends ThemedFragment { + private static final String LOG_TAG = "PlayListFragment"; + + private PlayListAdapter mPlayListAdapter; + private DBHelper dbHelper; + private RecyclerView mRecordingsListView; + private LinearLayoutManager llm; + private TextView emptyListLabel; + + public static PlayListFragment newInstance() { + return new PlayListFragment(); + } + + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + observer.startWatching(); + dbHelper = new DBHelper(getActivity()); + } + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_file_viewer, container, false); + initViews(v); + return v; + } + + private void initViews(View v) { + emptyListLabel = (TextView) v.findViewById(R.id.empty_list_label); + mRecordingsListView = (RecyclerView) v.findViewById(R.id.recyclerView); + mRecordingsListView.setHasFixedSize(true); + llm = new LinearLayoutManager(getActivity()); + llm.setOrientation(LinearLayoutManager.VERTICAL); + + //newest to oldest order (database stores from oldest to newest) + llm.setReverseLayout(true); + llm.setStackFromEnd(true); + + mRecordingsListView.setLayoutManager(llm); + mRecordingsListView.setItemAnimator(new DefaultItemAnimator()); + mPlayListAdapter = new PlayListAdapter(getActivity(), llm, new ArrayList<>()); + mRecordingsListView.setAdapter(mPlayListAdapter); + fillAdapter(); + } + + private void fillAdapter() { + dbHelper.getAllRecordings() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(recordingItems -> { + if (recordingItems.size() > 0) { + mPlayListAdapter.addAllAndNotify(recordingItems); + } else { + emptyListLabel.setVisibility(View.VISIBLE); + mRecordingsListView.setVisibility(View.GONE); + } + }); + } + + final FileObserver observer = new FileObserver( + android.os.Environment.getExternalStorageDirectory().toString() + "/SoundRecorder") { + // set up a file observer to watch this directory on sd card + @Override public void onEvent(int event, String file) { + if (event == FileObserver.DELETE) { + // user deletes a recording file out of the app + + String filePath = android.os.Environment.getExternalStorageDirectory().toString() + + "/SoundRecorder" + + file + + "]"; + + Log.d(LOG_TAG, "File deleted [" + + android.os.Environment.getExternalStorageDirectory().toString() + + "/SoundRecorder" + + file + + "]"); + + // remove file from database and recyclerview + mPlayListAdapter.removeOutOfApp(filePath); + } + } + }; + + @Override public void refreshTheme(ThemeHelper themeHelper) { + + } +} + + + + diff --git a/app/src/main/java/in/arjsna/voicerecorder/fragments/PlaybackFragment.java b/app/src/main/java/in/arjsna/audiorecorder/fragments/PlaybackFragment.java similarity index 74% rename from app/src/main/java/in/arjsna/voicerecorder/fragments/PlaybackFragment.java rename to app/src/main/java/in/arjsna/audiorecorder/fragments/PlaybackFragment.java index ff437f6..c924277 100644 --- a/app/src/main/java/in/arjsna/voicerecorder/fragments/PlaybackFragment.java +++ b/app/src/main/java/in/arjsna/audiorecorder/fragments/PlaybackFragment.java @@ -1,41 +1,44 @@ -package in.arjsna.voicerecorder.fragments; +package in.arjsna.audiorecorder.fragments; import android.app.AlertDialog; import android.app.Dialog; +import android.content.res.ColorStateList; import android.graphics.ColorFilter; import android.graphics.LightingColorFilter; import android.media.MediaPlayer; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; +import android.support.v7.widget.AppCompatSeekBar; import android.util.Log; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.SeekBar; import android.widget.TextView; -import com.melnykov.fab.FloatingActionButton; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.RecordingItem; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.RecordingItem; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.ThemedDialogFragment; +import in.arjsna.audiorecorder.theme.ThemedFab; import java.io.IOException; import java.util.concurrent.TimeUnit; -public class PlaybackFragment extends DialogFragment { +public class PlaybackFragment extends ThemedDialogFragment { private static final String LOG_TAG = "PlaybackFragment"; private static final String ARG_ITEM = "recording_item"; private RecordingItem item; - private Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(); private MediaPlayer mMediaPlayer = null; - private SeekBar mSeekBar = null; - private FloatingActionButton mPlayButton = null; + private AppCompatSeekBar mSeekBar = null; + private ThemedFab mPlayButton = null; private TextView mCurrentProgressTextView = null; - private TextView mFileNameTextView = null; private TextView mFileLengthTextView = null; //stores whether or not the mediaplayer is currently playing audio @@ -63,10 +66,6 @@ public PlaybackFragment newInstance(RecordingItem item) { seconds = TimeUnit.MILLISECONDS.toSeconds(itemDuration) - TimeUnit.MINUTES.toSeconds(minutes); } - @Override public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - } - @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); @@ -74,17 +73,17 @@ public PlaybackFragment newInstance(RecordingItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_media_playback, null); - mFileNameTextView = (TextView) view.findViewById(R.id.file_name_text_view); + TextView mFileNameTextView = (TextView) view.findViewById(R.id.file_name_text_view); mFileLengthTextView = (TextView) view.findViewById(R.id.file_length_text_view); mCurrentProgressTextView = (TextView) view.findViewById(R.id.current_progress_text_view); - mSeekBar = (SeekBar) view.findViewById(R.id.seekbar); + mSeekBar = (AppCompatSeekBar) view.findViewById(R.id.seekbar); ColorFilter filter = new LightingColorFilter(getResources().getColor(R.color.primary), getResources().getColor(R.color.primary)); mSeekBar.getProgressDrawable().setColorFilter(filter); mSeekBar.getThumb().setColorFilter(filter); - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + mSeekBar.setOnSeekBarChangeListener(new AppCompatSeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mMediaPlayer != null && fromUser) { mMediaPlayer.seekTo(progress); @@ -93,7 +92,8 @@ public PlaybackFragment newInstance(RecordingItem item) { long minutes = TimeUnit.MILLISECONDS.toMinutes(mMediaPlayer.getCurrentPosition()); long seconds = TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getCurrentPosition()) - TimeUnit.MINUTES.toSeconds(minutes); - mCurrentProgressTextView.setText(String.format("%02d:%02d", minutes, seconds)); + mCurrentProgressTextView.setText( + String.format(getString(R.string.play_time_format), minutes, seconds)); updateSeekBar(); } else if (mMediaPlayer == null && fromUser) { @@ -117,22 +117,22 @@ public PlaybackFragment newInstance(RecordingItem item) { long minutes = TimeUnit.MILLISECONDS.toMinutes(mMediaPlayer.getCurrentPosition()); long seconds = TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getCurrentPosition()) - TimeUnit.MINUTES.toSeconds(minutes); - mCurrentProgressTextView.setText(String.format("%02d:%02d", minutes, seconds)); + mCurrentProgressTextView.setText( + String.format(getString(R.string.play_time_format), minutes, seconds)); updateSeekBar(); } } }); - mPlayButton = (FloatingActionButton) view.findViewById(R.id.fab_play); - mPlayButton.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { - onPlay(isPlaying); - isPlaying = !isPlaying; - } + mPlayButton = (ThemedFab) view.findViewById(R.id.fab_play); + mPlayButton.setOnClickListener(v -> { + onPlay(isPlaying); + isPlaying = !isPlaying; }); mFileNameTextView.setText(item.getName()); - mFileLengthTextView.setText(String.format("%02d:%02d", minutes, seconds)); + mFileLengthTextView.setText( + String.format(getString(R.string.play_time_format), minutes, seconds)); builder.setView(view); @@ -196,20 +196,12 @@ private void startPlaying() { mMediaPlayer.prepare(); mSeekBar.setMax(mMediaPlayer.getDuration()); - mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { - @Override public void onPrepared(MediaPlayer mp) { - mMediaPlayer.start(); - } - }); + mMediaPlayer.setOnPreparedListener(mp -> mMediaPlayer.start()); } catch (IOException e) { Log.e(LOG_TAG, "prepare() failed"); } - mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override public void onCompletion(MediaPlayer mp) { - stopPlaying(); - } - }); + mMediaPlayer.setOnCompletionListener(mp -> stopPlaying()); updateSeekBar(); @@ -228,11 +220,7 @@ private void prepareMediaPlayerFromPoint(int progress) { mSeekBar.setMax(mMediaPlayer.getDuration()); mMediaPlayer.seekTo(progress); - mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override public void onCompletion(MediaPlayer mp) { - stopPlaying(); - } - }); + mMediaPlayer.setOnCompletionListener(mp -> stopPlaying()); } catch (IOException e) { Log.e(LOG_TAG, "prepare() failed"); } @@ -273,24 +261,31 @@ private void stopPlaying() { } //updating mSeekBar - private Runnable mRunnable = new Runnable() { - @Override public void run() { - if (mMediaPlayer != null) { + private final Runnable mRunnable = () -> { + if (mMediaPlayer != null) { - int mCurrentPosition = mMediaPlayer.getCurrentPosition(); - mSeekBar.setProgress(mCurrentPosition); + int mCurrentPosition = mMediaPlayer.getCurrentPosition(); + mSeekBar.setProgress(mCurrentPosition); - long minutes = TimeUnit.MILLISECONDS.toMinutes(mCurrentPosition); - long seconds = - TimeUnit.MILLISECONDS.toSeconds(mCurrentPosition) - TimeUnit.MINUTES.toSeconds(minutes); - mCurrentProgressTextView.setText(String.format("%02d:%02d", minutes, seconds)); + long minutes = TimeUnit.MILLISECONDS.toMinutes(mCurrentPosition); + long seconds = + TimeUnit.MILLISECONDS.toSeconds(mCurrentPosition) - TimeUnit.MINUTES.toSeconds(minutes); + mCurrentProgressTextView.setText( + String.format(getString(R.string.play_time_format), minutes, seconds)); - updateSeekBar(); - } + updateSeekBar(); } }; private void updateSeekBar() { mHandler.postDelayed(mRunnable, 1000); } + + @Override public void refreshTheme(ThemeHelper themeHelper) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.mSeekBar.setThumbTintList(ColorStateList.valueOf(themeHelper.getAccentColor())); + this.mSeekBar.setProgressTintList(ColorStateList.valueOf(themeHelper.getAccentColor())); + } + this.mPlayButton.setBackgroundTintList(ColorStateList.valueOf(themeHelper.getAccentColor())); + } } diff --git a/app/src/main/java/in/arjsna/audiorecorder/fragments/RecordFragment.java b/app/src/main/java/in/arjsna/audiorecorder/fragments/RecordFragment.java new file mode 100644 index 0000000..18e0a36 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/fragments/RecordFragment.java @@ -0,0 +1,269 @@ +package in.arjsna.audiorecorder.fragments; + +import android.animation.FloatEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.TextView; +import com.jakewharton.rxbinding2.view.RxView; +import in.arjsna.audiorecorder.AppConstants; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.activities.PlayListActivity; +import in.arjsna.audiorecorder.activities.SettingsActivity; +import in.arjsna.audiorecorder.audiovisualization.GLAudioVisualizationView; +import in.arjsna.audiorecorder.recording.AudioRecordService; +import in.arjsna.audiorecorder.recording.AudioRecorder; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.ThemedFragment; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import java.util.Locale; + +/** + * A simple {@link Fragment} subclass. + * Activities that contain this fragment must implement the + * to handle interaction events. + * Use the {@link RecordFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class RecordFragment extends ThemedFragment { + private static final String LOG_TAG = RecordFragment.class.getSimpleName(); + private FloatingActionButton mRecordButton = null; + private FloatingActionButton mPauseButton = null; + private GLAudioVisualizationView audioVisualization; + + private boolean mIsRecording = false; + private boolean mIsRecordingPaused = true; + + private TextView chronometer; + private boolean mIsServiceBound = false; + private AudioRecordService mAudioRecordService; + private ObjectAnimator alphaAnimator; + private FloatingActionButton mSettingsButton; + private FloatingActionButton mPlayListBtn; + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @return A new instance of fragment Record_Fragment. + */ + public static RecordFragment newInstance() { + return new RecordFragment(); + } + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View recordView = inflater.inflate(R.layout.fragment_record, container, false); + initViews(recordView); + bindEvents(); + bindToService(); + return recordView; + } + + private void bindEvents() { + RxView.clicks(mRecordButton).subscribe(o -> onChangeRecord()); + RxView.clicks(mSettingsButton).subscribe(o -> { + Intent settingsIntent = new Intent(getActivity(), SettingsActivity.class); + startActivity(settingsIntent); + }); + RxView.clicks(mPlayListBtn).subscribe(o -> { + Intent viewPlayListIntent = new Intent(getActivity(), PlayListActivity.class); + startActivity(viewPlayListIntent); + }); + + mPauseButton.setOnClickListener(v -> { + mIsRecordingPaused = !mIsRecordingPaused; + onPauseRecord(); + }); + } + + private void initViews(View recordView) { + chronometer = (TextView) recordView.findViewById(R.id.chronometer); + setChronometer(new AudioRecorder.RecordTime()); + + audioVisualization = (GLAudioVisualizationView) recordView.findViewById(R.id.visualizer_view); + + mSettingsButton = (FloatingActionButton) recordView.findViewById(R.id.settings_btn); + mPlayListBtn = (FloatingActionButton) recordView.findViewById(R.id.play_list_btn); + mRecordButton = (FloatingActionButton) recordView.findViewById(R.id.btnRecord); + mRecordButton.setImageResource( + mIsRecording ? R.drawable.ic_media_stop : R.drawable.ic_media_record); + mPauseButton = (FloatingActionButton) recordView.findViewById(R.id.btnPause); + mPauseButton.setVisibility(View.GONE); //hide pause button before recording starts + + alphaAnimator = + ObjectAnimator.ofObject(chronometer, "alpha", new FloatEvaluator(), 0.2f); + alphaAnimator.setRepeatMode(ValueAnimator.REVERSE); + alphaAnimator.setRepeatCount(ValueAnimator.INFINITE); + } + + private void onChangeRecord() { + Intent intent = new Intent(getActivity(), AudioRecordService.class); + if (!mIsRecording) { + mIsRecording = true; + mRecordButton.setImageResource(R.drawable.ic_media_stop); + getActivity().startService(intent); + bindToService(); + mPauseButton.setVisibility(View.VISIBLE); + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + stopRecording(); + } + } + + private void stopRecording() { + Intent intent = new Intent(getContext(), AudioRecordService.class); + mIsRecording = false; + mIsRecordingPaused = false; + mRecordButton.setImageResource(R.drawable.ic_media_record); + getActivity().stopService(intent); + getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + unbindService(); + setChronometer(new AudioRecorder.RecordTime()); + mPauseButton.setVisibility(View.GONE); + togglePauseBtn(); + } + + private final BroadcastReceiver stopServiceReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + if (!intent.hasExtra(AppConstants.ACTION_IN_SERVICE)) return; + String actionExtra = intent.getStringExtra(AppConstants.ACTION_IN_SERVICE); + switch (actionExtra) { + case AppConstants.ACTION_PAUSE: + mIsRecordingPaused = true; + togglePauseBtn(); + break; + case AppConstants.ACTION_RESUME: + mIsRecordingPaused = false; + togglePauseBtn(); + break; + case AppConstants.ACTION_STOP: + stopRecording(); + break; + } + } + }; + + private void unRegisterLocalBroadCastReceiver() { + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(stopServiceReceiver); + } + + private void registerLocalBroadCastReceiver() { + LocalBroadcastManager.getInstance(getActivity()) + .registerReceiver(stopServiceReceiver, new IntentFilter(AppConstants.ACTION_IN_SERVICE)); + } + + private void bindToService() { + Intent intent = new Intent(getActivity(), AudioRecordService.class); + getActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + registerLocalBroadCastReceiver(); + } + + private Disposable timerDisposable; + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + mAudioRecordService = + ((AudioRecordService.ServiceBinder) iBinder).getService(); + Log.i("Tesing", " " + mAudioRecordService.isRecording() + " recording"); + mIsRecording = mAudioRecordService.isRecording(); + mIsServiceBound = true; + if (mIsRecording) { + mIsRecordingPaused = mAudioRecordService.isPaused(); + onPauseRecord(); + audioVisualization.linkTo(mAudioRecordService.getHandler()); + mRecordButton.setImageResource(R.drawable.ic_media_stop); + timerDisposable = mAudioRecordService.subscribeForTimer(recordTimeConsumer); + } else { + unbindService(); + } + } + + @Override public void onServiceDisconnected(ComponentName componentName) { + } + }; + + private final Consumer recordTimeConsumer = this::setChronometer; + + private void setChronometer(AudioRecorder.RecordTime recordTime) { + chronometer.setText( + String.format(Locale.getDefault(), getString(R.string.record_time_format), recordTime.hours, + recordTime.minutes, + recordTime.seconds)); + } + + private void onPauseRecord() { + mPauseButton.setVisibility(View.VISIBLE); + if (mIsRecordingPaused) { + mAudioRecordService.pauseRecord(); + setAsResumeBtn(); + } else { + mAudioRecordService.resumeRecord(); + setAsPauseBtn(); + } + } + + private void setAsPauseBtn() { + alphaAnimator.cancel(); + chronometer.setAlpha(1.0f); + mPauseButton.setImageResource(R.drawable.ic_media_pause); + } + + private void setAsResumeBtn() { + alphaAnimator.start(); + mPauseButton.setImageResource(R.drawable.ic_media_record); + } + + private void togglePauseBtn() { + if (mIsRecordingPaused) { + setAsResumeBtn(); + } else { + setAsPauseBtn(); + } + } + + @Override public void onDestroy() { + super.onDestroy(); + audioVisualization.release(); + unbindService(); + } + + private void unbindService() { + unRegisterLocalBroadCastReceiver(); + if (timerDisposable != null) { + timerDisposable.dispose(); + } + if (mIsServiceBound) { + mIsServiceBound = false; + getActivity().unbindService(serviceConnection); + } + } + + @Override public void refreshTheme(ThemeHelper themeHelper) { + GLAudioVisualizationView.ColorsBuilder colorsBuilder = + new GLAudioVisualizationView.Builder(getActivity()); + colorsBuilder.setBackgroundColor(themeHelper.getPrimaryColor()); + colorsBuilder.setLayerColors(themeHelper.getLayerColor()); + audioVisualization.updateConfig(colorsBuilder); + chronometer.setTextColor(themeHelper.getLayerColor()[3]); + mRecordButton.setRippleColor(themeHelper.getLayerColor()[3]); + mSettingsButton.setRippleColor(themeHelper.getLayerColor()[3]); + mPlayListBtn.setRippleColor(themeHelper.getLayerColor()[3]); + mPauseButton.setRippleColor(themeHelper.getLayerColor()[3]); + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/fragments/SettingsFragment.java b/app/src/main/java/in/arjsna/audiorecorder/fragments/SettingsFragment.java new file mode 100644 index 0000000..1946831 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/fragments/SettingsFragment.java @@ -0,0 +1,101 @@ +package in.arjsna.audiorecorder.fragments; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.SettingBasic; +import in.arjsna.audiorecorder.theme.ColorsSetting; +import in.arjsna.audiorecorder.theme.ThemeHelper; +import in.arjsna.audiorecorder.theme.ThemedActivity; +import in.arjsna.audiorecorder.theme.ThemedFragment; + +public class SettingsFragment extends ThemedFragment { + + private View rootView; + private SettingBasic themeSetting; + private ThemedActivity parent; + private SettingBasic rateApp; + + @Nullable @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + rootView = inflater.inflate(R.layout.fragment_settings, container, false); + parent = (ThemedActivity) getActivity(); + initViews(); + bindEvents(); + return rootView; + } + + private void bindEvents() { + themeSetting.setOnClickListener(v -> { + final int originalColor = getPrimaryColor(); + new ColorsSetting((ThemedActivity) getActivity()).chooseColor(R.string.primary_color, + new ColorsSetting.ColorChooser() { + @Override + public void onColorSelected(ColorsSetting.SelectedColor color) { + Hawk.put(getString(R.string.preference_primary_color), color.colorPrimary); + Hawk.put(getString(R.string.preference_accent_color), color.colorPrimary); + Hawk.put(getString(R.string.preference_layer_colors), color.shades); + parent.updateTheme(); + parent.updateUiElements(); + } + + @Override + public void onDialogDismiss() { + Hawk.put(getString(R.string.preference_primary_color), originalColor); + Hawk.put(getString(R.string.preference_accent_color), originalColor); + parent.updateTheme(); + parent.updateUiElements(); + } + + @Override + public void onColorChanged(int color) { + Hawk.put(getString(R.string.preference_primary_color), color); + Hawk.put(getString(R.string.preference_accent_color), color); + parent.updateTheme(); + parent.updateUiElements(); + } + }, getPrimaryColor()); + }); + rateApp.setOnClickListener(v -> { + Uri uri = Uri.parse("market://details?id=" + getActivity().getPackageName()); + Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri); + goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | + Intent.FLAG_ACTIVITY_NEW_DOCUMENT | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + try { + startActivity(goToMarket); + } catch (ActivityNotFoundException e) { + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse( + "http://play.google.com/store/apps/details?id=" + getActivity().getPackageName()))); + } + }); + } + + private void initViews() { + themeSetting = (SettingBasic) rootView.findViewById(R.id.theme_settings); + rateApp = (SettingBasic) rootView.findViewById(R.id.rate_app); + } + + //Preference aboutPref = findPreference(getString(R.string.pref_about_key)); + // aboutPref.setSummary(getString(R.string.pref_about_desc, BuildConfig.VERSION_NAME)); + // aboutPref.setOnPreferenceClickListener(preference -> { + // LicensesFragment licensesFragment = new LicensesFragment(); + // licensesFragment.show( + // ((SettingsActivity) getActivity()).getSupportFragmentManager(), + // "dialog_licenses"); + // return true; + //}); + + @Override public void refreshTheme(ThemeHelper themeHelper) { + + } +} diff --git a/app/src/main/java/in/arjsna/voicerecorder/listeners/OnDatabaseChangedListener.java b/app/src/main/java/in/arjsna/audiorecorder/listeners/OnDatabaseChangedListener.java similarity index 72% rename from app/src/main/java/in/arjsna/voicerecorder/listeners/OnDatabaseChangedListener.java rename to app/src/main/java/in/arjsna/audiorecorder/listeners/OnDatabaseChangedListener.java index 365ef08..2627c94 100644 --- a/app/src/main/java/in/arjsna/voicerecorder/listeners/OnDatabaseChangedListener.java +++ b/app/src/main/java/in/arjsna/audiorecorder/listeners/OnDatabaseChangedListener.java @@ -1,4 +1,4 @@ -package in.arjsna.voicerecorder.listeners; +package in.arjsna.audiorecorder.listeners; public interface OnDatabaseChangedListener { void onNewDatabaseEntryAdded(); diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordService.java b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordService.java new file mode 100644 index 0000000..4af8a0b --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordService.java @@ -0,0 +1,172 @@ +package in.arjsna.audiorecorder.recording; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.AppConstants; +import in.arjsna.audiorecorder.R; +import in.arjsna.audiorecorder.activities.MainActivity; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import java.util.Locale; + +public class AudioRecordService extends Service { + private static final String LOG_TAG = "RecordingService"; + + private long mElapsedMillis = 0; + + private AudioRecorder audioRecorder; + private AudioRecordingDbmHandler handler; + private ServiceBinder mIBinder; + private NotificationManager mNotificationManager; + private static final int NOTIFY_ID = 100; + private AudioRecorder.RecordTime lastUpdated; + private boolean mIsClientBound = false; + + @Override public IBinder onBind(Intent intent) { + mIsClientBound = true; + return mIBinder; + } + + public boolean isRecording() { + return audioRecorder.isRecording(); + } + + @Override public void onCreate() { + super.onCreate(); + mIBinder = new ServiceBinder(); + audioRecorder = new AudioRecorder(getApplicationContext()); + handler = new AudioRecordingDbmHandler(); + handler.addRecorder(audioRecorder); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + } + + public AudioRecordingDbmHandler getHandler() { + return handler; + } + + @Override public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getAction() != null) { + switch (intent.getAction()) { + case AppConstants.ACTION_PAUSE: + pauseRecord(); + break; + case AppConstants.ACTION_RESUME: + resumeRecord(); + break; + case AppConstants.ACTION_STOP: + if (!mIsClientBound) { + stopSelf(); + } + } + if (mIsClientBound) { + intent.putExtra(AppConstants.ACTION_IN_SERVICE, intent.getAction()); + intent.setAction(AppConstants.ACTION_IN_SERVICE); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + } else { + startRecording(); + startForeground(NOTIFY_ID, createNotification(new AudioRecorder.RecordTime())); + } + return START_STICKY; + } + + @Override public void onDestroy() { + super.onDestroy(); + if (isRecording()) { + stopRecodingAndRelease(); + } + } + + private void stopRecodingAndRelease() { + audioRecorder.finishRecord(); + handler.stop(); + } + + public void startRecording() { + boolean prefHighQuality = + Hawk.get(getApplicationContext().getString(R.string.pref_high_quality_key), false); + audioRecorder.startRecord( + prefHighQuality ? Constants.RECORDER_SAMPLE_RATE_HIGH : Constants.RECORDER_SAMPLE_RATE_LOW); + handler.startDbmThread(); + audioRecorder.subscribeTimer(this::updateNotification); + } + + public Disposable subscribeForTimer(Consumer timerConsumer) { + return audioRecorder.subscribeTimer(timerConsumer); + } + + private void updateNotification(AudioRecorder.RecordTime recordTime) { + mElapsedMillis = recordTime.millis; + mNotificationManager.notify(NOTIFY_ID, createNotification(recordTime)); + } + + private Notification createNotification(AudioRecorder.RecordTime recordTime) { + lastUpdated = recordTime; + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getApplicationContext()).setSmallIcon( + R.drawable.ic_launcher_background) + .setContentTitle(getString(R.string.notification_recording)) + .setContentText( + String.format(Locale.getDefault(), getString(R.string.record_time_format), + recordTime.hours, + recordTime.minutes, + recordTime.seconds)) + .addAction(R.drawable.ic_media_stop, getString(R.string.stop_recording), + getActionIntent(AppConstants.ACTION_STOP)) + .setOngoing(true); + if (audioRecorder.isPaused()) { + mBuilder.addAction(R.drawable.ic_media_record, getString(R.string.resume_recording_button), + getActionIntent(AppConstants.ACTION_RESUME)); + } else { + mBuilder.addAction(R.drawable.ic_media_pause, getString(R.string.pause_recording_button), + getActionIntent(AppConstants.ACTION_PAUSE)); + } + mBuilder.setContentIntent(PendingIntent.getActivities(getApplicationContext(), 0, + new Intent[] {new Intent(getApplicationContext(), MainActivity.class)}, 0)); + + return mBuilder.build(); + } + + public void pauseRecord() { + audioRecorder.pauseRecord(); + updateNotification(lastUpdated); + } + + public boolean isPaused() { + return audioRecorder.isPaused(); + } + + public void resumeRecord() { + audioRecorder.resumeRecord(); + } + + public class ServiceBinder extends Binder { + public AudioRecordService getService() { + return AudioRecordService.this; + } + } + + private PendingIntent getActionIntent(String action) { + Intent pauseIntent = new Intent(this, AudioRecordService.class); + pauseIntent.setAction(action); + return PendingIntent.getService(this, 100, pauseIntent, 0); + } + + @Override public boolean onUnbind(Intent intent) { + mIsClientBound = false; + return true; + } + + @Override public void onRebind(Intent intent) { + mIsClientBound = true; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecorder.java b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecorder.java new file mode 100644 index 0000000..c98e1f6 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecorder.java @@ -0,0 +1,202 @@ +package in.arjsna.audiorecorder.recording; + +import android.content.Context; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.util.Log; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.processors.BehaviorProcessor; +import io.reactivex.processors.PublishProcessor; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.DisposableSubscriber; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class AudioRecorder implements IAudioRecorder { + + private static final int RECORDER_STATE_FAILURE = -1; + private static final int RECORDER_STATE_IDLE = 0; + private static final int RECORDER_STATE_STARTING = 1; + private static final int RECORDER_STATE_STOPPING = 2; + private static final int RECORDER_STATE_BUSY = 3; + + private volatile int recorderState; + + private int mRecorderSampleRate = 8000; + + private AudioSaveHelper audioSaveHelper; + + private final Object recorderStateMonitor = new Object(); + + private byte[] recordBuffer; + + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + + private final AtomicLong mRecordTimeCounter = new AtomicLong(0); + private final AtomicBoolean mIsPaused = new AtomicBoolean(false); + private RecordTime currentRecordTime; + + public AudioRecorder(Context applicationContext) { + this.audioSaveHelper = new AudioSaveHelper(applicationContext); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") private void onRecordFailure() { + recorderState = RECORDER_STATE_FAILURE; + finishRecord(); + } + + @Override public void startRecord(int recorderSampleRate) { + if (recorderState != RECORDER_STATE_IDLE) { + return; + } + this.mRecorderSampleRate = recorderSampleRate; + audioSaveHelper.setSampleRate(mRecorderSampleRate); + startTimer(); + recorderState = RECORDER_STATE_STARTING; + startRecordThread(); + } + + private final BehaviorProcessor recordTimeProcessor = BehaviorProcessor.create(); + + private void startTimer() { + getTimerObservable().subscribeOn(Schedulers.newThread()).subscribe(recordTimeProcessor); + } + + private Flowable getTimerObservable() { + return Flowable.interval(1000, TimeUnit.MILLISECONDS) + .filter(timeElapsed -> !mIsPaused.get()).map(tick -> { + long seconds = mRecordTimeCounter.incrementAndGet(); + RecordTime recordTime = new RecordTime(); + recordTime.millis = seconds * 1000; + recordTime.hours = seconds / (60 * 60); + seconds = seconds % (60 * 60); + recordTime.minutes = seconds / 60; + seconds = seconds % 60; + recordTime.seconds = seconds; + currentRecordTime = recordTime; + return recordTime; + }); + } + + private final Flowable audioDataFlowable = Flowable.create(emitter -> { + int bufferSize = 4 * 1024; + + AudioRecord recorder = + new AudioRecord(MediaRecorder.AudioSource.MIC, mRecorderSampleRate, + Constants.RECORDER_CHANNELS, Constants.RECORDER_AUDIO_ENCODING, bufferSize); + audioSaveHelper.createNewFile(); + + try { + if (recorderState == RECORDER_STATE_STARTING) { + recorderState = RECORDER_STATE_BUSY; + } + recorder.startRecording(); + + recordBuffer = new byte[bufferSize]; + do { + if (!mIsPaused.get()) { + int bytesRead = recorder.read(recordBuffer, 0, bufferSize); + emitter.onNext(recordBuffer); + if (bytesRead == 0) { + Log.e(AudioRecorder.class.getSimpleName(), "error: " + bytesRead); + onRecordFailure(); + } + } + } while (recorderState == RECORDER_STATE_BUSY); + } finally { + recorder.release(); + } + emitter.onComplete(); + }, BackpressureStrategy.DROP); + + private final PublishProcessor recordDataPublishProcessor = PublishProcessor.create(); + + private void startRecordThread() { + audioDataFlowable.subscribeOn(Schedulers.io()).subscribe(recordDataPublishProcessor); + compositeDisposable.add(recordDataPublishProcessor.onBackpressureBuffer() + .observeOn(Schedulers.io()) + .subscribeWith(new DisposableSubscriber() { + @Override public void onNext(byte[] bytes) { + audioSaveHelper.onDataReady(recordBuffer); + } + + @Override public void onError(Throwable t) { + + } + + @Override public void onComplete() { + synchronized (recorderStateMonitor) { + recorderState = RECORDER_STATE_IDLE; + recorderStateMonitor.notifyAll(); + } + } + })); + } + + @Override public void finishRecord() { + int recorderStateLocal = recorderState; + if (recorderStateLocal != RECORDER_STATE_IDLE) { + synchronized (recorderStateMonitor) { + recorderStateLocal = recorderState; + if (recorderStateLocal == RECORDER_STATE_STARTING + || recorderStateLocal == RECORDER_STATE_BUSY) { + + recorderStateLocal = recorderState = RECORDER_STATE_STOPPING; + } + + do { + try { + if (recorderStateLocal != RECORDER_STATE_IDLE) { + recorderStateMonitor.wait(); + } + } catch (InterruptedException ignore) { + /* Nothing to do */ + } + recorderStateLocal = recorderState; + } while (recorderStateLocal == RECORDER_STATE_STOPPING); + } + } + audioSaveHelper.onRecordingStopped(currentRecordTime); + compositeDisposable.dispose(); + } + + @Override public void pauseRecord() { + mIsPaused.set(true); + } + + @Override public void resumeRecord() { + mIsPaused.set(false); + } + + @Override public boolean isRecording() { + return recorderState != RECORDER_STATE_IDLE; + } + + public Flowable getAudioDataFlowable() { + return recordDataPublishProcessor; + } + + public Disposable subscribeTimer(Consumer timerConsumer) { + Disposable disposable = recordTimeProcessor.observeOn(AndroidSchedulers.mainThread()) + .subscribe(timerConsumer); + compositeDisposable.add(disposable); + return disposable; + } + + boolean isPaused() { + return mIsPaused.get(); + } + + public static class RecordTime { + public long seconds = 0; + public long minutes = 0; + public long hours = 0; + long millis = 0; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordingDbmHandler.java b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordingDbmHandler.java new file mode 100644 index 0000000..596ef41 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioRecordingDbmHandler.java @@ -0,0 +1,94 @@ +package in.arjsna.audiorecorder.recording; + +import android.util.Log; +import in.arjsna.audiorecorder.audiovisualization.DbmHandler; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.DisposableSubscriber; + +public class AudioRecordingDbmHandler extends DbmHandler { + + private static final float MAX_DB_VALUE = 170; + + private float[] dbs; + private float[] allAmps; + private Disposable disposableSubscriber; + + @Override protected void onDataReceivedImpl(byte[] bytes, int layersCount, float[] dBmArray, + float[] ampsArray) { + if (layersCount < 1) return; + final int bytesPerSample = 2; // As it is 16bit PCM + final double amplification = 100.0; // choose a number as you like + Complex[] fft = new Complex[bytes.length / bytesPerSample]; + for (int index = 0, floatIndex = 0; index < bytes.length - bytesPerSample + 1; + index += bytesPerSample, floatIndex++) { + double sample = 0; + for (int b = 0; b < bytesPerSample; b++) { + int v = bytes[index + b]; + if (b < bytesPerSample - 1) { + v &= 0xFF; + } + sample += v << (b * 8); + } + double sample32 = amplification * (sample / 32768.0); + fft[floatIndex] = new Complex(sample32, 0); + } + fft = FFT.fft(fft); + // calculate dBs and amplitudes + int dataSize = fft.length / 2 - 1; + if (dbs == null || dbs.length != dataSize) { + dbs = new float[dataSize]; + } + if (allAmps == null || allAmps.length != dataSize) { + allAmps = new float[dataSize]; + } + + for (int i = 0; i < dataSize; i++) { + dbs[i] = (float) fft[i].abs(); + float k = 1; + if (i == 0 || i == dataSize - 1) { + k = 2; + } + float re = (float) fft[2 * i].re(); + float im = (float) fft[2 * i + 1].im(); + float sqMag = re * re + im * im; + allAmps[i] = (float) (k * Math.sqrt(sqMag) / dataSize); + } + int size = dbs.length / layersCount; + for (int i = 0; i < layersCount; i++) { + int index = (int) ((i + 0.5f) * size); + float db = dbs[index]; + float amp = allAmps[index]; + dBmArray[i] = db > MAX_DB_VALUE ? 1 : db / MAX_DB_VALUE; + ampsArray[i] = amp; + } + } + + @Override public void startDbmThread() { + disposableSubscriber = audioRecorder.getAudioDataFlowable() + .onBackpressureLatest() + .observeOn(Schedulers.newThread()) + .subscribeWith(new DisposableSubscriber() { + @Override public void onNext(byte[] moreData) { + if (moreData != null && moreData.length > 0) { + onDataReceived(moreData); + } + } + + @Override public void onError(Throwable t) { + Log.i("Visual Error ", t.getMessage() + " "); + } + + @Override public void onComplete() { + Log.i("Visualise ", "complete"); + } + }); + } + + public void stop() { + if (disposableSubscriber != null) { + disposableSubscriber.dispose(); + } + calmDownAndStopRendering(); + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/AudioSaveHelper.java b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioSaveHelper.java new file mode 100644 index 0000000..6f4c4ee --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/AudioSaveHelper.java @@ -0,0 +1,208 @@ +package in.arjsna.audiorecorder.recording; + +import android.content.Context; +import android.media.AudioFormat; +import android.os.Environment; +import android.util.Log; +import in.arjsna.audiorecorder.DBHelper; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +class AudioSaveHelper { + + private final DBHelper mDbHelper; + private FileOutputStream os; + private File mFile; + private int mRecordSampleRate; + + public AudioSaveHelper(Context applicationContext) { + mDbHelper = new DBHelper(applicationContext); + } + + public void createNewFile() { + Log.i("TEsting", "creating file"); + String storeLocation = Environment.getExternalStorageDirectory().getAbsolutePath(); + File folder = new File(storeLocation + "/SoundRecorder"); + if (!folder.exists()) { + folder.mkdir(); + } + int count = 0; + String fileName; + do { + count++; + fileName = + "AudioRecord_" + (mDbHelper.getCount() + count) + Constants.AUDIO_RECORDER_FILE_EXT_WAV; + String mFilePath = storeLocation + "/SoundRecorder/" + fileName; + mFile = new File(mFilePath); + } while (mFile.exists() && !mFile.isDirectory()); + + try { + os = new FileOutputStream(mFile); + writeWavHeader(os, Constants.RECORDER_CHANNELS, mRecordSampleRate, + Constants.RECORDER_AUDIO_ENCODING); + } catch (IOException e) { + // TODO: 4/9/17 handle this + e.printStackTrace(); + } + } + + public void onDataReady(byte[] data) { + try { + os.write(data, 0, data.length); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void onRecordingStopped( + AudioRecorder.RecordTime currentRecordTime) { + try { + os.close(); + updateWavHeader(mFile); + saveFileDetails(currentRecordTime); + Log.i("Record Complete ", "Saving and closing"); + } catch (IOException e) { + mFile.deleteOnExit(); + e.printStackTrace(); + } + } + + private void saveFileDetails( + AudioRecorder.RecordTime currentRecordTime) { + mDbHelper.addRecording(mFile.getName(), mFile.getPath(), currentRecordTime.millis); + } + + /** + * Writes the proper 44-byte RIFF/WAVE header to/for the given stream + * Two size fields are left empty/null since we do not yet know the final stream size + * + * @param out The stream to write the header to + * @param channelMask An AudioFormat.CHANNEL_* mask + * @param sampleRate The sample rate in hertz + * @param encoding An AudioFormat.ENCODING_PCM_* value + * @throws IOException + */ + private void writeWavHeader(OutputStream out, int channelMask, int sampleRate, int encoding) + throws IOException { + short channels; + switch (channelMask) { + case AudioFormat.CHANNEL_IN_MONO: + channels = 1; + break; + case AudioFormat.CHANNEL_IN_STEREO: + channels = 2; + break; + default: + throw new IllegalArgumentException("Unacceptable channel mask"); + } + + short bitDepth; + switch (encoding) { + case AudioFormat.ENCODING_PCM_8BIT: + bitDepth = 8; + break; + case AudioFormat.ENCODING_PCM_16BIT: + bitDepth = 16; + break; + case AudioFormat.ENCODING_PCM_FLOAT: + bitDepth = 32; + break; + default: + throw new IllegalArgumentException("Unacceptable encoding"); + } + + writeWavHeader(out, channels, sampleRate, bitDepth); + } + + /** + * Writes the proper 44-byte RIFF/WAVE header to/for the given stream + * Two size fields are left empty/null since we do not yet know the final stream size + * + * @param out The stream to write the header to + * @param channels The number of channels + * @param sampleRate The sample rate in hertz + * @param bitDepth The bit depth + * @throws IOException + */ + private void writeWavHeader(OutputStream out, short channels, int sampleRate, short bitDepth) + throws IOException { + // Convert the multi-byte integers to raw bytes in little endian format as required by the spec + byte[] littleBytes = ByteBuffer.allocate(14) + .order(ByteOrder.LITTLE_ENDIAN) + .putShort(channels) + .putInt(sampleRate) + .putInt(sampleRate * channels * (bitDepth / 8)) + .putShort((short) (channels * (bitDepth / 8))) + .putShort(bitDepth) + .array(); + + // Not necessarily the best, but it's very easy to visualize this way + out.write(new byte[] { + // RIFF header + 'R', 'I', 'F', 'F', // ChunkID + 0, 0, 0, 0, // ChunkSize (must be updated later) + 'W', 'A', 'V', 'E', // Format + // fmt subchunk + 'f', 'm', 't', ' ', // Subchunk1ID + 16, 0, 0, 0, // Subchunk1Size + 1, 0, // AudioFormat + littleBytes[0], littleBytes[1], // NumChannels + littleBytes[2], littleBytes[3], littleBytes[4], littleBytes[5], // SampleRate + littleBytes[6], littleBytes[7], littleBytes[8], littleBytes[9], // ByteRate + littleBytes[10], littleBytes[11], // BlockAlign + littleBytes[12], littleBytes[13], // BitsPerSample + // data subchunk + 'd', 'a', 't', 'a', // Subchunk2ID + 0, 0, 0, 0, // Subchunk2Size (must be updated later) + }); + } + + /** + * Updates the given wav file's header to include the final chunk sizes + * + * @param wav The wav file to update + * @throws IOException + */ + private void updateWavHeader(File wav) throws IOException { + byte[] sizes = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN) + // There are probably a bunch of different/better ways to calculate + // these two given your circumstances. Cast should be safe since if the WAV is + // > 4 GB we've already made a terrible mistake. + .putInt((int) (wav.length() - 8)) // ChunkSize + .putInt((int) (wav.length() - 44)) // Subchunk2Size + .array(); + + RandomAccessFile accessWave = null; + //noinspection CaughtExceptionImmediatelyRethrown + try { + accessWave = new RandomAccessFile(wav, "rw"); + // ChunkSize + accessWave.seek(4); + accessWave.write(sizes, 0, 4); + + // Subchunk2Size + accessWave.seek(40); + accessWave.write(sizes, 4, 4); + } catch (IOException ex) { + // Rethrow but we still close accessWave in our finally + throw ex; + } finally { + if (accessWave != null) { + try { + accessWave.close(); + } catch (IOException ex) { + // + } + } + } + } + + public void setSampleRate(int sampleRate) { + this.mRecordSampleRate = sampleRate; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/Complex.java b/app/src/main/java/in/arjsna/audiorecorder/recording/Complex.java new file mode 100644 index 0000000..4d46d85 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/Complex.java @@ -0,0 +1,161 @@ +package in.arjsna.audiorecorder.recording; + +/****************************************************************************** + * Compilation: javac Complex.java + * Execution: java Complex + * + * Data type for complex numbers. + * + * The data type is "immutable" so once you create and initialize + * a Complex object, you cannot change it. The "final" keyword + * when declaring re and im enforces this rule, making it a + * compile-time error to change the .re or .im fields after + * they've been initialized. + * + * % java Complex + * a = 5.0 + 6.0i + * b = -3.0 + 4.0i + * Re(a) = 5.0 + * Im(a) = 6.0 + * b + a = 2.0 + 10.0i + * a - b = 8.0 + 2.0i + * a * b = -39.0 + 2.0i + * b * a = -39.0 + 2.0i + * a / b = 0.36 - 1.52i + * (a / b) * b = 5.0 + 6.0i + * conj(a) = 5.0 - 6.0i + * |a| = 7.810249675906654 + * tan(a) = -6.685231390246571E-6 + 1.0000103108981198i + * + ******************************************************************************/ + +class Complex { + private final double re; // the real part + private final double im; // the imaginary part + + // create a new object with the given real and imaginary parts + public Complex(double real, double imag) { + re = real; + im = imag; + } + + // return a string representation of the invoking Complex object + public String toString() { + if (im == 0) return re + ""; + if (re == 0) return im + "i"; + if (im < 0) return re + " - " + (-im) + "i"; + return re + " + " + im + "i"; + } + + // return abs/modulus/magnitude and angle/phase/argument + public double abs() { + return Math.hypot(re, im); + } // Math.sqrt(re*re + im*im) + + public double phase() { + return Math.atan2(im, re); + } // between -pi and pi + + // return a new Complex object whose value is (this + b) + public Complex plus(Complex b) { + Complex a = this; // invoking object + double real = a.re + b.re; + double imag = a.im + b.im; + return new Complex(real, imag); + } + + // return a new Complex object whose value is (this - b) + public Complex minus(Complex b) { + Complex a = this; + double real = a.re - b.re; + double imag = a.im - b.im; + return new Complex(real, imag); + } + + // return a new Complex object whose value is (this * b) + public Complex times(Complex b) { + Complex a = this; + double real = a.re * b.re - a.im * b.im; + double imag = a.re * b.im + a.im * b.re; + return new Complex(real, imag); + } + + // scalar multiplication + // return a new object whose value is (this * alpha) + public Complex times(double alpha) { + return new Complex(alpha * re, alpha * im); + } + + // return a new Complex object whose value is the conjugate of this + public Complex conjugate() { + return new Complex(re, -im); + } + + // return a new Complex object whose value is the reciprocal of this + public Complex reciprocal() { + double scale = re * re + im * im; + return new Complex(re / scale, -im / scale); + } + + // return the real or imaginary part + public double re() { + return re; + } + + public double im() { + return im; + } + + // return a / b + public Complex divides(Complex b) { + Complex a = this; + return a.times(b.reciprocal()); + } + + // return a new Complex object whose value is the complex exponential of this + public Complex exp() { + return new Complex(Math.exp(re) * Math.cos(im), Math.exp(re) * Math.sin(im)); + } + + // return a new Complex object whose value is the complex sine of this + public Complex sin() { + return new Complex(Math.sin(re) * Math.cosh(im), Math.cos(re) * Math.sinh(im)); + } + + // return a new Complex object whose value is the complex cosine of this + public Complex cos() { + return new Complex(Math.cos(re) * Math.cosh(im), -Math.sin(re) * Math.sinh(im)); + } + + // return a new Complex object whose value is the complex tangent of this + public Complex tan() { + return sin().divides(cos()); + } + + // a static version of plus + public static Complex plus(Complex a, Complex b) { + double real = a.re + b.re; + double imag = a.im + b.im; + return new Complex(real, imag); + } + + // sample client for testing + public static void main(String[] args) { + Complex a = new Complex(5.0, 6.0); + Complex b = new Complex(-3.0, 4.0); + + System.out.println("a = " + a); + System.out.println("b = " + b); + System.out.println("Re(a) = " + a.re()); + System.out.println("Im(a) = " + a.im()); + System.out.println("b + a = " + b.plus(a)); + System.out.println("a - b = " + a.minus(b)); + System.out.println("a * b = " + a.times(b)); + System.out.println("b * a = " + b.times(a)); + System.out.println("a / b = " + a.divides(b)); + System.out.println("(a / b) * b = " + a.divides(b).times(b)); + System.out.println("conj(a) = " + a.conjugate()); + System.out.println("|a| = " + a.abs()); + System.out.println("tan(a) = " + a.tan()); + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/Constants.java b/app/src/main/java/in/arjsna/audiorecorder/recording/Constants.java new file mode 100644 index 0000000..1c1259b --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/Constants.java @@ -0,0 +1,14 @@ +package in.arjsna.audiorecorder.recording; + +import android.media.AudioFormat; + +public class Constants { + public static final int RECORDER_BPP = 16; + public static final String AUDIO_RECORDER_FILE_EXT_WAV = ".wav"; + public static final int RECORDER_SAMPLE_RATE_LOW = 8000; + public static final int RECORDER_SAMPLE_RATE_HIGH = 44100; + public static final int BUFFER_BYTES_ELEMENTS = 2048 * 2; + public static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_MONO; + public static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT; + public static final int BUFFER_BYTES_PER_ELEMENT = RECORDER_AUDIO_ENCODING; +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/FFT.java b/app/src/main/java/in/arjsna/audiorecorder/recording/FFT.java new file mode 100644 index 0000000..f594652 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/FFT.java @@ -0,0 +1,210 @@ +package in.arjsna.audiorecorder.recording; + +/****************************************************************************** + * Compilation: javac FFT.java + * Execution: java FFT N + * Dependencies: Complex.java + * + * Compute the FFT and inverse FFT of a length N complex sequence. + * Bare bones implementation that runs in O(N log N) time. Our goal + * is to optimize the clarity of the code, rather than performance. + * + * Limitations + * ----------- + * - assumes N is a power of 2 + * + * - not the most memory efficient algorithm (because it uses + * an object type for representing complex numbers and because + * it re-allocates memory for the subarray, instead of doing + * in-place or reusing a single temporary array) + * + ******************************************************************************/ + +class FFT { + + // compute the FFT of x[], assuming its length is a power of 2 + public static Complex[] fft(Complex[] x) { + int N = x.length; + + // base case + if (N == 1) return new Complex[] { x[0] }; + + // radix 2 Cooley-Tukey FFT + if (N % 2 != 0) { + throw new RuntimeException("N is not a power of 2"); + } + + // fft of even terms + Complex[] even = new Complex[N / 2]; + for (int k = 0; k < N / 2; k++) { + even[k] = x[2 * k]; + } + Complex[] q = fft(even); + + // fft of odd terms + Complex[] odd = even; // reuse the array + for (int k = 0; k < N / 2; k++) { + odd[k] = x[2 * k + 1]; + } + Complex[] r = fft(odd); + + // combine + Complex[] y = new Complex[N]; + for (int k = 0; k < N / 2; k++) { + double kth = -2 * k * Math.PI / N; + Complex wk = new Complex(Math.cos(kth), Math.sin(kth)); + y[k] = q[k].plus(wk.times(r[k])); + y[k + N / 2] = q[k].minus(wk.times(r[k])); + } + return y; + } + + // compute the inverse FFT of x[], assuming its length is a power of 2 + public static Complex[] ifft(Complex[] x) { + int N = x.length; + Complex[] y = new Complex[N]; + + // take conjugate + for (int i = 0; i < N; i++) { + y[i] = x[i].conjugate(); + } + + // compute forward FFT + y = fft(y); + + // take conjugate again + for (int i = 0; i < N; i++) { + y[i] = y[i].conjugate(); + } + + // divide by N + for (int i = 0; i < N; i++) { + y[i] = y[i].times(1.0 / N); + } + + return y; + } + + // compute the circular convolution of x and y + public static Complex[] cconvolve(Complex[] x, Complex[] y) { + + // should probably pad x and y with 0s so that they have same length + // and are powers of 2 + if (x.length != y.length) { + throw new RuntimeException("Dimensions don't agree"); + } + + int N = x.length; + + // compute FFT of each sequence + Complex[] a = fft(x); + Complex[] b = fft(y); + + // point-wise multiply + Complex[] c = new Complex[N]; + for (int i = 0; i < N; i++) { + c[i] = a[i].times(b[i]); + } + + // compute inverse FFT + return ifft(c); + } + + // compute the linear convolution of x and y + private static Complex[] convolve(Complex[] x, Complex[] y) { + Complex ZERO = new Complex(0, 0); + + Complex[] a = new Complex[2 * x.length]; + System.arraycopy(x, 0, a, 0, x.length); + for (int i = x.length; i < 2 * x.length; i++) a[i] = ZERO; + + Complex[] b = new Complex[2 * y.length]; + System.arraycopy(y, 0, b, 0, y.length); + for (int i = y.length; i < 2 * y.length; i++) b[i] = ZERO; + + return cconvolve(a, b); + } + + // display an array of Complex numbers to standard output + private static void show(Complex[] x, String title) { + System.out.println(title); + System.out.println("-------------------"); + for (Complex aX : x) { + System.out.println(aX); + } + System.out.println(); + } + + /*************************************************************************** + * Test client and sample execution + * + * % java FFT 4 + * x + * ------------------- + * -0.03480425839330703 + * 0.07910192950176387 + * 0.7233322451735928 + * 0.1659819820667019 + * + * y = fft(x) + * ------------------- + * 0.9336118983487516 + * -0.7581365035668999 + 0.08688005256493803i + * 0.44344407521182005 + * -0.7581365035668999 - 0.08688005256493803i + * + * z = ifft(y) + * ------------------- + * -0.03480425839330703 + * 0.07910192950176387 + 2.6599344570851287E-18i + * 0.7233322451735928 + * 0.1659819820667019 - 2.6599344570851287E-18i + * + * c = cconvolve(x, x) + * ------------------- + * 0.5506798633981853 + * 0.23461407150576394 - 4.033186818023279E-18i + * -0.016542951108772352 + * 0.10288019294318276 + 4.033186818023279E-18i + * + * d = convolve(x, x) + * ------------------- + * 0.001211336402308083 - 3.122502256758253E-17i + * -0.005506167987577068 - 5.058885073636224E-17i + * -0.044092969479563274 + 2.1934338938072244E-18i + * 0.10288019294318276 - 3.6147323062478115E-17i + * 0.5494685269958772 + 3.122502256758253E-17i + * 0.240120239493341 + 4.655566391833896E-17i + * 0.02755001837079092 - 2.1934338938072244E-18i + * 4.01805098805014E-17i + * + ***************************************************************************/ + + public static void main(String[] args) { + int N = Integer.parseInt(args[0]); + Complex[] x = new Complex[N]; + + // original data + for (int i = 0; i < N; i++) { + x[i] = new Complex(i, 0); + x[i] = new Complex(-2 * Math.random() + 1, 0); + } + show(x, "x"); + + // FFT of original data + Complex[] y = fft(x); + show(y, "y = fft(x)"); + + // take inverse FFT + Complex[] z = ifft(y); + show(z, "z = ifft(y)"); + + // circular convolution of x with itself + Complex[] c = cconvolve(x, x); + show(c, "c = cconvolve(x, x)"); + + // linear convolution of x with itself + Complex[] d = convolve(x, x); + show(d, "d = convolve(x, x)"); + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/recording/IAudioRecorder.java b/app/src/main/java/in/arjsna/audiorecorder/recording/IAudioRecorder.java new file mode 100644 index 0000000..e68c878 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/recording/IAudioRecorder.java @@ -0,0 +1,16 @@ +package in.arjsna.audiorecorder.recording; + +/** + * Interface for audio recorder + */ +interface IAudioRecorder { + void startRecord(int recordSampleRate); + + void finishRecord(); + + void pauseRecord(); + + void resumeRecord(); + + boolean isRecording(); +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ColorPalette.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ColorPalette.java new file mode 100644 index 0000000..650ad18 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ColorPalette.java @@ -0,0 +1,351 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.graphics.Color; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.ColorUtils; +import in.arjsna.audiorecorder.R; + +public class ColorPalette { + + public static int[] getAccentColors(Context context) { + return new int[] { + ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_blue_grey_500), + }; + } + + public static int getObscuredColor(int c) { + float[] hsv = new float[3]; + int color = c; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.85f; // value component + color = Color.HSVToColor(hsv); + return color; + } + + public static int getTransparentColor(int color, int alpha) { + return ColorUtils.setAlphaComponent(color, alpha); + } + + public static String getHexColor(int color) { + return String.format("#%06X", (0xFFFFFF & color)); + } + + public static int getDarkerColor(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.72f; // value component + return Color.HSVToColor(hsv); + } + + public static int[] getTransparencyShadows(int color) { + int[] shadows = new int[10]; + for (int i = 0; i < 10; i++) + shadows[i] = (ColorPalette.getTransparentColor(color, ((100 - (i * 10)) * 255) / 100)); + return shadows; + } + + public static int[] getBaseColors(Context context) { + return new int[] { + ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_pink_500), + ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_indigo_500), + ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_light_green_500), + ContextCompat.getColor(context, R.color.md_lime_500), + ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_amber_500), + ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_blue_grey_500), + ContextCompat.getColor(context, R.color.md_grey_500), + ContextCompat.getColor(context, R.color.av_color5) + }; + } + + public static int[] getColors(Context context, int c) { + if (c == ContextCompat.getColor(context, R.color.md_red_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_red_600), + //ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_red_400), + ContextCompat.getColor(context, R.color.md_red_300), + ContextCompat.getColor(context, R.color.md_red_200), + //ContextCompat.getColor(context, R.color.md_red_800), + //ContextCompat.getColor(context, R.color.md_red_700), + //ContextCompat.getColor(context, R.color.md_red_900), + //ContextCompat.getColor(context, R.color.md_red_A200), + //ContextCompat.getColor(context, R.color.md_red_A400), + //ContextCompat.getColor(context, R.color.md_red_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.av_color5)) { + return new int[] { + ContextCompat.getColor(context, R.color.av_color4), + ContextCompat.getColor(context, R.color.av_color3), + ContextCompat.getColor(context, R.color.av_color2), + ContextCompat.getColor(context, R.color.av_color1), + }; + } else if (c == ContextCompat.getColor(context, R.color.md_pink_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_pink_600), + //ContextCompat.getColor(context, R.color.md_pink_500), + ContextCompat.getColor(context, R.color.md_pink_400), + ContextCompat.getColor(context, R.color.md_pink_300), + ContextCompat.getColor(context, R.color.md_pink_200), + //ContextCompat.getColor(context, R.color.md_pink_700), + //ContextCompat.getColor(context, R.color.md_pink_800), + //ContextCompat.getColor(context, R.color.md_pink_900), + //ContextCompat.getColor(context, R.color.md_pink_A200), + //ContextCompat.getColor(context, R.color.md_pink_A400), + //ContextCompat.getColor(context, R.color.md_pink_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_purple_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_purple_600), + //ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_purple_400), + ContextCompat.getColor(context, R.color.md_purple_300), + ContextCompat.getColor(context, R.color.md_purple_200), + //ContextCompat.getColor(context, R.color.md_purple_700), + //ContextCompat.getColor(context, R.color.md_purple_800), + //ContextCompat.getColor(context, R.color.md_purple_900), + //ContextCompat.getColor(context, R.color.md_purple_A200), + //ContextCompat.getColor(context, R.color.md_purple_A400), + //ContextCompat.getColor(context, R.color.md_purple_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_deep_purple_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_deep_purple_600), + //ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_400), + ContextCompat.getColor(context, R.color.md_deep_purple_300), + ContextCompat.getColor(context, R.color.md_deep_purple_200), + //ContextCompat.getColor(context, R.color.md_deep_purple_700), + //ContextCompat.getColor(context, R.color.md_deep_purple_800), + //ContextCompat.getColor(context, R.color.md_deep_purple_900), + //ContextCompat.getColor(context, R.color.md_deep_purple_A200), + //ContextCompat.getColor(context, R.color.md_deep_purple_A400), + //ContextCompat.getColor(context, R.color.md_deep_purple_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_indigo_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_indigo_600), + //ContextCompat.getColor(context, R.color.md_indigo_500), + ContextCompat.getColor(context, R.color.md_indigo_400), + ContextCompat.getColor(context, R.color.md_indigo_300), + ContextCompat.getColor(context, R.color.md_indigo_200), + //ContextCompat.getColor(context, R.color.md_indigo_700), + //ContextCompat.getColor(context, R.color.md_indigo_800), + //ContextCompat.getColor(context, R.color.md_indigo_900), + //ContextCompat.getColor(context, R.color.md_indigo_A200), + //ContextCompat.getColor(context, R.color.md_indigo_A400), + //ContextCompat.getColor(context, R.color.md_indigo_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_blue_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_blue_600), + //ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_blue_400), + ContextCompat.getColor(context, R.color.md_blue_300), + ContextCompat.getColor(context, R.color.md_blue_200), + //ContextCompat.getColor(context, R.color.md_blue_700), + //ContextCompat.getColor(context, R.color.md_blue_800), + //ContextCompat.getColor(context, R.color.md_blue_900), + //ContextCompat.getColor(context, R.color.md_blue_A200), + //ContextCompat.getColor(context, R.color.md_blue_A400), + //ContextCompat.getColor(context, R.color.md_blue_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_light_blue_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_light_blue_600), + //ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_400), + ContextCompat.getColor(context, R.color.md_light_blue_300), + ContextCompat.getColor(context, R.color.md_light_blue_200), + //ContextCompat.getColor(context, R.color.md_light_blue_700), + //ContextCompat.getColor(context, R.color.md_light_blue_800), + //ContextCompat.getColor(context, R.color.md_light_blue_900), + //ContextCompat.getColor(context, R.color.md_light_blue_A200), + //ContextCompat.getColor(context, R.color.md_light_blue_A400), + //ContextCompat.getColor(context, R.color.md_light_blue_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_cyan_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_cyan_600), + //ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_cyan_400), + ContextCompat.getColor(context, R.color.md_cyan_300), + ContextCompat.getColor(context, R.color.md_cyan_200), + //ContextCompat.getColor(context, R.color.md_cyan_700), + //ContextCompat.getColor(context, R.color.md_cyan_800), + //ContextCompat.getColor(context, R.color.md_cyan_900), + //ContextCompat.getColor(context, R.color.md_cyan_A200), + //ContextCompat.getColor(context, R.color.md_cyan_A400), + //ContextCompat.getColor(context, R.color.md_cyan_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_teal_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_teal_600), + //ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_teal_400), + ContextCompat.getColor(context, R.color.md_teal_300), + ContextCompat.getColor(context, R.color.md_teal_200), + //ContextCompat.getColor(context, R.color.md_teal_700), + //ContextCompat.getColor(context, R.color.md_teal_800), + //ContextCompat.getColor(context, R.color.md_teal_900), + //ContextCompat.getColor(context, R.color.md_teal_A200), + //ContextCompat.getColor(context, R.color.md_teal_A400), + //ContextCompat.getColor(context, R.color.md_teal_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_green_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_green_600), + //ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_green_400), + ContextCompat.getColor(context, R.color.md_green_300), + ContextCompat.getColor(context, R.color.md_green_200), + //ContextCompat.getColor(context, R.color.md_green_700), + //ContextCompat.getColor(context, R.color.md_green_800), + //ContextCompat.getColor(context, R.color.md_green_900), + //ContextCompat.getColor(context, R.color.md_green_A200), + //ContextCompat.getColor(context, R.color.md_green_A400), + //ContextCompat.getColor(context, R.color.md_green_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_light_green_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_light_green_600), + //ContextCompat.getColor(context, R.color.md_light_green_500), + ContextCompat.getColor(context, R.color.md_light_green_400), + ContextCompat.getColor(context, R.color.md_light_green_300), + ContextCompat.getColor(context, R.color.md_light_green_200), + //ContextCompat.getColor(context, R.color.md_light_green_700), + //ContextCompat.getColor(context, R.color.md_light_green_800), + //ContextCompat.getColor(context, R.color.md_light_green_900), + //ContextCompat.getColor(context, R.color.md_light_green_A200), + //ContextCompat.getColor(context, R.color.md_light_green_A400), + //ContextCompat.getColor(context, R.color.md_light_green_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_lime_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_lime_600), + //ContextCompat.getColor(context, R.color.md_lime_500), + ContextCompat.getColor(context, R.color.md_lime_400), + ContextCompat.getColor(context, R.color.md_lime_300), + ContextCompat.getColor(context, R.color.md_lime_200), + //ContextCompat.getColor(context, R.color.md_lime_700), + //ContextCompat.getColor(context, R.color.md_lime_800), + //ContextCompat.getColor(context, R.color.md_lime_900), + //ContextCompat.getColor(context, R.color.md_lime_A200), + //ContextCompat.getColor(context, R.color.md_lime_A400), + //ContextCompat.getColor(context, R.color.md_lime_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_yellow_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_yellow_600), + //ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_yellow_300), + ContextCompat.getColor(context, R.color.md_yellow_400), + ContextCompat.getColor(context, R.color.md_yellow_200), + //ContextCompat.getColor(context, R.color.md_yellow_700), + //ContextCompat.getColor(context, R.color.md_yellow_800), + //ContextCompat.getColor(context, R.color.md_yellow_900), + //ContextCompat.getColor(context, R.color.md_yellow_A200), + //ContextCompat.getColor(context, R.color.md_yellow_A400), + //ContextCompat.getColor(context, R.color.md_yellow_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_amber_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_amber_600), + //ContextCompat.getColor(context, R.color.md_amber_500), + ContextCompat.getColor(context, R.color.md_amber_400), + ContextCompat.getColor(context, R.color.md_amber_300), + ContextCompat.getColor(context, R.color.md_amber_200), + //ContextCompat.getColor(context, R.color.md_amber_700), + //ContextCompat.getColor(context, R.color.md_amber_800), + //ContextCompat.getColor(context, R.color.md_amber_900), + //ContextCompat.getColor(context, R.color.md_amber_A200), + //ContextCompat.getColor(context, R.color.md_amber_A400), + //ContextCompat.getColor(context, R.color.md_amber_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_orange_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_orange_600), + //ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_orange_400), + ContextCompat.getColor(context, R.color.md_orange_300), + ContextCompat.getColor(context, R.color.md_orange_200), + //ContextCompat.getColor(context, R.color.md_orange_700), + //ContextCompat.getColor(context, R.color.md_orange_800), + //ContextCompat.getColor(context, R.color.md_orange_900), + //ContextCompat.getColor(context, R.color.md_orange_A200), + //ContextCompat.getColor(context, R.color.md_orange_A400), + //ContextCompat.getColor(context, R.color.md_orange_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_deep_orange_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_deep_orange_600), + //ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_400), + ContextCompat.getColor(context, R.color.md_deep_orange_300), + ContextCompat.getColor(context, R.color.md_deep_orange_200), + //ContextCompat.getColor(context, R.color.md_deep_orange_700), + //ContextCompat.getColor(context, R.color.md_deep_orange_800), + //ContextCompat.getColor(context, R.color.md_deep_orange_900), + //ContextCompat.getColor(context, R.color.md_deep_orange_A200), + //ContextCompat.getColor(context, R.color.md_deep_orange_A400), + //ContextCompat.getColor(context, R.color.md_deep_orange_A700) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_brown_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_brown_600), + //ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_brown_400), + ContextCompat.getColor(context, R.color.md_brown_300), + ContextCompat.getColor(context, R.color.md_brown_200), + //ContextCompat.getColor(context, R.color.md_brown_700), + //ContextCompat.getColor(context, R.color.md_brown_800), + //ContextCompat.getColor(context, R.color.md_brown_900), + }; + } else if (c == ContextCompat.getColor(context, R.color.md_grey_500)) { + return new int[] { + ContextCompat.getColor(context, R.color.md_grey_800), + ContextCompat.getColor(context, R.color.md_grey_700), + ContextCompat.getColor(context, R.color.md_grey_600), + //ContextCompat.getColor(context, R.color.md_grey_500), + ContextCompat.getColor(context, R.color.md_grey_400), + //ContextCompat.getColor(context, R.color.md_grey_900), + //Color.parseColor("#000000") + }; + } else { + return new int[] { + ContextCompat.getColor(context, R.color.md_blue_grey_600), + //ContextCompat.getColor(context, R.color.md_blue_grey_500), + ContextCompat.getColor(context, R.color.md_blue_grey_400), + ContextCompat.getColor(context, R.color.md_blue_grey_300), + ContextCompat.getColor(context, R.color.md_blue_grey_200), + //ContextCompat.getColor(context, R.color.md_blue_grey_700), + //ContextCompat.getColor(context, R.color.md_blue_grey_800), + //ContextCompat.getColor(context, R.color.md_blue_grey_900) + }; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ColorsSetting.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ColorsSetting.java new file mode 100644 index 0000000..9b94b95 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ColorsSetting.java @@ -0,0 +1,78 @@ +package in.arjsna.audiorecorder.theme; + +import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import in.arjsna.audiorecorder.R; +import uz.shift.colorpicker.LineColorPicker; + +public class ColorsSetting extends ThemedSetting { + + public ColorsSetting(ThemedActivity activity) { + super(activity); + } + + public static class SelectedColor { + public int colorPrimary; + public int[] shades; + } + + public interface ColorChooser { + void onColorSelected(SelectedColor color); + + void onDialogDismiss(); + + void onColorChanged(int color); + } + + public void chooseColor(@StringRes int title, final ColorChooser chooser, int defaultColor) { + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + + View dialogLayout = + LayoutInflater.from(getActivity()).inflate(R.layout.dialog_color_picker, null); + final LineColorPicker colorPicker = + (LineColorPicker) dialogLayout.findViewById(R.id.color_picker_primary); + final LineColorPicker colorPicker2 = + (LineColorPicker) dialogLayout.findViewById(R.id.color_picker_primary_2); + final TextView dialogTitle = (TextView) dialogLayout.findViewById(R.id.dialog_title); + dialogTitle.setText(title); + dialogTitle.setBackgroundColor(getActivity().getPrimaryColor()); + colorPicker.setSelected(true); + colorPicker.setOnColorChangedListener(c -> { + dialogTitle.setBackgroundColor(c); + colorPicker2.setColors(ColorPalette.getColors(getActivity(), colorPicker.getColor())); + colorPicker2.setSelectedColor(colorPicker.getColor()); + dialogTitle.setBackgroundColor(colorPicker2.getColors()[0]); + chooser.onColorChanged(colorPicker2.getColors()[0]); + }); + + int[] baseColors = ColorPalette.getBaseColors(getActivity()); + colorPicker.setColors(baseColors); + colorPicker.setSelectedColor(defaultColor); + + dialogBuilder.setView(dialogLayout); + + dialogBuilder.setNegativeButton( + getActivity().getString(R.string.dialog_action_cancel).toUpperCase(), + (dialog, which) -> { + dialog.cancel(); + chooser.onDialogDismiss(); + }); + + dialogBuilder.setPositiveButton( + getActivity().getString(R.string.dialog_action_ok).toUpperCase(), + (dialog, which) -> { + AlertDialog alertDialog = (AlertDialog) dialog; + alertDialog.setOnDismissListener(null); + SelectedColor selectedColor = new SelectedColor(); + selectedColor.colorPrimary = colorPicker.getColor(); + selectedColor.shades = colorPicker2.getColors(); + chooser.onColorSelected(selectedColor); + }); + + dialogBuilder.setOnDismissListener(dialog -> chooser.onDialogDismiss()); + dialogBuilder.show(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/Theme.java b/app/src/main/java/in/arjsna/audiorecorder/theme/Theme.java new file mode 100644 index 0000000..1f9e180 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/Theme.java @@ -0,0 +1,27 @@ +package in.arjsna.audiorecorder.theme; + +public enum Theme { + LIGHT(1), DARK(2), AMOLED(3); + + int value; + + Theme(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Theme fromValue(int value) { + switch (value) { + case 1: + default: + return LIGHT; + case 2: + return DARK; + case 3: + return AMOLED; + } + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemeHelper.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemeHelper.java new file mode 100644 index 0000000..647af86 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemeHelper.java @@ -0,0 +1,301 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.ColorRes; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.SwitchCompat; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.TextView; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.R; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class ThemeHelper { + + private Context context; + + Theme baseTheme; + private int primaryColor; + private int accentColor; + private int[] layerColor; + + private ThemeHelper(Context context) { + this.context = context; + } + + public static ThemeHelper getInstance(Context context) { + return new ThemeHelper(context); + } + + public static ThemeHelper getInstanceLoaded(Context context) { + ThemeHelper instance = getInstance(context); + instance.updateTheme(); + return instance; + } + + public void updateTheme() { + this.primaryColor = Hawk.get(context.getString(R.string.preference_primary_color), + getColor(R.color.av_color5)); + this.accentColor = Hawk.get(context.getString(R.string.preference_accent_color), + getColor(R.color.av_color5)); + this.layerColor = Hawk.get(context.getString(R.string.preference_layer_colors), + ColorPalette.getColors(context, ContextCompat.getColor(context, R.color.av_color5))); + baseTheme = Theme.fromValue(Hawk.get(context.getString(R.string.preference_base_theme), 1)); + } + + public int[] getLayerColor() { + return layerColor; + } + + public int getPrimaryColor() { + return primaryColor; + } + + public int getAccentColor() { + return accentColor; + } + + public Theme getBaseTheme() { + return baseTheme; + } + + public void setBaseTheme(Theme baseTheme) { + this.baseTheme = baseTheme; + Hawk.put(context.getString(R.string.preference_base_theme), getBaseTheme().getValue()); + } + + public static int getPrimaryColor(Context context) { + return Hawk.get(context.getString(R.string.preference_primary_color), + ContextCompat.getColor(context, R.color.md_indigo_500)); + } + + public boolean isPrimaryEqualAccent() { + return (this.primaryColor == this.accentColor); + } + + public static int getAccentColor(Context context) { + return Hawk.get(context.getString(R.string.preference_accent_color), + ContextCompat.getColor(context, R.color.md_light_blue_500)); + } + + public static Theme getBaseTheme(Context context) { + return Theme.fromValue( + Hawk.get(context.getString(R.string.preference_base_theme), Theme.LIGHT.value)); + } + + public int getColor(@ColorRes int color) { + return ContextCompat.getColor(context, color); + } + + private static int getColor(Context context, @ColorRes int color) { + return ContextCompat.getColor(context, color); + } + + public void themeSeekBar(SeekBar bar) { + bar.getProgressDrawable() + .setColorFilter(new PorterDuffColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN)); + bar.getThumb() + .setColorFilter(new PorterDuffColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN)); + } + + public int getBackgroundColor() { + switch (baseTheme) { + case DARK: + return getColor(R.color.md_dark_background); + case AMOLED: + return getColor(R.color.md_black_1000); + case LIGHT: + default: + return getColor(R.color.md_light_background); + } + } + + public int getInvertedBackgroundColor() { + switch (baseTheme) { + case DARK: + case AMOLED: + return getColor(R.color.md_light_background); + case LIGHT: + default: + return getColor(R.color.md_dark_background); + } + } + + public int getTextColor() { + switch (baseTheme) { + case DARK: + case AMOLED: + return getColor(R.color.md_grey_200); + case LIGHT: + default: + return getColor(R.color.md_grey_800); + } + } + + public int getSubTextColor() { + switch (baseTheme) { + case DARK: + case AMOLED: + return getColor(R.color.md_grey_400); + case LIGHT: + default: + return getColor(R.color.md_grey_600); + } + } + + public int getCardBackgroundColor() { + switch (baseTheme) { + case DARK: + return getColor(R.color.md_dark_cards); + case AMOLED: + return getColor(R.color.md_black_1000); + case LIGHT: + default: + return getColor(R.color.md_light_cards); + } + } + + public int getButtonBackgroundColor() { + switch (baseTheme) { + case DARK: + return getColor(R.color.md_grey_700); + case AMOLED: + return getColor(R.color.md_grey_900); + case LIGHT: + default: + return getColor(R.color.md_grey_200); + } + } + + public int getDrawerBackground() { + switch (baseTheme) { + case DARK: + return getColor(R.color.md_dark_cards); + case AMOLED: + return getColor(R.color.md_black_1000); + case LIGHT: + default: + return getColor(R.color.md_light_cards); + } + } + + public int getDefaultThemeToolbarColor3th() { + switch (baseTheme) { + case DARK: + return getColor(R.color.md_black_1000); + case LIGHT: + default: + case AMOLED: + return getColor(R.color.md_blue_grey_800); + } + } + + private ColorStateList getTintList() { + return new ColorStateList( + new int[][] { + new int[] {-android.R.attr.state_enabled}, //disabled + new int[] {android.R.attr.state_enabled} //enabled + }, new int[] {getTextColor(), getAccentColor()}); + } + + public void themeRadioButton(RadioButton radioButton) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + radioButton.setButtonTintList(getTintList()); + radioButton.setTextColor(getTextColor()); + } + } + + public void themeCheckBox(CheckBox chk) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + chk.setButtonTintList(getTintList()); + chk.setTextColor(getTextColor()); + } + } + + public void themeButton(Button btn) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + btn.setTextColor(getTextColor()); + btn.setBackgroundColor(getButtonBackgroundColor()); + } + } + + public void setSwitchCompactColor(SwitchCompat sw, int color) { + /** SWITCH HEAD **/ + sw.getThumbDrawable().setColorFilter( + sw.isChecked() ? color : + (baseTheme.equals(Theme.LIGHT) ? getColor(R.color.md_grey_200) + : getColor(R.color.md_grey_600)), + PorterDuff.Mode.MULTIPLY); + /** SWITCH BODY **/ + sw.getTrackDrawable().setColorFilter( + sw.isChecked() ? ColorPalette.getTransparentColor(color, 100) : + (baseTheme.equals(Theme.LIGHT) ? getColor(R.color.md_grey_400) + : getColor(R.color.md_grey_900)), + PorterDuff.Mode.MULTIPLY); + } + + public void setTextViewColor(TextView txt, int color) { + txt.setTextColor(color); + } + + public void setScrollViewColor(ScrollView scr) { + try { + Field mScrollCacheField = View.class.getDeclaredField("mScrollCache"); + mScrollCacheField.setAccessible(true); + Object mScrollCache = mScrollCacheField.get(scr); // scr is your Scroll View + + Field scrollBarField = mScrollCache.getClass().getDeclaredField("scrollBar"); + scrollBarField.setAccessible(true); + Object scrollBar = scrollBarField.get(mScrollCache); + + Method method = + scrollBar.getClass().getDeclaredMethod("setVerticalThumbDrawable", Drawable.class); + method.setAccessible(true); + + ColorDrawable ColorDraw = new ColorDrawable(getPrimaryColor()); + method.invoke(scrollBar, ColorDraw); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void setColorScrollBarDrawable(Drawable drawable) { + drawable.setColorFilter(new PorterDuffColorFilter(getPrimaryColor(), PorterDuff.Mode.SRC_ATOP)); + } + + public static void setCursorColor(EditText editText, int color) { + try { + Field fCursorDrawableRes = + TextView.class.getDeclaredField("mCursorDrawableRes"); + fCursorDrawableRes.setAccessible(true); + int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); + Field fEditor = TextView.class.getDeclaredField("mEditor"); + fEditor.setAccessible(true); + Object editor = fEditor.get(editText); + Class clazz = editor.getClass(); + Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); + fCursorDrawable.setAccessible(true); + + Drawable[] drawables = new Drawable[2]; + drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); + fCursorDrawable.set(editor, drawables); + } catch (final Throwable ignored) { + } + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/Themed.java b/app/src/main/java/in/arjsna/audiorecorder/theme/Themed.java new file mode 100644 index 0000000..fad4691 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/Themed.java @@ -0,0 +1,5 @@ +package in.arjsna.audiorecorder.theme; + +public interface Themed { + void refreshTheme(ThemeHelper themeHelper); +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedActivity.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedActivity.java new file mode 100644 index 0000000..873a08a --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedActivity.java @@ -0,0 +1,160 @@ +package in.arjsna.audiorecorder.theme; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.SwitchCompat; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.TextView; +import com.orhanobut.hawk.Hawk; +import in.arjsna.audiorecorder.R; + +public abstract class ThemedActivity extends AppCompatActivity implements UiElementInizializer { + + private ThemeHelper themeHelper; + + private boolean coloredNavBar; + private boolean applyThemeSingleImgAct; + private boolean customIconColor; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + themeHelper = ThemeHelper.getInstance(getApplicationContext()); + } + + @Override + public void onResume() { + super.onResume(); + updateTheme(); + updateUiElements(); + setStatusBarColor(); + } + + public ThemeHelper getThemeHelper() { + return themeHelper; + } + + public void updateTheme() { + themeHelper.updateTheme(); + applyThemeSingleImgAct = Hawk.get("apply_theme_img_act", true); + } + + @CallSuper + @Override + public void updateUiElements() { + setStatusBarColor(); + for (View view : ViewUtil.getAllChildren(findViewById(android.R.id.content))) { + if (view instanceof Themed) ((Themed) view).refreshTheme(getThemeHelper()); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void setNavBarColor() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (isNavigationBarColored()) { + getWindow().setNavigationBarColor(getPrimaryColor()); + } else { + getWindow().setNavigationBarColor( + ContextCompat.getColor(getApplicationContext(), R.color.md_black_1000)); + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + protected void setStatusBarColor() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(ColorPalette.getObscuredColor(getPrimaryColor())); + } + } + + protected void setScrollViewColor(ScrollView scr) { + themeHelper.setScrollViewColor(scr); + } + + public boolean isNavigationBarColored() { + return coloredNavBar; + } + + public boolean themeOnSingleImgAct() { + return applyThemeSingleImgAct; + } + + public void setBaseTheme(Theme baseTheme) { + themeHelper.setBaseTheme(baseTheme); + } + + public void themeSeekBar(SeekBar bar) { + themeHelper.themeSeekBar(bar); + } + + public int getPrimaryColor() { + return themeHelper.getPrimaryColor(); + } + + public int getAccentColor() { + return themeHelper.getAccentColor(); + } + + public Theme getBaseTheme() { + return themeHelper.getBaseTheme(); + } + + public int getBackgroundColor() { + return themeHelper.getBackgroundColor(); + } + + protected int getInvertedBackgroundColor() { + return themeHelper.getInvertedBackgroundColor(); + } + + public int getTextColor() { + return themeHelper.getTextColor(); + } + + public int getSubTextColor() { + return themeHelper.getSubTextColor(); + } + + public int getCardBackgroundColor() { + return themeHelper.getCardBackgroundColor(); + } + + protected int getDrawerBackground() { + return themeHelper.getDrawerBackground(); + } + + protected int getDefaultThemeToolbarColor3th() { + return themeHelper.getDefaultThemeToolbarColor3th(); + } + + public void themeRadioButton(RadioButton radioButton) { + themeHelper.themeRadioButton(radioButton); + } + + public void themeCheckBox(CheckBox chk) { + themeHelper.themeCheckBox(chk); + } + + protected void themeButton(Button btn) { + themeHelper.themeButton(btn); + } + + public void setSwitchColor(int color, SwitchCompat... sw) { + for (SwitchCompat switchCompat : sw) + themeHelper.setSwitchCompactColor(switchCompat, color); + } + + public void setTextViewColor(int color, TextView... textViews) { + for (TextView txt : textViews) + themeHelper.setTextViewColor(txt, color); + } +} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedDialogFragment.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedDialogFragment.java new file mode 100644 index 0000000..748c3fb --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedDialogFragment.java @@ -0,0 +1,32 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.support.v4.app.DialogFragment; + +public abstract class ThemedDialogFragment extends DialogFragment implements Themed { + + ThemeHelper themeHelper; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /*if (context instanceof ThemedActivity) + themeHelper = ((ThemedActivity) context).getThemeHelper();*/ + themeHelper = ThemeHelper.getInstance(getContext()); + } + + @Override + public void onResume() { + super.onResume(); + themeHelper.updateTheme(); + refreshTheme(themeHelper); + } + + public ThemeHelper getThemeHelper() { + return themeHelper; + } + + public int getPrimaryColor() { + return themeHelper.getPrimaryColor(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFab.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFab.java new file mode 100644 index 0000000..261e51f --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFab.java @@ -0,0 +1,26 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.support.design.widget.FloatingActionButton; +import android.util.AttributeSet; + +public class ThemedFab extends FloatingActionButton implements Themed { + + public ThemedFab(Context context) { + super(context); + } + + public ThemedFab(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ThemedFab(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void refreshTheme(ThemeHelper theme) { + setBackgroundTintList(ColorStateList.valueOf(theme.getAccentColor())); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFragment.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFragment.java new file mode 100644 index 0000000..2ea47eb --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedFragment.java @@ -0,0 +1,52 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.support.v4.app.Fragment; + +public abstract class ThemedFragment extends Fragment implements Themed { + + ThemeHelper themeHelper; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /*if (context instanceof ThemedActivity) + themeHelper = ((ThemedActivity) context).getThemeHelper();*/ + themeHelper = ThemeHelper.getInstance(getContext()); + } + + @Override + public void onResume() { + super.onResume(); + themeHelper.updateTheme(); + refreshTheme(themeHelper); + } + + public ThemeHelper getThemeHelper() { + return themeHelper; + } + + public int getPrimaryColor() { + return themeHelper.getPrimaryColor(); + } + + public int getAccentColor() { + return themeHelper.getAccentColor(); + } + + public Theme getBaseTheme() { + return themeHelper.getBaseTheme(); + } + + public int getBackgroundColor() { + return themeHelper.getBackgroundColor(); + } + + public int getCardBackgroundColor() { + return themeHelper.getCardBackgroundColor(); + } + + public int getTextColor() { + return themeHelper.getTextColor(); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedSetting.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedSetting.java new file mode 100644 index 0000000..96e57ad --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedSetting.java @@ -0,0 +1,17 @@ +package in.arjsna.audiorecorder.theme; + +class ThemedSetting { + + private ThemedActivity activity; + + ThemedSetting(ThemedActivity activity) { + this.activity = activity; + } + + public ThemedSetting() { + } + + public ThemedActivity getActivity() { + return activity; + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedToolbar.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedToolbar.java new file mode 100644 index 0000000..f74c10e --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ThemedToolbar.java @@ -0,0 +1,25 @@ +package in.arjsna.audiorecorder.theme; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v7.widget.Toolbar; +import android.util.AttributeSet; + +public class ThemedToolbar extends Toolbar implements Themed { + public ThemedToolbar(Context context) { + this(context, null); + } + + public ThemedToolbar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ThemedToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void refreshTheme(ThemeHelper themeHelper) { + setBackgroundColor(themeHelper.getPrimaryColor()); + } +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/UiElementInizializer.java b/app/src/main/java/in/arjsna/audiorecorder/theme/UiElementInizializer.java new file mode 100644 index 0000000..248d9b6 --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/UiElementInizializer.java @@ -0,0 +1,5 @@ +package in.arjsna.audiorecorder.theme; + +public interface UiElementInizializer { + void updateUiElements(); +} diff --git a/app/src/main/java/in/arjsna/audiorecorder/theme/ViewUtil.java b/app/src/main/java/in/arjsna/audiorecorder/theme/ViewUtil.java new file mode 100644 index 0000000..159472c --- /dev/null +++ b/app/src/main/java/in/arjsna/audiorecorder/theme/ViewUtil.java @@ -0,0 +1,42 @@ +package in.arjsna.audiorecorder.theme; + +import android.app.Activity; +import android.content.res.Resources; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ViewUtil { + // http://stackoverflow.com/questions/18668897/android-get-all-children-elements-of-a-viewgroup + public static List getAllChildren(View target) { + if (!(target instanceof ViewGroup)) return Collections.singletonList(target); + + ArrayList allChildren = new ArrayList<>(); + ViewGroup viewGroup = (ViewGroup) target; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View child = viewGroup.getChildAt(i); + ArrayList targetsChildren = new ArrayList<>(); + targetsChildren.add(target); + targetsChildren.addAll(getAllChildren(child)); + allChildren.addAll(targetsChildren); + } + return allChildren; + } + + public static boolean hasNavBar(Activity activity) { + Resources resources = activity.getResources(); + int id = resources.getIdentifier("config_showNavigationBar", "bool", "android"); + if (id > 0) { + return resources.getBoolean(id); + } else { // Check for keys + boolean hasMenuKey = ViewConfiguration.get(activity).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + return !hasMenuKey && !hasBackKey; + } + } +} diff --git a/app/src/main/java/in/arjsna/voicerecorder/MySharedPreferences.java b/app/src/main/java/in/arjsna/voicerecorder/MySharedPreferences.java deleted file mode 100644 index 0bd51cb..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/MySharedPreferences.java +++ /dev/null @@ -1,21 +0,0 @@ -package in.arjsna.voicerecorder; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -public class MySharedPreferences { - private static String PREF_HIGH_QUALITY = "pref_high_quality"; - - public static void setPrefHighQuality(Context context, boolean isEnabled) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(PREF_HIGH_QUALITY, isEnabled); - editor.apply(); - } - - public static boolean getPrefHighQuality(Context context) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - return preferences.getBoolean(PREF_HIGH_QUALITY, false); - } -} diff --git a/app/src/main/java/in/arjsna/voicerecorder/RecordingService.java b/app/src/main/java/in/arjsna/voicerecorder/RecordingService.java deleted file mode 100644 index b620cde..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/RecordingService.java +++ /dev/null @@ -1,163 +0,0 @@ -package in.arjsna.voicerecorder; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.media.MediaRecorder; -import android.os.Environment; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.widget.Toast; -import in.arjsna.voicerecorder.activities.MainActivity; -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.Timer; -import java.util.TimerTask; - -public class RecordingService extends Service { - - private static final String LOG_TAG = "RecordingService"; - - private String mFileName = null; - private String mFilePath = null; - - private MediaRecorder mRecorder = null; - - private DBHelper mDatabase; - - private long mStartingTimeMillis = 0; - private long mElapsedMillis = 0; - private int mElapsedSeconds = 0; - private OnTimerChangedListener onTimerChangedListener = null; - private static final SimpleDateFormat mTimerFormat = - new SimpleDateFormat("mm:ss", Locale.getDefault()); - - private Timer mTimer = null; - private TimerTask mIncrementTimerTask = null; - - @Override public IBinder onBind(Intent intent) { - return null; - } - - public interface OnTimerChangedListener { - void onTimerChanged(int seconds); - } - - @Override public void onCreate() { - super.onCreate(); - mDatabase = new DBHelper(getApplicationContext()); - } - - @Override public int onStartCommand(Intent intent, int flags, int startId) { - startRecording(); - return START_STICKY; - } - - @Override public void onDestroy() { - if (mRecorder != null) { - stopRecording(); - } - - super.onDestroy(); - } - - public void startRecording() { - setFileNameAndPath(); - - mRecorder = new MediaRecorder(); - mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - mRecorder.setOutputFile(mFilePath); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mRecorder.setAudioChannels(1); - if (MySharedPreferences.getPrefHighQuality(this)) { - mRecorder.setAudioSamplingRate(44100); - mRecorder.setAudioEncodingBitRate(192000); - } - - try { - mRecorder.prepare(); - mRecorder.start(); - mStartingTimeMillis = System.currentTimeMillis(); - - //startTimer(); - //startForeground(1, createNotification()); - - } catch (IOException e) { - Log.e(LOG_TAG, "prepare() failed"); - } - } - - public void setFileNameAndPath() { - int count = 0; - File f; - - do { - count++; - - mFileName = - getString(R.string.default_file_name) + "_" + (mDatabase.getCount() + count) + ".mp4"; - mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath(); - mFilePath += "/SoundRecorder/" + mFileName; - - f = new File(mFilePath); - } while (f.exists() && !f.isDirectory()); - } - - public void stopRecording() { - mRecorder.stop(); - mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis); - mRecorder.release(); - Toast.makeText(this, getString(R.string.toast_recording_finish) + " " + mFilePath, - Toast.LENGTH_LONG).show(); - - //remove notification - if (mIncrementTimerTask != null) { - mIncrementTimerTask.cancel(); - mIncrementTimerTask = null; - } - - mRecorder = null; - - try { - mDatabase.addRecording(mFileName, mFilePath, mElapsedMillis); - } catch (Exception e) { - Log.e(LOG_TAG, "exception", e); - } - } - - private void startTimer() { - mTimer = new Timer(); - mIncrementTimerTask = new TimerTask() { - @Override public void run() { - mElapsedSeconds++; - if (onTimerChangedListener != null) onTimerChangedListener.onTimerChanged(mElapsedSeconds); - NotificationManager mgr = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mgr.notify(1, createNotification()); - } - }; - mTimer.scheduleAtFixedRate(mIncrementTimerTask, 1000, 1000); - } - - //TODO: - private Notification createNotification() { - NotificationCompat.Builder mBuilder = - new NotificationCompat.Builder(getApplicationContext()).setSmallIcon( - R.drawable.ic_mic_white_36dp) - .setContentTitle(getString(R.string.notification_recording)) - .setContentText(mTimerFormat.format(mElapsedSeconds * 1000)) - .setOngoing(true); - - mBuilder.setContentIntent(PendingIntent.getActivities(getApplicationContext(), 0, - new Intent[] { new Intent(getApplicationContext(), MainActivity.class) }, 0)); - - return mBuilder.build(); - } -} diff --git a/app/src/main/java/in/arjsna/voicerecorder/activities/MainActivity.java b/app/src/main/java/in/arjsna/voicerecorder/activities/MainActivity.java deleted file mode 100644 index 1b70baa..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/activities/MainActivity.java +++ /dev/null @@ -1,94 +0,0 @@ -package in.arjsna.voicerecorder.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; -import com.astuetz.PagerSlidingTabStrip; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.fragments.FileViewerFragment; -import in.arjsna.voicerecorder.fragments.RecordFragment; - -public class MainActivity extends AppCompatActivity { - - private static final String LOG_TAG = MainActivity.class.getSimpleName(); - - private PagerSlidingTabStrip tabs; - private ViewPager pager; - - @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - pager = (ViewPager) findViewById(R.id.pager); - pager.setAdapter(new MyAdapter(getSupportFragmentManager())); - tabs = (PagerSlidingTabStrip) findViewById(R.id.tabs); - tabs.setViewPager(pager); - - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - toolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light); - if (toolbar != null) { - setSupportActionBar(toolbar); - } - } - - @Override public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_settings: - Intent i = new Intent(this, SettingsActivity.class); - startActivity(i); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public class MyAdapter extends FragmentPagerAdapter { - private String[] titles = { - getString(R.string.tab_title_record), getString(R.string.tab_title_saved_recordings) - }; - - public MyAdapter(FragmentManager fm) { - super(fm); - } - - @Override public Fragment getItem(int position) { - switch (position) { - case 0: { - return RecordFragment.newInstance(position); - } - case 1: { - return FileViewerFragment.newInstance(position); - } - } - return null; - } - - @Override public int getCount() { - return titles.length; - } - - @Override public CharSequence getPageTitle(int position) { - return titles[position]; - } - } - - public MainActivity() { - } -} diff --git a/app/src/main/java/in/arjsna/voicerecorder/adapters/FileViewerAdapter.java b/app/src/main/java/in/arjsna/voicerecorder/adapters/FileViewerAdapter.java deleted file mode 100644 index 914ac24..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/adapters/FileViewerAdapter.java +++ /dev/null @@ -1,284 +0,0 @@ -package in.arjsna.voicerecorder.adapters; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Environment; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; -import in.arjsna.voicerecorder.DBHelper; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.RecordingItem; -import in.arjsna.voicerecorder.fragments.PlaybackFragment; -import in.arjsna.voicerecorder.listeners.OnDatabaseChangedListener; -import java.io.File; -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -public class FileViewerAdapter extends RecyclerView.Adapter - implements OnDatabaseChangedListener { - - private static final String LOG_TAG = "FileViewerAdapter"; - - private DBHelper mDatabase; - - RecordingItem item; - Context mContext; - LinearLayoutManager llm; - - public FileViewerAdapter(Context context, LinearLayoutManager linearLayoutManager) { - super(); - mContext = context; - mDatabase = new DBHelper(mContext); - mDatabase.setOnDatabaseChangedListener(this); - llm = linearLayoutManager; - } - - @Override public void onBindViewHolder(final RecordingsViewHolder holder, int position) { - - item = getItem(position); - long itemDuration = item.getLength(); - - long minutes = TimeUnit.MILLISECONDS.toMinutes(itemDuration); - long seconds = - TimeUnit.MILLISECONDS.toSeconds(itemDuration) - TimeUnit.MINUTES.toSeconds(minutes); - - holder.vName.setText(item.getName()); - holder.vLength.setText(String.format("%02d:%02d", minutes, seconds)); - holder.vDateAdded.setText(DateUtils.formatDateTime(mContext, item.getTime(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NUMERIC_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR)); - - // define an on click listener to open PlaybackFragment - holder.cardView.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View view) { - try { - PlaybackFragment playbackFragment = - new PlaybackFragment().newInstance(getItem(holder.getPosition())); - - FragmentTransaction transaction = - ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction(); - - playbackFragment.show(transaction, "dialog_playback"); - } catch (Exception e) { - Log.e(LOG_TAG, "exception", e); - } - } - }); - - holder.cardView.setOnLongClickListener(new View.OnLongClickListener() { - @Override public boolean onLongClick(View v) { - - ArrayList entrys = new ArrayList(); - entrys.add(mContext.getString(R.string.dialog_file_share)); - entrys.add(mContext.getString(R.string.dialog_file_rename)); - entrys.add(mContext.getString(R.string.dialog_file_delete)); - - final CharSequence[] items = entrys.toArray(new CharSequence[entrys.size()]); - - // File delete confirm - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(mContext.getString(R.string.dialog_title_options)); - builder.setItems(items, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int item) { - if (item == 0) { - shareFileDialog(holder.getPosition()); - } - if (item == 1) { - renameFileDialog(holder.getPosition()); - } else if (item == 2) { - deleteFileDialog(holder.getPosition()); - } - } - }); - builder.setCancelable(true); - builder.setNegativeButton(mContext.getString(R.string.dialog_action_cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - AlertDialog alert = builder.create(); - alert.show(); - - return false; - } - }); - } - - @Override public RecordingsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - - View itemView = LayoutInflater. - from(parent.getContext()). - inflate(R.layout.card_view, parent, false); - - mContext = parent.getContext(); - - return new RecordingsViewHolder(itemView); - } - - public static class RecordingsViewHolder extends RecyclerView.ViewHolder { - protected TextView vName; - protected TextView vLength; - protected TextView vDateAdded; - protected View cardView; - - public RecordingsViewHolder(View v) { - super(v); - vName = (TextView) v.findViewById(R.id.file_name_text); - vLength = (TextView) v.findViewById(R.id.file_length_text); - vDateAdded = (TextView) v.findViewById(R.id.file_date_added_text); - cardView = v.findViewById(R.id.card_view); - } - } - - @Override public int getItemCount() { - return mDatabase.getCount(); - } - - public RecordingItem getItem(int position) { - return mDatabase.getItemAt(position); - } - - @Override public void onNewDatabaseEntryAdded() { - //item added to top of the list - notifyItemInserted(getItemCount() - 1); - llm.scrollToPosition(getItemCount() - 1); - } - - @Override - //TODO - public void onDatabaseEntryRenamed() { - - } - - public void remove(int position) { - //remove item from database, recyclerview and storage - - //delete file from storage - File file = new File(getItem(position).getFilePath()); - file.delete(); - - Toast.makeText(mContext, - String.format(mContext.getString(R.string.toast_file_delete), getItem(position).getName()), - Toast.LENGTH_SHORT).show(); - - mDatabase.removeItemWithId(getItem(position).getId()); - notifyItemRemoved(position); - } - - //TODO - public void removeOutOfApp(String filePath) { - //user deletes a saved recording out of the application through another application - } - - public void rename(int position, String name) { - //rename a file - - String mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath(); - mFilePath += "/SoundRecorder/" + name; - File f = new File(mFilePath); - - if (f.exists() && !f.isDirectory()) { - //file name is not unique, cannot rename file. - Toast.makeText(mContext, String.format(mContext.getString(R.string.toast_file_exists), name), - Toast.LENGTH_SHORT).show(); - } else { - //file name is unique, rename file - File oldFilePath = new File(getItem(position).getFilePath()); - oldFilePath.renameTo(f); - mDatabase.renameItem(getItem(position), name, mFilePath); - notifyItemChanged(position); - } - } - - public void shareFileDialog(int position) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, - Uri.fromFile(new File(getItem(position).getFilePath()))); - shareIntent.setType("audio/mp4"); - mContext.startActivity(Intent.createChooser(shareIntent, mContext.getText(R.string.send_to))); - } - - public void renameFileDialog(final int position) { - // File rename dialog - AlertDialog.Builder renameFileBuilder = new AlertDialog.Builder(mContext); - - LayoutInflater inflater = LayoutInflater.from(mContext); - View view = inflater.inflate(R.layout.dialog_rename_file, null); - - final EditText input = (EditText) view.findViewById(R.id.new_name); - - renameFileBuilder.setTitle(mContext.getString(R.string.dialog_title_rename)); - renameFileBuilder.setCancelable(true); - renameFileBuilder.setPositiveButton(mContext.getString(R.string.dialog_action_ok), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - try { - String value = input.getText().toString().trim() + ".mp4"; - rename(position, value); - } catch (Exception e) { - Log.e(LOG_TAG, "exception", e); - } - - dialog.cancel(); - } - }); - renameFileBuilder.setNegativeButton(mContext.getString(R.string.dialog_action_cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - renameFileBuilder.setView(view); - AlertDialog alert = renameFileBuilder.create(); - alert.show(); - } - - public void deleteFileDialog(final int position) { - // File delete confirm - AlertDialog.Builder confirmDelete = new AlertDialog.Builder(mContext); - confirmDelete.setTitle(mContext.getString(R.string.dialog_title_delete)); - confirmDelete.setMessage(mContext.getString(R.string.dialog_text_delete)); - confirmDelete.setCancelable(true); - confirmDelete.setPositiveButton(mContext.getString(R.string.dialog_action_yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - try { - //remove item from database, recyclerview, and storage - remove(position); - } catch (Exception e) { - Log.e(LOG_TAG, "exception", e); - } - - dialog.cancel(); - } - }); - confirmDelete.setNegativeButton(mContext.getString(R.string.dialog_action_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - - AlertDialog alert = confirmDelete.create(); - alert.show(); - } -} diff --git a/app/src/main/java/in/arjsna/voicerecorder/fragments/FileViewerFragment.java b/app/src/main/java/in/arjsna/voicerecorder/fragments/FileViewerFragment.java deleted file mode 100644 index 82e8016..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/fragments/FileViewerFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -package in.arjsna.voicerecorder.fragments; - -import android.os.Bundle; -import android.os.FileObserver; -import android.support.v4.app.Fragment; -import android.support.v7.widget.DefaultItemAnimator; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.adapters.FileViewerAdapter; - -public class FileViewerFragment extends Fragment { - private static final String ARG_POSITION = "position"; - private static final String LOG_TAG = "FileViewerFragment"; - - private int position; - private FileViewerAdapter mFileViewerAdapter; - - public static FileViewerFragment newInstance(int position) { - FileViewerFragment f = new FileViewerFragment(); - Bundle b = new Bundle(); - b.putInt(ARG_POSITION, position); - f.setArguments(b); - - return f; - } - - @Override public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - position = getArguments().getInt(ARG_POSITION); - observer.startWatching(); - } - - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_file_viewer, container, false); - - RecyclerView mRecyclerView = (RecyclerView) v.findViewById(R.id.recyclerView); - mRecyclerView.setHasFixedSize(true); - LinearLayoutManager llm = new LinearLayoutManager(getActivity()); - llm.setOrientation(LinearLayoutManager.VERTICAL); - - //newest to oldest order (database stores from oldest to newest) - llm.setReverseLayout(true); - llm.setStackFromEnd(true); - - mRecyclerView.setLayoutManager(llm); - mRecyclerView.setItemAnimator(new DefaultItemAnimator()); - - mFileViewerAdapter = new FileViewerAdapter(getActivity(), llm); - mRecyclerView.setAdapter(mFileViewerAdapter); - - return v; - } - - FileObserver observer = new FileObserver( - android.os.Environment.getExternalStorageDirectory().toString() + "/SoundRecorder") { - // set up a file observer to watch this directory on sd card - @Override public void onEvent(int event, String file) { - if (event == FileObserver.DELETE) { - // user deletes a recording file out of the app - - String filePath = android.os.Environment.getExternalStorageDirectory().toString() - + "/SoundRecorder" - + file - + "]"; - - Log.d(LOG_TAG, "File deleted [" - + android.os.Environment.getExternalStorageDirectory().toString() - + "/SoundRecorder" - + file - + "]"); - - // remove file from database and recyclerview - mFileViewerAdapter.removeOutOfApp(filePath); - } - } - }; -} - - - - diff --git a/app/src/main/java/in/arjsna/voicerecorder/fragments/LicensesFragment.java b/app/src/main/java/in/arjsna/voicerecorder/fragments/LicensesFragment.java deleted file mode 100644 index 4adc39e..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/fragments/LicensesFragment.java +++ /dev/null @@ -1,23 +0,0 @@ -package in.arjsna.voicerecorder.fragments; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.view.LayoutInflater; -import android.view.View; -import in.arjsna.voicerecorder.R; - -public class LicensesFragment extends DialogFragment { - @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - LayoutInflater dialogInflater = getActivity().getLayoutInflater(); - View openSourceLicensesView = dialogInflater.inflate(R.layout.fragment_licenses, null); - - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); - dialogBuilder.setView(openSourceLicensesView) - .setTitle((getString(R.string.dialog_title_licenses))) - .setNeutralButton(android.R.string.ok, null); - - return dialogBuilder.create(); - } -} diff --git a/app/src/main/java/in/arjsna/voicerecorder/fragments/RecordFragment.java b/app/src/main/java/in/arjsna/voicerecorder/fragments/RecordFragment.java deleted file mode 100644 index c9b79c7..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/fragments/RecordFragment.java +++ /dev/null @@ -1,174 +0,0 @@ -package in.arjsna.voicerecorder.fragments; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Environment; -import android.os.SystemClock; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.Chronometer; -import android.widget.TextView; -import android.widget.Toast; -import com.melnykov.fab.FloatingActionButton; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.RecordingService; -import java.io.File; - -/** - * A simple {@link Fragment} subclass. - * Activities that contain this fragment must implement the - * to handle interaction events. - * Use the {@link RecordFragment#newInstance} factory method to - * create an instance of this fragment. - */ -public class RecordFragment extends Fragment { - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String ARG_POSITION = "position"; - private static final String LOG_TAG = RecordFragment.class.getSimpleName(); - - private int position; - - //Recording controls - private FloatingActionButton mRecordButton = null; - private Button mPauseButton = null; - - private TextView mRecordingPrompt; - private int mRecordPromptCount = 0; - - private boolean mStartRecording = true; - private boolean mPauseRecording = true; - - private Chronometer mChronometer = null; - long timeWhenPaused = 0; //stores time when user clicks pause button - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @return A new instance of fragment Record_Fragment. - */ - public static RecordFragment newInstance(int position) { - RecordFragment f = new RecordFragment(); - Bundle b = new Bundle(); - b.putInt(ARG_POSITION, position); - f.setArguments(b); - - return f; - } - - public RecordFragment() { - } - - @Override public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - position = getArguments().getInt(ARG_POSITION); - } - - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View recordView = inflater.inflate(R.layout.fragment_record, container, false); - - mChronometer = (Chronometer) recordView.findViewById(R.id.chronometer); - //update recording prompt text - mRecordingPrompt = (TextView) recordView.findViewById(R.id.recording_status_text); - - mRecordButton = (FloatingActionButton) recordView.findViewById(R.id.btnRecord); - mRecordButton.setColorNormal(getResources().getColor(R.color.primary)); - mRecordButton.setColorPressed(getResources().getColor(R.color.primary_dark)); - mRecordButton.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { - onRecord(mStartRecording); - mStartRecording = !mStartRecording; - } - }); - - mPauseButton = (Button) recordView.findViewById(R.id.btnPause); - mPauseButton.setVisibility(View.GONE); //hide pause button before recording starts - mPauseButton.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { - onPauseRecord(mPauseRecording); - mPauseRecording = !mPauseRecording; - } - }); - - return recordView; - } - - // Recording Start/Stop - //TODO: recording pause - private void onRecord(boolean start) { - - Intent intent = new Intent(getActivity(), RecordingService.class); - - if (start) { - // start recording - mRecordButton.setImageResource(R.drawable.ic_media_stop); - //mPauseButton.setVisibility(View.VISIBLE); - Toast.makeText(getActivity(), R.string.toast_recording_start, Toast.LENGTH_SHORT).show(); - File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder"); - if (!folder.exists()) { - //folder /SoundRecorder doesn't exist, create the folder - folder.mkdir(); - } - - //start Chronometer - mChronometer.setBase(SystemClock.elapsedRealtime()); - mChronometer.start(); - mChronometer.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() { - @Override public void onChronometerTick(Chronometer chronometer) { - if (mRecordPromptCount == 0) { - mRecordingPrompt.setText(getString(R.string.record_in_progress) + "."); - } else if (mRecordPromptCount == 1) { - mRecordingPrompt.setText(getString(R.string.record_in_progress) + ".."); - } else if (mRecordPromptCount == 2) { - mRecordingPrompt.setText(getString(R.string.record_in_progress) + "..."); - mRecordPromptCount = -1; - } - - mRecordPromptCount++; - } - }); - - //start RecordingService - getActivity().startService(intent); - //keep screen on while recording - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - mRecordingPrompt.setText(getString(R.string.record_in_progress) + "."); - mRecordPromptCount++; - } else { - //stop recording - mRecordButton.setImageResource(R.drawable.ic_mic_white_36dp); - //mPauseButton.setVisibility(View.GONE); - mChronometer.stop(); - mChronometer.setBase(SystemClock.elapsedRealtime()); - timeWhenPaused = 0; - mRecordingPrompt.setText(getString(R.string.record_prompt)); - - getActivity().stopService(intent); - //allow the screen to turn off again once recording is finished - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - - //TODO: implement pause recording - private void onPauseRecord(boolean pause) { - if (pause) { - //pause recording - mPauseButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_media_play, 0, 0, 0); - mRecordingPrompt.setText((String) getString(R.string.resume_recording_button).toUpperCase()); - timeWhenPaused = mChronometer.getBase() - SystemClock.elapsedRealtime(); - mChronometer.stop(); - } else { - //resume recording - mPauseButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_media_pause, 0, 0, 0); - mRecordingPrompt.setText((String) getString(R.string.pause_recording_button).toUpperCase()); - mChronometer.setBase(SystemClock.elapsedRealtime() + timeWhenPaused); - mChronometer.start(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/in/arjsna/voicerecorder/fragments/SettingsFragment.java b/app/src/main/java/in/arjsna/voicerecorder/fragments/SettingsFragment.java deleted file mode 100644 index 9357938..0000000 --- a/app/src/main/java/in/arjsna/voicerecorder/fragments/SettingsFragment.java +++ /dev/null @@ -1,40 +0,0 @@ -package in.arjsna.voicerecorder.fragments; - -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.support.annotation.Nullable; -import in.arjsna.voicerecorder.BuildConfig; -import in.arjsna.voicerecorder.MySharedPreferences; -import in.arjsna.voicerecorder.R; -import in.arjsna.voicerecorder.activities.SettingsActivity; - -public class SettingsFragment extends PreferenceFragment { - @Override public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.preferences); - - CheckBoxPreference highQualityPref = (CheckBoxPreference) findPreference( - getResources().getString(R.string.pref_high_quality_key)); - highQualityPref.setChecked(MySharedPreferences.getPrefHighQuality(getActivity())); - highQualityPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - MySharedPreferences.setPrefHighQuality(getActivity(), (boolean) newValue); - return true; - } - }); - - Preference aboutPref = findPreference(getString(R.string.pref_about_key)); - aboutPref.setSummary(getString(R.string.pref_about_desc, BuildConfig.VERSION_NAME)); - aboutPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override public boolean onPreferenceClick(Preference preference) { - LicensesFragment licensesFragment = new LicensesFragment(); - licensesFragment.show( - ((SettingsActivity) getActivity()).getSupportFragmentManager().beginTransaction(), - "dialog_licenses"); - return true; - } - }); - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_fileviewer.png b/app/src/main/res/drawable-hdpi/ic_fileviewer.png deleted file mode 100644 index 3943b5b..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_fileviewer.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index f8cabda..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_media_pause.png b/app/src/main/res/drawable-hdpi/ic_media_pause.png deleted file mode 100644 index 66c122f..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_media_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_media_play.png b/app/src/main/res/drawable-hdpi/ic_media_play.png deleted file mode 100644 index ec3c760..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_media_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_media_stop.png b/app/src/main/res/drawable-hdpi/ic_media_stop.png deleted file mode 100644 index 28181a1..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_media_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_mic_white_36dp.png b/app/src/main/res/drawable-hdpi/ic_mic_white_36dp.png deleted file mode 100644 index bc4a660..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_mic_white_36dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_fileviewer.png b/app/src/main/res/drawable-mdpi/ic_fileviewer.png deleted file mode 100644 index 9a34535..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_fileviewer.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index a15bc69..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_media_pause.png b/app/src/main/res/drawable-mdpi/ic_media_pause.png deleted file mode 100644 index eb88fca..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_media_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_media_play.png b/app/src/main/res/drawable-mdpi/ic_media_play.png deleted file mode 100644 index cca64c6..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_media_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_media_stop.png b/app/src/main/res/drawable-mdpi/ic_media_stop.png deleted file mode 100644 index 9a01578..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_media_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_mic_white_36dp.png b/app/src/main/res/drawable-mdpi/ic_mic_white_36dp.png deleted file mode 100644 index 56df87e..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_mic_white_36dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-v21/ic_audiotrack_grey.xml b/app/src/main/res/drawable-v21/ic_audiotrack_grey.xml new file mode 100644 index 0000000..aa0d7ec --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_audiotrack_grey.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_media_pause.xml b/app/src/main/res/drawable-v21/ic_media_pause.xml new file mode 100644 index 0000000..3bd88bc --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_media_pause.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_media_play.xml b/app/src/main/res/drawable-v21/ic_media_play.xml new file mode 100644 index 0000000..d2be36d --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_media_play.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_media_record.xml b/app/src/main/res/drawable-v21/ic_media_record.xml new file mode 100644 index 0000000..f70cc1e --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_media_record.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_media_stop.xml b/app/src/main/res/drawable-v21/ic_media_stop.xml new file mode 100644 index 0000000..4c559c7 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_media_stop.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_playlist_white.xml b/app/src/main/res/drawable-v21/ic_playlist_white.xml new file mode 100644 index 0000000..0f5e8db --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_playlist_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ic_settings_white.xml b/app/src/main/res/drawable-v21/ic_settings_white.xml new file mode 100644 index 0000000..fa2e5c4 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_settings_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-v21/ripple.xml b/app/src/main/res/drawable-v21/ripple.xml new file mode 100644 index 0000000..8cdf466 --- /dev/null +++ b/app/src/main/res/drawable-v21/ripple.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_fileviewer.png b/app/src/main/res/drawable-xhdpi/ic_fileviewer.png deleted file mode 100644 index 2146873..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_fileviewer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 9566bea..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_media_pause.png b/app/src/main/res/drawable-xhdpi/ic_media_pause.png deleted file mode 100644 index e891e5b..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_media_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_media_play.png b/app/src/main/res/drawable-xhdpi/ic_media_play.png deleted file mode 100644 index 8203596..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_media_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_media_stop.png b/app/src/main/res/drawable-xhdpi/ic_media_stop.png deleted file mode 100644 index 52de442..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_media_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_mic_white_36dp.png b/app/src/main/res/drawable-xhdpi/ic_mic_white_36dp.png deleted file mode 100644 index dda6906..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_mic_white_36dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fileviewer.png b/app/src/main/res/drawable-xxhdpi/ic_fileviewer.png deleted file mode 100644 index 7f500f3..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fileviewer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 9e9e43e..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_media_pause.png b/app/src/main/res/drawable-xxhdpi/ic_media_pause.png deleted file mode 100644 index d996334..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_media_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_media_play.png b/app/src/main/res/drawable-xxhdpi/ic_media_play.png deleted file mode 100644 index f7a5872..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_media_play.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_media_stop.png b/app/src/main/res/drawable-xxhdpi/ic_media_stop.png deleted file mode 100644 index a15a681..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_media_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_white_36dp.png b/app/src/main/res/drawable-xxhdpi/ic_mic_white_36dp.png deleted file mode 100644 index 70c5d68..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_mic_white_36dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fileviewer.png b/app/src/main/res/drawable-xxxhdpi/ic_fileviewer.png deleted file mode 100644 index 05754e9..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_fileviewer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png deleted file mode 100644 index 69446e3..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_mic_white_36dp.png b/app/src/main/res/drawable-xxxhdpi/ic_mic_white_36dp.png deleted file mode 100644 index 977f7bf..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_mic_white_36dp.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_audiotrack_grey.xml b/app/src/main/res/drawable/ic_audiotrack_grey.xml new file mode 100644 index 0000000..aa0d7ec --- /dev/null +++ b/app/src/main/res/drawable/ic_audiotrack_grey.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_media_pause.xml b/app/src/main/res/drawable/ic_media_pause.xml new file mode 100644 index 0000000..3bd88bc --- /dev/null +++ b/app/src/main/res/drawable/ic_media_pause.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_media_play.xml b/app/src/main/res/drawable/ic_media_play.xml new file mode 100644 index 0000000..d2be36d --- /dev/null +++ b/app/src/main/res/drawable/ic_media_play.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_media_record.xml b/app/src/main/res/drawable/ic_media_record.xml new file mode 100644 index 0000000..f70cc1e --- /dev/null +++ b/app/src/main/res/drawable/ic_media_record.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_media_stop.xml b/app/src/main/res/drawable/ic_media_stop.xml new file mode 100644 index 0000000..4c559c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_media_stop.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_playlist_white.xml b/app/src/main/res/drawable/ic_playlist_white.xml new file mode 100644 index 0000000..0f5e8db --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings_white.xml b/app/src/main/res/drawable/ic_settings_white.xml new file mode 100644 index 0000000..fa2e5c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_white.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/icon_web.png b/app/src/main/res/drawable/icon_web.png deleted file mode 100644 index 07dec46..0000000 Binary files a/app/src/main/res/drawable/icon_web.png and /dev/null differ diff --git a/app/src/main/res/drawable/record_progress_bar.xml b/app/src/main/res/drawable/record_progress_bar.xml deleted file mode 100644 index 59892fd..0000000 --- a/app/src/main/res/drawable/record_progress_bar.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/record_progress_bar_background.xml b/app/src/main/res/drawable/record_progress_bar_background.xml deleted file mode 100644 index fb9906b..0000000 --- a/app/src/main/res/drawable/record_progress_bar_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/seekbar_progress.xml b/app/src/main/res/drawable/seekbar_progress.xml deleted file mode 100644 index 6ae22e8..0000000 --- a/app/src/main/res/drawable/seekbar_progress.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9fd19e4..419d4bf 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,38 +1,8 @@ - - - - - - - - - - - + diff --git a/app/src/main/res/layout/activity_preferences.xml b/app/src/main/res/layout/activity_preferences.xml index 8082627..bba5b5d 100644 --- a/app/src/main/res/layout/activity_preferences.xml +++ b/app/src/main/res/layout/activity_preferences.xml @@ -1,9 +1,12 @@ - + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_record_list.xml b/app/src/main/res/layout/activity_record_list.xml new file mode 100644 index 0000000..7320425 --- /dev/null +++ b/app/src/main/res/layout/activity_record_list.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout/card_view.xml b/app/src/main/res/layout/card_view.xml index 0ec48c4..1d8a352 100644 --- a/app/src/main/res/layout/card_view.xml +++ b/app/src/main/res/layout/card_view.xml @@ -1,68 +1,78 @@ + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" +> - - - - - + - - - - - + - - + - + - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_color_picker.xml b/app/src/main/res/layout/dialog_color_picker.xml new file mode 100644 index 0000000..1969fd9 --- /dev/null +++ b/app/src/main/res/layout/dialog_color_picker.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_rename_file.xml b/app/src/main/res/layout/dialog_rename_file.xml index d92ac78..44dcd3f 100644 --- a/app/src/main/res/layout/dialog_rename_file.xml +++ b/app/src/main/res/layout/dialog_rename_file.xml @@ -1,20 +1,16 @@ - - - - + android:padding="20dp" + > + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_file_viewer.xml b/app/src/main/res/layout/fragment_file_viewer.xml index 4abb583..bfc00c6 100644 --- a/app/src/main/res/layout/fragment_file_viewer.xml +++ b/app/src/main/res/layout/fragment_file_viewer.xml @@ -1,13 +1,21 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" +> + + - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_licenses.xml b/app/src/main/res/layout/fragment_licenses.xml deleted file mode 100644 index 4094599..0000000 --- a/app/src/main/res/layout/fragment_licenses.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_playback.xml b/app/src/main/res/layout/fragment_media_playback.xml index f1a82d7..a410384 100644 --- a/app/src/main/res/layout/fragment_media_playback.xml +++ b/app/src/main/res/layout/fragment_media_playback.xml @@ -1,85 +1,92 @@ + android:layout_height="wrap_content" +> - + + - - + android:layout_margin="7dp" + android:orientation="vertical" + > - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_record.xml b/app/src/main/res/layout/fragment_record.xml index f5f774b..cd918cf 100644 --- a/app/src/main/res/layout/fragment_record.xml +++ b/app/src/main/res/layout/fragment_record.xml @@ -1,71 +1,96 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipChildren="false" +> - + - + - + -