diff --git a/app/build.gradle b/app/build.gradle index a8742302..ca5817e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,13 @@ android { compileSdkVersion 29 buildToolsVersion '29.0.2' + + repositories { + maven { + url "https://jitpack.io" + } + } + defaultConfig { applicationId 'by.naxa.soundrecorder' minSdkVersion 16 @@ -54,4 +61,6 @@ dependencies { // Firebase implementation 'com.google.firebase:firebase-analytics:17.2.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' + compile 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' + } diff --git a/app/lib/.gitignore b/app/lib/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/lib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/lib/build.gradle b/app/lib/build.gradle new file mode 100644 index 00000000..47d4389d --- /dev/null +++ b/app/lib/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 24 + buildToolsVersion "24.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile 'com.github.adrielcafe:ffmpeg-android-java:2a627f6ecd@aar' +} \ No newline at end of file diff --git a/app/lib/proguard-rules.pro b/app/lib/proguard-rules.pro new file mode 100644 index 00000000..f83ae791 --- /dev/null +++ b/app/lib/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/adrielcafe/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/lib/src/main/AndroidManifest.xml b/app/lib/src/main/AndroidManifest.xml new file mode 100644 index 00000000..481879ec --- /dev/null +++ b/app/lib/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/lib/src/main/java/cafe/adriel/androidaudioconverter/AndroidAudioConverter.java b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/AndroidAudioConverter.java new file mode 100644 index 00000000..95bf2268 --- /dev/null +++ b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/AndroidAudioConverter.java @@ -0,0 +1,135 @@ +package cafe.adriel.androidaudioconverter; + +import android.content.Context; + +import com.github.hiteshsondhi88.libffmpeg.FFmpeg; +import com.github.hiteshsondhi88.libffmpeg.FFmpegExecuteResponseHandler; +import com.github.hiteshsondhi88.libffmpeg.FFmpegLoadBinaryResponseHandler; + +import java.io.File; +import java.io.IOException; + +import cafe.adriel.androidaudioconverter.callback.IConvertCallback; +import cafe.adriel.androidaudioconverter.callback.ILoadCallback; +import cafe.adriel.androidaudioconverter.model.AudioFormat; + +public class AndroidAudioConverter { + + private static boolean loaded; + + private Context context; + private File audioFile; + private AudioFormat format; + private IConvertCallback callback; + + private AndroidAudioConverter(Context context){ + this.context = context; + } + + public static boolean isLoaded(){ + return loaded; + } + + public static void load(Context context, final ILoadCallback callback){ + try { + FFmpeg.getInstance(context).loadBinary(new FFmpegLoadBinaryResponseHandler() { + @Override + public void onStart() { + + } + + @Override + public void onSuccess() { + loaded = true; + callback.onSuccess(); + } + + @Override + public void onFailure() { + loaded = false; + callback.onFailure(new Exception("Failed to loaded FFmpeg lib")); + } + + @Override + public void onFinish() { + + } + }); + } catch (Exception e){ + loaded = false; + callback.onFailure(e); + } + } + + public static AndroidAudioConverter with(Context context) { + return new AndroidAudioConverter(context); + } + + public AndroidAudioConverter setFile(File originalFile) { + this.audioFile = originalFile; + return this; + } + + public AndroidAudioConverter setFormat(AudioFormat format) { + this.format = format; + return this; + } + + public AndroidAudioConverter setCallback(IConvertCallback callback) { + this.callback = callback; + return this; + } + + public void convert() { + if(!isLoaded()){ + callback.onFailure(new Exception("FFmpeg not loaded")); + return; + } + if(audioFile == null || !audioFile.exists()){ + callback.onFailure(new IOException("File not exists")); + return; + } + if(!audioFile.canRead()){ + callback.onFailure(new IOException("Can't read the file. Missing permission?")); + return; + } + final File convertedFile = getConvertedFile(audioFile, format); + final String[] cmd = new String[]{"-y", "-i", audioFile.getPath(), convertedFile.getPath()}; + try { + FFmpeg.getInstance(context).execute(cmd, new FFmpegExecuteResponseHandler() { + @Override + public void onStart() { + + } + + @Override + public void onProgress(String message) { + + } + + @Override + public void onSuccess(String message) { + callback.onSuccess(convertedFile); + } + + @Override + public void onFailure(String message) { + callback.onFailure(new IOException(message)); + } + + @Override + public void onFinish() { + + } + }); + } catch (Exception e){ + callback.onFailure(e); + } + } + + private static File getConvertedFile(File originalFile, AudioFormat format){ + String[] f = originalFile.getPath().split("\\."); + String filePath = originalFile.getPath().replace(f[f.length - 1], format.getFormat()); + return new File(filePath); + } +} \ No newline at end of file diff --git a/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/IConvertCallback.java b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/IConvertCallback.java new file mode 100644 index 00000000..e007f320 --- /dev/null +++ b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/IConvertCallback.java @@ -0,0 +1,11 @@ +package cafe.adriel.androidaudioconverter.callback; + +import java.io.File; + +public interface IConvertCallback { + + void onSuccess(File convertedFile); + + void onFailure(Exception error); + +} \ No newline at end of file diff --git a/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/ILoadCallback.java b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/ILoadCallback.java new file mode 100644 index 00000000..ccb4ad77 --- /dev/null +++ b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/callback/ILoadCallback.java @@ -0,0 +1,9 @@ +package cafe.adriel.androidaudioconverter.callback; + +public interface ILoadCallback { + + void onSuccess(); + + void onFailure(Exception error); + +} \ No newline at end of file diff --git a/app/lib/src/main/java/cafe/adriel/androidaudioconverter/model/AudioFormat.java b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/model/AudioFormat.java new file mode 100644 index 00000000..4bc2ac86 --- /dev/null +++ b/app/lib/src/main/java/cafe/adriel/androidaudioconverter/model/AudioFormat.java @@ -0,0 +1,14 @@ +package cafe.adriel.androidaudioconverter.model; + +public enum AudioFormat { + AAC, + MP3, + M4A, + WMA, + WAV, + FLAC; + + public String getFormat() { + return name().toLowerCase(); + } +} \ No newline at end of file diff --git a/app/lib/src/main/res/values/strings.xml b/app/lib/src/main/res/values/strings.xml new file mode 100644 index 00000000..da0b9ffa --- /dev/null +++ b/app/lib/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AndroidAudioConverter + \ No newline at end of file diff --git a/app/src/main/java/by/naxa/soundrecorder/activities/MainActivity.java b/app/src/main/java/by/naxa/soundrecorder/activities/MainActivity.java index 9e9a40d3..d84dd760 100644 --- a/app/src/main/java/by/naxa/soundrecorder/activities/MainActivity.java +++ b/app/src/main/java/by/naxa/soundrecorder/activities/MainActivity.java @@ -34,6 +34,8 @@ import by.naxa.soundrecorder.fragments.FileViewerFragment; import by.naxa.soundrecorder.fragments.RecordFragment; import by.naxa.soundrecorder.util.EventBroadcaster; +import cafe.adriel.androidaudioconverter.AndroidAudioConverter; +import cafe.adriel.androidaudioconverter.callback.ILoadCallback; public class MainActivity extends AppCompatActivity { @@ -45,6 +47,20 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + + AndroidAudioConverter.load(this, new ILoadCallback() { + @Override + public void onSuccess() { + // Great! + } + @Override + public void onFailure(Exception error) { + // FFmpeg is not supported by device + } + }); + + if (SoundRecorderApplication.getInstance().isNightModeEnabled()) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); //For night mode theme } else { diff --git a/app/src/main/java/by/naxa/soundrecorder/adapters/FileViewerAdapter.java b/app/src/main/java/by/naxa/soundrecorder/adapters/FileViewerAdapter.java index c5d2e6d4..a69a392b 100644 --- a/app/src/main/java/by/naxa/soundrecorder/adapters/FileViewerAdapter.java +++ b/app/src/main/java/by/naxa/soundrecorder/adapters/FileViewerAdapter.java @@ -30,11 +30,15 @@ import androidx.fragment.app.FragmentTransaction; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + +import org.w3c.dom.Text; + import by.naxa.soundrecorder.BuildConfig; import by.naxa.soundrecorder.DBHelper; import by.naxa.soundrecorder.R; import by.naxa.soundrecorder.RecordingItem; import by.naxa.soundrecorder.fragments.PlaybackFragment; +import by.naxa.soundrecorder.fragments.SettingsFragment; import by.naxa.soundrecorder.listeners.OnDatabaseChangedListener; import by.naxa.soundrecorder.listeners.OnSingleClickListener; import by.naxa.soundrecorder.util.EventBroadcaster; @@ -238,6 +242,7 @@ public void removeOutOfApp(String filePath) { * rename a file */ public void rename(int position, String name) { + final String mFilePath = Paths.combine( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), Paths.SOUND_RECORDER_FOLDER, name); @@ -275,11 +280,17 @@ private void shareFileDialog(int position) { } 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); + TextView text = (TextView)view.findViewById(R.id.textView); + text.setText(SettingsFragment.getFormat()); + + // TextView textView = (TextView) view.findViewById(R.id.textView); + // textView.setText(SettingsFragment.getFormat()); final TextInputEditText input = view.findViewById(R.id.new_name); @@ -292,7 +303,8 @@ public void onClick(DialogInterface dialog, int id) { final Editable editable = input.getText(); if (editable == null) return; - final String value = editable.toString().trim() + ".mp4"; + + final String value = editable.toString().trim() + SettingsFragment.getFormat(); rename(position, value); } catch (Exception e) { if (Fabric.isInitialized()) Crashlytics.logException(e); diff --git a/app/src/main/java/by/naxa/soundrecorder/fragments/RecordFragment.java b/app/src/main/java/by/naxa/soundrecorder/fragments/RecordFragment.java index 1ea3819a..e1599af6 100644 --- a/app/src/main/java/by/naxa/soundrecorder/fragments/RecordFragment.java +++ b/app/src/main/java/by/naxa/soundrecorder/fragments/RecordFragment.java @@ -20,6 +20,7 @@ import android.view.ViewGroup; import android.widget.Chronometer; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,6 +46,9 @@ import by.naxa.soundrecorder.util.Paths; import by.naxa.soundrecorder.util.PermissionsHelper; import by.naxa.soundrecorder.util.ScreenLock; +import cafe.adriel.androidaudioconverter.AndroidAudioConverter; +import cafe.adriel.androidaudioconverter.callback.IConvertCallback; +import cafe.adriel.androidaudioconverter.model.AudioFormat; /** * A simple {@link Fragment} subclass. @@ -329,7 +333,42 @@ private void updateUI(RecorderState state, long chronometerBaseTime) { /** * Stop recording */ + + public void convertAudio(){ + /** + * Update with a valid audio file! + * Supported formats: {@link AndroidAudioConverter.AudioFormat} + */ + String path = Paths.combine( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), + Paths.SOUND_RECORDER_FOLDER, "My Recording_1.mp4"); + + File source = new File(path); + + IConvertCallback callback = new IConvertCallback() { + @Override + public void onSuccess(File convertedFile) { + Toast.makeText(getContext(), "SUCCESS: " + convertedFile.getPath(), Toast.LENGTH_LONG).show(); + } + @Override + public void onFailure(Exception error) { + Toast.makeText(getContext(), "ERROR: " + error.getMessage(), Toast.LENGTH_LONG).show(); + } + }; + + + Toast.makeText(getContext(), "Converting audio file...", Toast.LENGTH_SHORT).show(); + AndroidAudioConverter.with(getContext()) + .setFile(source) + .setFormat(AudioFormat.MP3) + .setCallback(callback) + .convert(); + } + + private void stopRecording() { + + final FragmentActivity activity = getActivity(); if (activity == null) { Log.wtf(LOG_TAG, "RecordFragment failed to stop recording, getActivity() returns null."); diff --git a/app/src/main/java/by/naxa/soundrecorder/fragments/SettingsFragment.java b/app/src/main/java/by/naxa/soundrecorder/fragments/SettingsFragment.java index 993aa170..fc0a28b5 100644 --- a/app/src/main/java/by/naxa/soundrecorder/fragments/SettingsFragment.java +++ b/app/src/main/java/by/naxa/soundrecorder/fragments/SettingsFragment.java @@ -1,10 +1,15 @@ package by.naxa.soundrecorder.fragments; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; import android.preference.SwitchPreference; +import android.widget.CheckBox; import android.widget.Toast; import androidx.annotation.Nullable; @@ -21,14 +26,31 @@ * Created by Daniel on 5/22/2017. */ public class SettingsFragment extends PreferenceFragment { + private static String format = ".mp4"; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); + final ListPreference listPreferenceCategory = (ListPreference) findPreference("pref_list"); + + + listPreferenceCategory.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + format = newValue.toString(); + Toast.makeText(getActivity(), "Selected: "+format, Toast.LENGTH_LONG).show(); + + return true; + } + }); + + final CheckBoxPreference highQualityPref = (CheckBoxPreference) findPreference( getResources().getString(R.string.pref_high_quality_key)); + highQualityPref.setChecked(MySharedPreferences.getPrefHighQuality(getActivity())); highQualityPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override @@ -62,7 +84,7 @@ public boolean onPreferenceClick(Preference preference) { darkModePref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if(darkModePref.isChecked()) { + if (darkModePref.isChecked()) { SoundRecorderApplication.getInstance().setIsNightModeEnabled(false); Toast.makeText(getActivity(), "Dark Mode is OFF", Toast.LENGTH_SHORT).show(); darkModePref.setChecked(false); @@ -77,4 +99,8 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { } }); } + + public static String getFormat() { + return format; + } } diff --git a/app/src/main/java/by/naxa/soundrecorder/services/RecordingService.java b/app/src/main/java/by/naxa/soundrecorder/services/RecordingService.java index 73447df0..5296100c 100644 --- a/app/src/main/java/by/naxa/soundrecorder/services/RecordingService.java +++ b/app/src/main/java/by/naxa/soundrecorder/services/RecordingService.java @@ -32,12 +32,16 @@ import by.naxa.soundrecorder.DBHelper; import by.naxa.soundrecorder.R; import by.naxa.soundrecorder.RecorderState; +import by.naxa.soundrecorder.fragments.SettingsFragment; import by.naxa.soundrecorder.util.Command; import by.naxa.soundrecorder.util.EventBroadcaster; import by.naxa.soundrecorder.util.MyIntentBuilder; import by.naxa.soundrecorder.util.MySharedPreferences; import by.naxa.soundrecorder.util.NotificationCompatPie; import by.naxa.soundrecorder.util.Paths; +import cafe.adriel.androidaudioconverter.AndroidAudioConverter; +import cafe.adriel.androidaudioconverter.callback.IConvertCallback; +import cafe.adriel.androidaudioconverter.model.AudioFormat; import io.fabric.sdk.android.Fabric; /** @@ -46,10 +50,12 @@ public class RecordingService extends Service { private static final String LOG_TAG = "RecordingService"; + cafe.adriel.androidaudioconverter.model.AudioFormat a; private String mFileName = null; private String mFilePath = null; + private MediaRecorder mRecorder = null; private DBHelper mDatabase; @@ -57,6 +63,7 @@ public class RecordingService extends Service { private long mStartingTimeMillis = 0; private long mElapsedMillis = 0; + private volatile RecorderState state = RecorderState.STOPPED; private int tempFileCount = 0; @@ -66,6 +73,10 @@ public class RecordingService extends Service { // Binder given to clients private final IBinder mBinder = new LocalBinder(); + public RecordingService() { + + } + @Override public IBinder onBind(Intent intent) { @@ -141,23 +152,30 @@ public void stopService() { stopSelf(); } + @Override public void onDestroy() { if (mRecorder != null) { + stopRecording(); + } + super.onDestroy(); } public void setFileNameAndPath(boolean isFilePathTemp) { + if (isFilePathTemp) { - mFileName = getString(R.string.default_file_name) + (++tempFileCount) + "_" + ".tmp"; + mFileName = getString(R.string.default_file_name) + (++tempFileCount) + "_" + SettingsFragment.getFormat(); Paths.createDirectory(getExternalCacheDir(), Paths.SOUND_RECORDER_FOLDER); mFilePath = Paths.combine( getExternalCacheDir(), Paths.SOUND_RECORDER_FOLDER, mFileName); } else { + + int count = 0; File f; @@ -165,7 +183,7 @@ public void setFileNameAndPath(boolean isFilePathTemp) { ++count; mFileName = - getString(R.string.default_file_name) + "_" + (mDatabase.getCount() + count) + ".mp4"; + getString(R.string.default_file_name) + "_" + (mDatabase.getCount() + count) + SettingsFragment.getFormat(); mFilePath = Paths.combine( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), @@ -180,20 +198,29 @@ public void setFileNameAndPath(boolean isFilePathTemp) { * Start or resume sound recording. */ public void startRecording() { + if (state == RecorderState.RECORDING || state == RecorderState.PREPARING) return; changeStateTo(RecorderState.PREPARING); boolean isTemporary = true; setFileNameAndPath(isTemporary); - // Configure the MediaRecorder for a new recording mRecorder = new MediaRecorder(); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + + if (SettingsFragment.getFormat().equals(".amr")) { + + mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + } else { + + mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + } mRecorder.setOutputFile(mFilePath); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mRecorder.setAudioChannels(1); + if (MySharedPreferences.getPrefHighQuality(this)) { mRecorder.setAudioSamplingRate(44100); mRecorder.setAudioEncodingBitRate(192000); @@ -244,7 +271,33 @@ public void pauseRecording() { } } + public void convertAudio() { + + File source = new File(mFilePath); + + IConvertCallback callback = new IConvertCallback() { + @Override + public void onSuccess(File convertedFile) { + Toast.makeText(getApplicationContext(), "SUCCESS: " + convertedFile.getPath(), Toast.LENGTH_LONG).show(); + } + + @Override + public void onFailure(Exception error) { + Toast.makeText(getApplicationContext(), "ERROR: " + error.getMessage(), Toast.LENGTH_LONG).show(); + } + }; + + Toast.makeText(getApplicationContext(), "Converting...", Toast.LENGTH_SHORT).show(); + AndroidAudioConverter.with(getApplicationContext()) + .setFile(source) + .setFormat(AudioFormat.MP3) + .setCallback(callback) + .convert(); + } + + public void stopRecording() { + if (state == RecorderState.STOPPED) { Log.wtf(LOG_TAG, "stopRecording: already STOPPED."); return; @@ -257,7 +310,10 @@ public void stopRecording() { filesPaused.add(mFilePath); boolean isTemporary = false; + + setFileNameAndPath(isTemporary); + String pathToSend = ""; try { if (stateBefore != RecorderState.PAUSED) { @@ -287,12 +343,19 @@ public void stopRecording() { } } + try { mDatabase.addRecording(mFileName, mFilePath, mElapsedMillis); } catch (Exception e) { if (Fabric.isInitialized()) Crashlytics.logException(e); Log.e(LOG_TAG, "exception", e); } + + + if (SettingsFragment.getFormat().equals(".mp3")) { + convertAudio(); + + } } /** diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml new file mode 100644 index 00000000..a504d10c --- /dev/null +++ b/app/src/main/res/values/array.xml @@ -0,0 +1,28 @@ + + + + + MP4 + + + MP3(converted) + + + AMR + + + + + + + + .mp4 + + + .mp3 + + + .amr + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fee3b2d..2e47b55c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,15 @@ Yes, delete No + + Settings + Android Example - ListPreference + + + userName + foregroundColor + backgroundColor + No saved recordings Pause Resume diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index f1ddd32f..8bb2c6df 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -9,12 +9,23 @@ android:title="@string/pref_high_quality_title" android:summary="@string/pref_high_quality_desc"/> + + + +