diff --git a/app/build.gradle b/app/build.gradle index ec57c308..1abe8502 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ ext.versions = [ ] def versionMajor = 0 -def versionMinor = 5 -def versionPatch = 18 +def versionMinor = 6 +def versionPatch = 2 android { compileSdkVersion versions.targetSdkVersion @@ -59,9 +59,9 @@ android { buildTypes { release { - minifyEnabled false - shrinkResources false - useProguard false + minifyEnabled true + shrinkResources true + useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { @@ -98,9 +98,9 @@ android { } } -//repositories { -// maven { url 'https://maven.fabric.io/public' } -//} +repositories { + maven { url 'https://maven.fabric.io/public' } +} // Remove not needed buildVariants. android.variantFilter { variant -> diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..80ef6edc 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,5 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keep class com.dimowner.audiorecorder.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 958bf45e..92c10ec6 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,13 +39,15 @@ android:screenOrientation="portrait"/> + - + diff --git a/app/src/main/java/com/dimowner/audiorecorder/AppConstants.java b/app/src/main/java/com/dimowner/audiorecorder/AppConstants.java index 0fa3b78c..8b24cd1c 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/AppConstants.java +++ b/app/src/main/java/com/dimowner/audiorecorder/AppConstants.java @@ -36,6 +36,8 @@ private AppConstants() {} public static final int RECORDING_FORMAT_M4A = 0; public static final int RECORDING_FORMAT_WAV = 1; + public static final int DEFAULT_PER_PAGE = 50; + //BEGINNING-------------- Waveform visualisation constants ---------------------------------- /** Density pixel count per one second of time. @@ -73,6 +75,10 @@ private AppConstants() {} public final static int RECORD_ENCODING_BITRATE_128000 = 128000; public final static int RECORD_ENCODING_BITRATE_192000 = 192000; + public static final int SORT_DATE = 1; + public static final int SORT_NAME = 2; + public static final int SORT_DURATION = 3; + // public final static int RECORD_AUDIO_CHANNELS_COUNT = 2; public final static int RECORD_AUDIO_MONO = 1; public final static int RECORD_AUDIO_STEREO = 2; diff --git a/app/src/main/java/com/dimowner/audiorecorder/Contract.java b/app/src/main/java/com/dimowner/audiorecorder/Contract.java index 3bf56ec5..60f962b0 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/Contract.java +++ b/app/src/main/java/com/dimowner/audiorecorder/Contract.java @@ -26,6 +26,8 @@ interface View { void showError(String message); void showError(int resId); + + void showMessage(int resId); } interface UserActionsListener { diff --git a/app/src/main/java/com/dimowner/audiorecorder/Injector.java b/app/src/main/java/com/dimowner/audiorecorder/Injector.java index c2559ab8..d1eeb060 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/Injector.java +++ b/app/src/main/java/com/dimowner/audiorecorder/Injector.java @@ -47,6 +47,7 @@ public class Injector { private BackgroundQueue recordingTasks; private BackgroundQueue importTasks; private BackgroundQueue processingTasks; + private BackgroundQueue copyTasks; private MainContract.UserActionsListener mainPresenter; private RecordsContract.UserActionsListener recordsPresenter; @@ -105,6 +106,13 @@ public BackgroundQueue provideProcessingTasksQueue() { return processingTasks; } + public BackgroundQueue provideCopyTasksQueue() { + if (copyTasks == null) { + copyTasks = new BackgroundQueue("CopyTasks"); + } + return copyTasks; + } + public ColorMap provideColorMap() { return ColorMap.getInstance(providePrefs()); } @@ -133,8 +141,8 @@ public MainContract.UserActionsListener provideMainPresenter() { public RecordsContract.UserActionsListener provideRecordsPresenter() { if (recordsPresenter == null) { recordsPresenter = new RecordsPresenter(provideLocalRepository(), provideFileRepository(), - provideLoadingTasksQueue(), provideRecordingTasksQueue(), provideAudioPlayer(), - provideAppRecorder(), providePrefs()); + provideLoadingTasksQueue(), provideRecordingTasksQueue(), provideCopyTasksQueue(), + provideAudioPlayer(), provideAppRecorder(), providePrefs()); } return recordsPresenter; } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/AppRecorderImpl.java b/app/src/main/java/com/dimowner/audiorecorder/app/AppRecorderImpl.java index b6857744..38c87db4 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/AppRecorderImpl.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/AppRecorderImpl.java @@ -7,12 +7,12 @@ import com.dimowner.audiorecorder.data.Prefs; import com.dimowner.audiorecorder.data.database.LocalRepository; import com.dimowner.audiorecorder.exception.AppException; +import com.dimowner.audiorecorder.exception.CantProcessRecord; import com.dimowner.audiorecorder.util.AndroidUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import timber.log.Timber; @@ -113,7 +113,12 @@ public void run() { onRecordFinishProcessing(); } }); - } catch (IOException e) { + } catch (IOException | OutOfMemoryError e) { + AndroidUtils.runOnUIThread(new Runnable() { + @Override public void run() { + onError(new CantProcessRecord()); + } + }); Timber.e(e); } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/info/ActivityInformation.java b/app/src/main/java/com/dimowner/audiorecorder/app/info/ActivityInformation.java new file mode 100644 index 00000000..a6e7d9eb --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/info/ActivityInformation.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019 Dmitriy Ponomarenko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dimowner.audiorecorder.app.info; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.dimowner.audiorecorder.ARApplication; +import com.dimowner.audiorecorder.ColorMap; +import com.dimowner.audiorecorder.R; +import com.dimowner.audiorecorder.util.AndroidUtils; +import com.dimowner.audiorecorder.util.TimeUtils; + +public class ActivityInformation extends Activity { + + private static final String KEY_NAME = "pref_name"; + private static final String KEY_FORMAT = "pref_format"; + private static final String KEY_DURATION = "pref_duration"; + private static final String KEY_SIZE = "pref_size"; + private static final String KEY_LOCATION = "pref_location"; + + private ColorMap colorMap; + + + public static Intent getStartIntent(Context context, String name, String format, long duration, long size, String location) { + Intent intent = new Intent(context, ActivityInformation.class); + intent.putExtra(KEY_NAME, name); + intent.putExtra(KEY_FORMAT, format); + intent.putExtra(KEY_DURATION, duration); + intent.putExtra(KEY_SIZE, size); + intent.putExtra(KEY_LOCATION, location); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + colorMap = ARApplication.getInjector().provideColorMap(); + setTheme(colorMap.getAppThemeResource()); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_info); + + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + LinearLayout toolbar = findViewById(R.id.toolbar); + toolbar.setPadding(0, AndroidUtils.getStatusBarHeight(getApplicationContext()), 0, 0); + + Bundle extras = getIntent().getExtras(); + TextView txtName = findViewById(R.id.txt_name); + TextView txtFormat = findViewById(R.id.txt_format); + TextView txtDuration = findViewById(R.id.txt_duration); + TextView txtSize = findViewById(R.id.txt_size); + TextView txtLocation = findViewById(R.id.txt_location); + + if (extras != null) { + if (extras.containsKey(KEY_NAME)) { + txtName.setText(extras.getString(KEY_NAME)); + } + if (extras.containsKey(KEY_FORMAT)) { + txtFormat.setText(extras.getString(KEY_FORMAT)); + } + if (extras.containsKey(KEY_DURATION)) { + txtDuration.setText(TimeUtils.formatTimeIntervalHourMinSec2(extras.getLong(KEY_DURATION))); + } + if (extras.containsKey(KEY_SIZE)) { + txtSize.setText(AndroidUtils.formatSize(extras.getLong(KEY_SIZE))); + } + if (extras.containsKey(KEY_LOCATION)) { + txtLocation.setText(extras.getString(KEY_LOCATION)); + } + } + + findViewById(R.id.btn_back).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/main/MainActivity.java b/app/src/main/java/com/dimowner/audiorecorder/app/main/MainActivity.java index 6b5088e7..cbdc37ee 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/main/MainActivity.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/main/MainActivity.java @@ -26,14 +26,15 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; -import android.support.v4.content.FileProvider; +import android.support.annotation.NonNull; import android.text.Editable; import android.text.TextWatcher; import android.util.TypedValue; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -41,6 +42,7 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; @@ -52,6 +54,7 @@ import com.dimowner.audiorecorder.R; import com.dimowner.audiorecorder.app.PlaybackService; import com.dimowner.audiorecorder.app.RecordingService; +import com.dimowner.audiorecorder.app.info.ActivityInformation; import com.dimowner.audiorecorder.app.records.RecordsActivity; import com.dimowner.audiorecorder.app.settings.SettingsActivity; import com.dimowner.audiorecorder.app.widget.WaveformView; @@ -69,18 +72,18 @@ public class MainActivity extends Activity implements MainContract.View, View.On // TODO: Fix WaveForm blinking when seek // TODO: Show Record info -// TODO: Ability to delete record by swipe left // TODO: Ability to scroll up from the bottom of the list // TODO: Ability to search by record name in list -// TODO: Add pagination for records list // TODO: Welcome screen // TODO: Guidelines // TODO: Check how work max recording duration +// TODO: Add scroll animation to start when stop playback public static final int REQ_CODE_REC_AUDIO_AND_WRITE_EXTERNAL = 101; public static final int REQ_CODE_RECORD_AUDIO = 303; public static final int REQ_CODE_WRITE_EXTERNAL_STORAGE = 404; - public static final int REQ_CODE_READ_EXTERNAL_STORAGE = 405; + public static final int REQ_CODE_READ_EXTERNAL_STORAGE_IMPORT = 405; + public static final int REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK = 406; public static final int REQ_CODE_IMPORT_AUDIO = 11; private WaveformView waveformView; @@ -155,6 +158,7 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { }); presenter = ARApplication.getInjector().provideMainPresenter(); + presenter.executeFirstRun(); waveformView.setOnSeekListener(new WaveformView.OnSeekListener() { @Override @@ -163,8 +167,9 @@ public void onSeek(int px) { } @Override public void onSeeking(int px, long mills) { - if (waveformView.getWaveformLength() > 0) { - playProgress.setProgress(1000 * (int) AndroidUtils.pxToDp(px) / waveformView.getWaveformLength()); + int length = waveformView.getWaveformLength(); + if (length > 0) { + playProgress.setProgress(1000 * (int) AndroidUtils.pxToDp(px) / length); } txtProgress.setText(TimeUtils.formatTimeIntervalHourMinSec2(mills)); } @@ -207,12 +212,20 @@ public void onClick(View view) { switch (view.getId()) { case R.id.btn_play: //This method Starts or Pause playback. - presenter.startPlayback(); + if (FileUtil.isFileInExternalStorage(presenter.getActiveRecordPath())) { + if (checkStoragePermissionPlayback()) { + presenter.startPlayback(); + } + } else { + presenter.startPlayback(); + } break; case R.id.btn_record: - if (checkRecordPermission()) { - //Start or stop recording - presenter.startRecording(); + if (checkRecordPermission2()) { + if (checkStoragePermission2()) { + //Start or stop recording + presenter.startRecording(); + } } break; case R.id.btn_stop: @@ -225,25 +238,11 @@ public void onClick(View view) { startActivity(SettingsActivity.getStartIntent(getApplicationContext())); break; case R.id.btn_share: - String sharePath = presenter.getActiveRecordPath(); - if (sharePath != null) { - Uri photoURI = FileProvider.getUriForFile( - getApplicationContext(), - getApplicationContext().getApplicationContext().getPackageName() + ".app_file_provider", - new File(sharePath) - ); - Intent share = new Intent(Intent.ACTION_SEND); - share.setType("audio/*"); - share.putExtra(Intent.EXTRA_STREAM, photoURI); - share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(Intent.createChooser(share, getResources().getString(R.string.share_record, presenter.getActiveRecordName()))); - } else { - Timber.e("There no active record selected!"); - Toast.makeText(getApplicationContext(), R.string.please_select_record_to_share, Toast.LENGTH_LONG).show(); - } +// AndroidUtils.shareAudioFile(getApplicationContext(), presenter.getActiveRecordPath(), presenter.getActiveRecordName()); + showMenu(view); break; case R.id.btn_import: - if (checkStoragePermission()) { + if (checkStoragePermissionImport()) { startFileSelector(); } break; @@ -258,7 +257,9 @@ public void onClick(View view) { private void startFileSelector() { Intent intent_upload = new Intent(); intent_upload.setType("audio/*"); - intent_upload.setAction(Intent.ACTION_GET_CONTENT); + intent_upload.addCategory(Intent.CATEGORY_OPENABLE); +// intent_upload.setAction(Intent.ACTION_GET_CONTENT); + intent_upload.setAction(Intent.ACTION_OPEN_DOCUMENT); startActivityForResult(intent_upload, REQ_CODE_IMPORT_AUDIO); } @@ -301,6 +302,11 @@ public void showError(int resId) { Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); } + @Override + public void showMessage(int resId) { + Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); + } + @Override public void showRecordingStart() { btnRecord.setImageResource(R.drawable.ic_record_rec); @@ -308,6 +314,7 @@ public void showRecordingStart() { btnImport.setEnabled(false); btnShare.setEnabled(false); playProgress.setProgress(0); + playProgress.setEnabled(false); txtDuration.setText(R.string.zero_time); waveformView.showRecording(); } @@ -318,6 +325,7 @@ public void showRecordingStop() { btnPlay.setEnabled(true); btnImport.setEnabled(true); btnShare.setEnabled(true); + playProgress.setEnabled(true); waveformView.hideRecording(); waveformView.clearRecordingData(); } @@ -447,6 +455,11 @@ public void showName(String name) { txtName.setText(name); } + @Override + public void showRecordInfo(String name, String format, long duration, long size, String location) { + startActivity(ActivityInformation.getStartIntent(getApplicationContext(), name, format, duration, size, location)); + } + @Override public void updateRecordingView(List data) { waveformView.setRecordingData(data); @@ -481,6 +494,58 @@ public void hideRecordProcessing() { pnlRecordProcessing.setVisibility(View.INVISIBLE); } + private void showMenu(View v) { + PopupMenu popup = new PopupMenu(v.getContext(), v); + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_share: + AndroidUtils.shareAudioFile(getApplicationContext(), presenter.getActiveRecordPath(), presenter.getActiveRecordName()); + break; + case R.id.menu_info: + presenter.onRecordInfo(); + break; + case R.id.menu_rename: + setRecordName(presenter.getActiveRecordId(), new File(presenter.getActiveRecordPath())); + break; + case R.id.menu_open_with: + AndroidUtils.openAudioFile(getApplicationContext(), presenter.getActiveRecordPath(), presenter.getActiveRecordName()); + break; +// case R.id.menu_download: +// presenter.copyToDownloads(item.getPath(), item.getName()); +// break; + case R.id.menu_delete: + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); + builder.setTitle(R.string.warning) + .setIcon(R.drawable.ic_delete_forever) + .setMessage(R.string.delete_record) + .setCancelable(false) + .setPositiveButton(R.string.btn_yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + presenter.deleteActiveRecord(); + dialog.dismiss(); + } + }) + .setNegativeButton(R.string.btn_no, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + break; + } + return false; + } + }); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.menu_more, popup.getMenu()); + AndroidUtils.insertMenuItemIcons(v.getContext(), popup); + popup.show(); + } + public void setRecordName(final long recordId, File file) { //Create dialog layout programmatically. LinearLayout container = new LinearLayout(getApplicationContext()); @@ -525,18 +590,22 @@ public void onClick(DialogInterface dialog, int id) { if (!fileName.equalsIgnoreCase(newName)) { presenter.renameRecord(recordId, newName); } - hideKeyboard(); dialog.dismiss(); } }) .setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { - hideKeyboard(); dialog.dismiss(); } }) .create(); alertDialog.show(); + alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + hideKeyboard(); + } + }); editText.requestFocus(); editText.setSelection(editText.getText().length()); showKeyboard(); @@ -554,12 +623,71 @@ public void hideKeyboard(){ inputMethodManager.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); } - private boolean checkStoragePermission() { + private boolean checkStoragePermissionImport() { if (presenter.isStorePublic()) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_CODE_READ_EXTERNAL_STORAGE); + requestPermissions( + new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}, + REQ_CODE_READ_EXTERNAL_STORAGE_IMPORT); + return false; + } + } + } + return true; + } + + private boolean checkStoragePermissionPlayback() { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}, + REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK); + return false; + } + } + return true; + } + + private boolean checkRecordPermission2() { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQ_CODE_RECORD_AUDIO); + return false; + } + } + return true; + } + + private boolean checkStoragePermission2() { + if (presenter.isStorePublic()) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + AndroidUtils.showDialog(this, R.string.warning, R.string.need_write_permission, + new View.OnClickListener() { + @Override + public void onClick(View v) { + requestPermissions( + new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}, + REQ_CODE_WRITE_EXTERNAL_STORAGE); + } + }, + new View.OnClickListener() { + @Override + public void onClick(View v) { + presenter.setStoragePrivate(getApplicationContext()); + presenter.startRecording(); + } + } + ); return false; } } @@ -597,7 +725,7 @@ && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageMan } @Override - public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQ_CODE_REC_AUDIO_AND_WRITE_EXTERNAL && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED @@ -605,15 +733,28 @@ public void onRequestPermissionsResult(int requestCode, String permissions[], in presenter.startRecording(); } else if (requestCode == REQ_CODE_RECORD_AUDIO && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - presenter.startRecording(); + if (checkStoragePermission2()) { + presenter.startRecording(); + } } else if (requestCode == REQ_CODE_WRITE_EXTERNAL_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) { - presenter.startRecording(); - } else if (requestCode == REQ_CODE_READ_EXTERNAL_STORAGE && grantResults.length > 0 + if (checkRecordPermission2()) { + presenter.startRecording(); + } + } else if (requestCode == REQ_CODE_READ_EXTERNAL_STORAGE_IMPORT && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) { startFileSelector(); + } else if (requestCode == REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED + && grantResults[1] == PackageManager.PERMISSION_GRANTED) { + presenter.startPlayback(); + } else if (requestCode == REQ_CODE_WRITE_EXTERNAL_STORAGE + && (grantResults[0] == PackageManager.PERMISSION_DENIED + || grantResults[1] == PackageManager.PERMISSION_DENIED)) { + presenter.setStoragePrivate(getApplicationContext()); + presenter.startRecording(); } } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/main/MainContract.java b/app/src/main/java/com/dimowner/audiorecorder/app/main/MainContract.java index cc36dc66..51a0d0c6 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/main/MainContract.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/main/MainContract.java @@ -57,11 +57,15 @@ interface View extends Contract.View { void showDuration(String duration); void showName(String name); + void showRecordInfo(String name, String format, long duration, long size, String location); + void updateRecordingView(List data); } interface UserActionsListener extends Contract.UserActionsListener { + void executeFirstRun(); + void setAudioRecorder(RecorderContract.Recorder recorder); void startRecording(); @@ -80,6 +84,8 @@ interface UserActionsListener extends Contract.UserActionsListener 0) { + view.onPlayProgress(mills, AndroidUtils.convertMillsToPx(mills, + AndroidUtils.dpToPx(dpPerSecond)), (int) (1000 * mills / duration)); + } } }}); } @@ -266,6 +272,13 @@ public void clear() { recordingsTasks.close(); } + @Override + public void executeFirstRun() { + if (prefs.isFirstRun()) { + prefs.firstRunExecuted(); + } + } + @Override public void setAudioRecorder(RecorderContract.Recorder recorder) { appRecorder.setRecorder(recorder); @@ -454,8 +467,9 @@ public void run() { } }); } - } catch (IOException e) { + } catch (IOException | OutOfMemoryError e) { Timber.e(e); + view.showError(R.string.error_process_waveform); } isProcessing = false; } @@ -479,7 +493,13 @@ public void run() { @Override public void updateRecordingDir(Context context) { - this.fileRepository.updateRecordingDir(context, prefs); + fileRepository.updateRecordingDir(context, prefs); + } + + @Override + public void setStoragePrivate(Context context) { + prefs.setStoreDirPublic(false); + fileRepository.updateRecordingDir(context, prefs); } @Override @@ -514,6 +534,49 @@ public int getActiveRecordId() { } } + @Override + public void deleteActiveRecord() { + if (record != null) { + audioPlayer.stop(); + } + recordingsTasks.postRunnable(new Runnable() { + @Override public void run() { + localRepository.deleteRecord(record.getId()); + fileRepository.deleteRecordFile(record.getPath()); + if (record != null) { + prefs.setActiveRecord(-1); + dpPerSecond = AppConstants.SHORT_RECORD_DP_PER_SECOND; + } + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + view.showWaveForm(new int[]{}, 0); + view.showName(""); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(0)); + view.showMessage(R.string.record_deleted_successfully); + view.hideProgress(); + record = null; + } + } + }); + } + }); + } + + @Override + public void onRecordInfo() { + String format; + if (record.getPath().contains(AppConstants.M4A_EXTENSION)) { + format = AppConstants.M4A_EXTENSION; + } else if (record.getPath().contains(AppConstants.WAV_EXTENSION)) { + format = AppConstants.WAV_EXTENSION; + } else { + format = ""; + } + view.showRecordInfo(record.getName(), format, record.getDuration()/1000, new File(record.getPath()).length(), record.getPath()); + } + @Override public void importAudioFile(final Context context, final Uri uri) { if (view != null) { @@ -533,18 +596,85 @@ public void run() { File newFile = fileRepository.provideRecordFile(name); if (FileUtil.copyFile(fileDescriptor, newFile)) { - id = localRepository.insertFile(newFile.getAbsolutePath()); - prefs.setActiveRecord(id); - } - AndroidUtils.runOnUIThread(new Runnable() { - @Override public void run() { - if (view != null) { - view.hideImportProgress(); - audioPlayer.stop(); - loadActiveRecord(); + long duration = AndroidUtils.readRecordDuration(newFile); + if (duration/1000000 < AppConstants.LONG_RECORD_THRESHOLD_SECONDS) { + //Do simple import for short records. + id = localRepository.insertFile(newFile.getAbsolutePath()); + prefs.setActiveRecord(id); + AndroidUtils.runOnUIThread(new Runnable() { + @Override public void run() { + if (view != null) { + view.hideImportProgress(); + audioPlayer.stop(); + loadActiveRecord(); + } + } + }); + } else { + //Do 2 step import: 1) Import record with empty waveform. 2) Process and update waveform in background. + record = localRepository.insertRecord( + new Record( + Record.NO_ID, + newFile.getName(), + duration, //mills + newFile.lastModified(), + new Date().getTime(), + newFile.getAbsolutePath(), + false, + true, + new int[ARApplication.getLongWaveformSampleCount()])); + + id = record.getId(); + prefs.setActiveRecord(id); + songDuration = duration; + dpPerSecond = ARApplication.getDpPerSecond((float) songDuration / 1000000f); + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + audioPlayer.stop(); + view.showWaveForm(record.getAmps(), songDuration); + view.showName(FileUtil.removeFileExtension(record.getName())); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(songDuration / 1000)); + view.hideProgress(); + } + } + }); + + try { + if (view != null) { + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + view.hideImportProgress(); + view.showRecordProcessing(); + } + } + }); + isProcessing = true; + localRepository.updateWaveform((int)id); + record = localRepository.getRecord((int)id); + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + view.showWaveForm(record.getAmps(), songDuration); + view.hideRecordProcessing(); + } + } + }); + } + } catch (IOException | OutOfMemoryError e) { + Timber.e(e); + if (view != null) { + view.hideRecordProcessing(); + view.showError(R.string.error_process_waveform); + } } + isProcessing = false; } - }); + } } catch (SecurityException e) { Timber.e(e); AndroidUtils.runOnUIThread(new Runnable() { diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/records/EndlessRecyclerViewScrollListener.java b/app/src/main/java/com/dimowner/audiorecorder/app/records/EndlessRecyclerViewScrollListener.java new file mode 100644 index 00000000..824410df --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/records/EndlessRecyclerViewScrollListener.java @@ -0,0 +1,99 @@ +package com.dimowner.audiorecorder.app.records; + +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; + +public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { + // Sets the starting page index + private static final int STARTING_PAGE_INDEX = 1; + // The minimum amount of items to have below your current scroll position + // before loading more. + private int visibleThreshold = 5; + // The current offset index of data you have loaded + private int currentPage = 1; + // The total number of items in the dataset after the last load + private int previousTotalItemCount = 0; + // True if we are still waiting for the last set of data to load. + private boolean loading = true; + + private RecyclerView.LayoutManager mLayoutManager; + + public EndlessRecyclerViewScrollListener(L layoutManager) { + this.mLayoutManager = layoutManager; + if (layoutManager instanceof StaggeredGridLayoutManager) { + visibleThreshold = visibleThreshold * ((StaggeredGridLayoutManager) layoutManager).getSpanCount(); + } else if (layoutManager instanceof GridLayoutManager) { + visibleThreshold = visibleThreshold * ((GridLayoutManager) layoutManager).getSpanCount(); + } + } + + private int getLastVisibleItem(int[] lastVisibleItemPositions) { + int maxSize = 0; + for (int i = 0; i < lastVisibleItemPositions.length; i++) { + if (i == 0) { + maxSize = lastVisibleItemPositions[i]; + } else if (lastVisibleItemPositions[i] > maxSize) { + maxSize = lastVisibleItemPositions[i]; + } + } + return maxSize; + } + + // This happens many TIMES a second during a scroll, so be wary of the code you place here. + // We are given a few useful parameters to help us work out if we need to load some more data, + // but first we check if we are waiting for the previous load to finish. + @Override + public void onScrolled(RecyclerView view, int dx, int dy) { + int lastVisibleItemPosition = 0; + int totalItemCount = mLayoutManager.getItemCount(); + + if (mLayoutManager instanceof StaggeredGridLayoutManager) { + int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null); + // get maximum element within the list + lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions); + } else if (mLayoutManager instanceof LinearLayoutManager) { + lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition(); + } else if (mLayoutManager instanceof GridLayoutManager) { + lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition(); + } + + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < previousTotalItemCount) { + this.currentPage = STARTING_PAGE_INDEX; + this.previousTotalItemCount = totalItemCount; + if (totalItemCount == 0) { + this.loading = true; + } + } + // If it’s still loading, we check to see if the dataset count has + // changed, if so we conclude it has finished loading and update the current page + // number and total item count. + if (loading && (totalItemCount > previousTotalItemCount+1)) { + loading = false; + previousTotalItemCount = totalItemCount; + } + + // If it isn’t currently loading, we check to see if we have breached + // the visibleThreshold and need to reload more data. + // If we do need to reload some more data, we execute onLoadMore to fetch the data. + // threshold should reflect how many total columns there are too + if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount && totalItemCount > visibleThreshold) { + currentPage++; + onLoadMore(currentPage, totalItemCount); + loading = true; + } + } + + // Defines the process for actually loading more data based on page + public abstract void onLoadMore(int page, int totalItemsCount); + + //Used to reset inner state, if adapter data was fully changed + public void reset() { + currentPage = 1; + previousTotalItemCount = 0; + loading = true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsActivity.java b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsActivity.java index e7c8616b..8aee74a2 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsActivity.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsActivity.java @@ -16,6 +16,7 @@ package com.dimowner.audiorecorder.app.records; +import android.Manifest; import android.animation.Animator; import android.app.Activity; import android.app.AlertDialog; @@ -24,14 +25,18 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.support.annotation.NonNull; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; import android.util.TypedValue; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; @@ -39,6 +44,7 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; @@ -49,6 +55,7 @@ import com.dimowner.audiorecorder.ColorMap; import com.dimowner.audiorecorder.R; import com.dimowner.audiorecorder.app.PlaybackService; +import com.dimowner.audiorecorder.app.info.ActivityInformation; import com.dimowner.audiorecorder.app.widget.SimpleWaveformView; import com.dimowner.audiorecorder.app.widget.TouchLayout; import com.dimowner.audiorecorder.app.widget.WaveformView; @@ -65,6 +72,8 @@ public class RecordsActivity extends Activity implements RecordsContract.View, View.OnClickListener { + public static final int REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK = 406; + private RecyclerView recyclerView; private LinearLayoutManager layoutManager; private RecordsAdapter adapter; @@ -78,12 +87,14 @@ public class RecordsActivity extends Activity implements RecordsContract.View, V private ImageButton btnPrev; private ImageButton btnDelete; private ImageButton btnBookmarks; + private ImageButton btnSort; private ImageButton btnCheckBookmark; private TextView txtProgress; private TextView txtDuration; private TextView txtName; private TextView txtEmpty; private TextView txtTitle; + private TextView txtSubTitle; private TouchLayout touchLayout; private WaveformView waveformView; private ProgressBar panelProgress; @@ -108,7 +119,8 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_records); - AndroidUtils.setTranslucent(this, true); + toolbar = findViewById(R.id.toolbar); +// AndroidUtils.setTranslucent(this, true); ImageButton btnBack = findViewById(R.id.btn_back); btnBack.setOnClickListener(new View.OnClickListener() { @@ -116,7 +128,6 @@ protected void onCreate(Bundle savedInstanceState) { finish(); ARApplication.getInjector().releaseRecordsPresenter(); }}); - toolbar = findViewById(R.id.toolbar); bottomDivider = findViewById(R.id.bottomDivider); progressBar = findViewById(R.id.progress); @@ -127,9 +138,11 @@ protected void onCreate(Bundle savedInstanceState) { btnPrev = findViewById(R.id.btn_prev); btnDelete = findViewById(R.id.btn_delete); btnBookmarks = findViewById(R.id.btn_bookmarks); + btnSort = findViewById(R.id.btn_sort); btnCheckBookmark = findViewById(R.id.btn_check_bookmark); txtEmpty = findViewById(R.id.txtEmpty); txtTitle = findViewById(R.id.txt_title); + txtSubTitle = findViewById(R.id.txt_sub_title); btnPlay.setOnClickListener(this); btnStop.setOnClickListener(this); btnNext.setOnClickListener(this); @@ -137,6 +150,7 @@ protected void onCreate(Bundle savedInstanceState) { btnDelete.setOnClickListener(this); btnBookmarks.setOnClickListener(this); btnCheckBookmark.setOnClickListener(this); + btnSort.setOnClickListener(this); playProgress = findViewById(R.id.play_progress); txtProgress = findViewById(R.id.txt_progress); @@ -178,10 +192,11 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { recyclerView.setHasFixedSize(true); layoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(layoutManager); + recyclerView.addOnScrollListener(new MyScrollListener(layoutManager)); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrolled(RecyclerView rv, int dx, int dy) { + public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) { super.onScrolled(rv, dx, dy); handleToolbarScroll(dy); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -212,8 +227,9 @@ public void onItemClick(View view, long id, String path, final int position) { presenter.setActiveRecord(id, new RecordsContract.Callback() { @Override public void onSuccess() { presenter.stopPlayback(); - presenter.startPlayback(); - adapter.setActiveItem(position); + if (startPlayback()) { + adapter.setActiveItem(position); + } } @Override public void onError(Exception e) { Timber.e(e); @@ -229,12 +245,55 @@ public void onItemClick(View view, long id, String path, final int position) { presenter.removeFromBookmarks(id); } }); + adapter.setOnItemOptionListener(new RecordsAdapter.OnItemOptionListener() { + @Override + public void onItemOptionSelected(int menuId, final ListItem item) { + switch (menuId) { + case R.id.menu_share: + AndroidUtils.shareAudioFile(getApplicationContext(), item.getPath(), item.getName()); + break; + case R.id.menu_info: + presenter.onRecordInfo(item.getName(), item.getDuration(), item.getPath()); + break; + case R.id.menu_rename: + setRecordName(item.getId(), new File(item.getPath())); + break; + case R.id.menu_open_with: + AndroidUtils.openAudioFile(getApplicationContext(), item.getPath(), item.getName()); + break; +// case R.id.menu_download: +// presenter.copyToDownloads(item.getPath(), item.getName()); +// break; + case R.id.menu_delete: + AlertDialog.Builder builder = new AlertDialog.Builder(RecordsActivity.this); + builder.setTitle(R.string.warning) + .setIcon(R.drawable.ic_delete_forever) + .setMessage(R.string.delete_record) + .setCancelable(false) + .setPositiveButton(R.string.btn_yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + presenter.deleteRecord(item.getId(), item.getPath()); + dialog.dismiss(); + } + }) + .setNegativeButton(R.string.btn_no, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + break; + } + } + }); recyclerView.setAdapter(adapter); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - // Set the padding to match the Status Bar height - toolbar.setPadding(0, AndroidUtils.getStatusBarHeight(getApplicationContext()), 0, 0); - } +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// // Set the padding to match the Status Bar height +// toolbar.setPadding(0, AndroidUtils.getStatusBarHeight(getApplicationContext()), 0, 0); +// } presenter = ARApplication.getInjector().provideRecordsPresenter(); waveformView.setOnSeekListener(new WaveformView.OnSeekListener() { @@ -336,12 +395,25 @@ private void showToolbar() { AnimationUtil.viewAnimationY(toolbar, 0f, null); } + private boolean startPlayback() { + if (FileUtil.isFileInExternalStorage(presenter.getActiveRecordPath())) { + if (checkStoragePermissionPlayback()) { + presenter.startPlayback(); + return true; + } + } else { + presenter.startPlayback(); + return true; + } + return false; + } + @Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_play: //This method Starts or Pause playback. - presenter.startPlayback(); + startPlayback(); break; case R.id.btn_stop: presenter.stopPlayback(); @@ -353,18 +425,19 @@ public void onClick(View view) { presenter.setActiveRecord(id, new RecordsContract.Callback() { @Override public void onSuccess() { presenter.stopPlayback(); - presenter.startPlayback(); - int pos = adapter.findPositionById(id); - if (pos >= 0) { - recyclerView.scrollToPosition(pos); - int o = recyclerView.computeVerticalScrollOffset(); - if (o > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - toolbar.setTranslationZ(getResources().getDimension(R.dimen.toolbar_elevation)); - toolbar.setBackgroundResource(colorMap.getPrimaryColorRes()); + if (startPlayback()) { + int pos = adapter.findPositionById(id); + if (pos >= 0) { + recyclerView.scrollToPosition(pos); + int o = recyclerView.computeVerticalScrollOffset(); + if (o > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setTranslationZ(getResources().getDimension(R.dimen.toolbar_elevation)); + toolbar.setBackgroundResource(colorMap.getPrimaryColorRes()); + } } + adapter.setActiveItem(pos); } - adapter.setActiveItem(pos); } } @Override public void onError(Exception e) { @@ -378,11 +451,12 @@ public void onClick(View view) { presenter.setActiveRecord(id2, new RecordsContract.Callback() { @Override public void onSuccess() { presenter.stopPlayback(); - presenter.startPlayback(); - int pos2 = adapter.findPositionById(id2); - if (pos2 >= 0) { - recyclerView.scrollToPosition(pos2); - adapter.setActiveItem(pos2); + if (startPlayback()) { + int pos2 = adapter.findPositionById(id2); + if (pos2 >= 0) { + recyclerView.scrollToPosition(pos2); + adapter.setActiveItem(pos2); + } } } @Override public void onError(Exception e) { @@ -418,6 +492,9 @@ public void onClick(DialogInterface dialog, int id) { case R.id.btn_bookmarks: presenter.applyBookmarksFilter(); break; + case R.id.btn_sort: + showMenu(view); + break; case R.id.txt_name: if (presenter.getActiveRecordId() != -1) { setRecordName(presenter.getActiveRecordId(), new File(presenter.getActiveRecordPath())); @@ -426,6 +503,31 @@ public void onClick(DialogInterface dialog, int id) { } } + private void showMenu(View v) { + PopupMenu popup = new PopupMenu(v.getContext(), v); + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_date: + presenter.updateRecordsOrder(AppConstants.SORT_DATE); + break; + case R.id.menu_name: + presenter.updateRecordsOrder(AppConstants.SORT_NAME); + break; + case R.id.menu_duration: + presenter.updateRecordsOrder(AppConstants.SORT_DURATION); + break; + } + return false; + } + }); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.menu_sort, popup.getMenu()); + AndroidUtils.insertMenuItemIcons(v.getContext(), popup); + popup.show(); + } + @Override protected void onStart() { super.onStart(); @@ -451,11 +553,11 @@ public void onBackPressed() { private void handleToolbarScroll(int dy) { float inset = toolbar.getTranslationY() - dy; int height; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - height = toolbar.getHeight() + AndroidUtils.getStatusBarHeight(getApplicationContext()); - } else { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// height = toolbar.getHeight() + AndroidUtils.getStatusBarHeight(getApplicationContext()); +// } else { height = toolbar.getHeight(); - } +// } if (inset < -height) { inset = -height; @@ -533,12 +635,12 @@ public void run() { } @Override - public void showRecords(List records) { + public void showRecords(List records, int order) { if (records.size() == 0) { - txtEmpty.setVisibility(View.VISIBLE); - adapter.setData(new ArrayList()); +// txtEmpty.setVisibility(View.VISIBLE); + adapter.setData(new ArrayList(), order); } else { - adapter.setData(records); + adapter.setData(records, order); txtEmpty.setVisibility(View.GONE); if (touchLayout.getVisibility() == View.VISIBLE) { adapter.showFooter(); @@ -546,6 +648,12 @@ public void showRecords(List records) { } } + @Override + public void addRecords(List records, int order) { + adapter.addData(records, order); + txtEmpty.setVisibility(View.GONE); + } + @Override public void showEmptyList() { txtEmpty.setText(R.string.no_records); @@ -575,10 +683,15 @@ public void showRecordName(String name) { @Override public void onDeleteRecord(long id) { - adapter.deleteItem(id); +// adapter.deleteItem(id); + presenter.loadRecords(); if (adapter.getAudioRecordsCount() == 0) { showEmptyList(); } + } + + @Override + public void hidePlayPanel() { hidePanel(); } @@ -598,16 +711,40 @@ public void removedFromBookmarks(int id, boolean isActive) { adapter.markRemovedFromBookmarks(id); } + @Override + public void showSortType(int type) { + switch (type) { + case AppConstants.SORT_DATE: + txtSubTitle.setText(R.string.by_date); + break; + case AppConstants.SORT_NAME: + txtSubTitle.setText(R.string.by_name); + break; + case AppConstants.SORT_DURATION: + txtSubTitle.setText(R.string.by_duration); + break; + } + } + @Override public void bookmarksSelected() { btnBookmarks.setImageResource(R.drawable.ic_bookmark); txtTitle.setText(R.string.bookmarks); + btnSort.setVisibility(View.GONE); + txtSubTitle.setVisibility(View.GONE); } @Override public void bookmarksUnselected() { btnBookmarks.setImageResource(R.drawable.ic_bookmark_bordered); txtTitle.setText(R.string.records); + btnSort.setVisibility(View.VISIBLE); + txtSubTitle.setVisibility(View.VISIBLE); + } + + @Override + public void showRecordInfo(String name, String format, long duration, long size, String location) { + startActivity(ActivityInformation.getStartIntent(getApplicationContext(), name, format, duration, size, location)); } @Override @@ -630,6 +767,11 @@ public void showError(int resId) { Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); } + @Override + public void showMessage(int resId) { + Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); + } + public void setRecordName(final long recordId, File file) { //Create dialog layout programmatically. LinearLayout container = new LinearLayout(getApplicationContext()); @@ -673,19 +815,24 @@ public void onClick(DialogInterface dialog, int id) { String newName = editText.getText().toString(); if (!fileName.equalsIgnoreCase(newName)) { presenter.renameRecord(recordId, newName); + presenter.loadRecords(); } - hideKeyboard(); dialog.dismiss(); } }) .setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { - hideKeyboard(); dialog.dismiss(); } }) .create(); alertDialog.show(); + alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + hideKeyboard(); + } + }); editText.requestFocus(); editText.setSelection(editText.getText().length()); showKeyboard(); @@ -702,4 +849,41 @@ public void hideKeyboard(){ InputMethodManager inputMethodManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); } + + private boolean checkStoragePermissionPlayback() { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}, + REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK); + return false; + } + } + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQ_CODE_READ_EXTERNAL_STORAGE_PLAYBACK && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED + && grantResults[1] == PackageManager.PERMISSION_GRANTED) { + presenter.startPlayback(); + } + } + + public class MyScrollListener extends EndlessRecyclerViewScrollListener { + + public MyScrollListener(L layoutManager) { + super(layoutManager); + } + + @Override + public void onLoadMore(int page, int totalItemsCount) { +// Timber.v("onLoadMore page = " + page + " count = " + totalItemsCount); + presenter.loadRecordsPage(page); + } + } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java index d48cae82..bdcf20a4 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java @@ -17,19 +17,22 @@ package com.dimowner.audiorecorder.app.records; import android.graphics.Typeface; -import android.os.Build; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.TextView; +import com.dimowner.audiorecorder.AppConstants; import com.dimowner.audiorecorder.R; import com.dimowner.audiorecorder.app.widget.SimpleWaveformView; import com.dimowner.audiorecorder.util.AndroidUtils; @@ -48,8 +51,9 @@ public class RecordsAdapter extends RecyclerView.Adapter(); } @@ -60,11 +64,11 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, View view = new View(viewGroup.getContext()); int height; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - height = AndroidUtils.getStatusBarHeight(viewGroup.getContext()) + (int) viewGroup.getContext().getResources().getDimension(R.dimen.toolbar_height); - } else { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// height = AndroidUtils.getStatusBarHeight(viewGroup.getContext()) + (int) viewGroup.getContext().getResources().getDimension(R.dimen.toolbar_height); +// } else { height = (int) viewGroup.getContext().getResources().getDimension(R.dimen.toolbar_height); - } +// } LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, height); @@ -101,16 +105,17 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, } @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int p) { + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int pos) { if (viewHolder.getItemViewType() == ListItem.ITEM_TYPE_NORMAL) { final ItemViewHolder holder = (ItemViewHolder) viewHolder; + final int p = holder.getAdapterPosition(); holder.name.setText(data.get(p).getName()); holder.description.setText(data.get(p).getDurationStr()); holder.created.setText(data.get(p).getAddedTimeStr()); if (data.get(p).isBookmarked()) { - holder.bookmark.setImageResource(R.drawable.ic_bookmark_small); + holder.btnBookmark.setImageResource(R.drawable.ic_bookmark_small); } else { - holder.bookmark.setImageResource(R.drawable.ic_bookmark_bordered_small); + holder.btnBookmark.setImageResource(R.drawable.ic_bookmark_bordered_small); } if (viewHolder.getLayoutPosition() == activeItem) { holder.view.setBackgroundResource(R.color.selected_item_color); @@ -118,7 +123,7 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, holder.view.setBackgroundResource(android.R.color.transparent); } - holder.bookmark.setOnClickListener(new View.OnClickListener() { + holder.btnBookmark.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onAddToBookmarkListener != null && data.size() > p) { @@ -130,6 +135,12 @@ public void onClick(View v) { } } }); + holder.btnMore.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showMenu(v, p); + } + }); holder.waveformView.setWaveform(data.get(p).getAmps()); holder.view.setOnClickListener(new View.OnClickListener() { @@ -141,25 +152,42 @@ public void onClick(View v) { }}); } else if (viewHolder.getItemViewType() == ListItem.ITEM_TYPE_DATE) { UniversalViewHolder holder = (UniversalViewHolder) viewHolder; - ((TextView)holder.view).setText(TimeUtils.formatDateSmart(data.get(p).getAdded(), holder.view.getContext())); + ((TextView)holder.view).setText(TimeUtils.formatDateSmart(data.get(viewHolder.getAdapterPosition()).getAdded(), holder.view.getContext())); } } - public void setActiveItem(int activeItem) { + private void showMenu(View v, final int pos) { + PopupMenu popup = new PopupMenu(v.getContext(), v); + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (onItemOptionListener != null) { + onItemOptionListener.onItemOptionSelected(item.getItemId(), data.get(pos)); + } + return false; + } + }); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.menu_more, popup.getMenu()); + AndroidUtils.insertMenuItemIcons(v.getContext(), popup); + popup.show(); + } + + void setActiveItem(int activeItem) { int prev = this.activeItem; this.activeItem = activeItem; notifyItemChanged(prev); notifyItemChanged(activeItem); } - public void setActiveItemById(long id) { - int pos = findPositionById(id); - if (pos >= 0) { - setActiveItem(pos); - } - } +// public void setActiveItemById(long id) { +// int pos = findPositionById(id); +// if (pos >= 0) { +// setActiveItem(pos); +// } +// } - public int findPositionById(long id) { + int findPositionById(long id) { if (id >= 0) { for (int i = 0; i < data.size() - 1; i++) { if (data.get(i).getId() == id) { @@ -180,46 +208,88 @@ public int getItemViewType(int position) { return data.get(position).getType(); } - public void setData(List data) { - this.data = data; + void setData(List d, int order) { + updateShowHeader(order); if (showDateHeaders) { - addDateHeaders(); + data = addDateHeaders(d); + } else { + data = d; } - this.data.add(0, ListItem.createHeaderItem()); + data.add(0, ListItem.createHeaderItem()); notifyDataSetChanged(); } - private void addDateHeaders() { +// public void addData(List d) { +// this.data.addAll(addDateHeaders(d)); +// notifyItemRangeInserted(data.size() - d.size(), d.size()); +// } + + void addData(List d, int order) { + if (data.size() > 0) { + updateShowHeader(order); + if (showDateHeaders) { + if (findFooter() >= 0) { + data.addAll(data.size() - 1, addDateHeaders(d)); + } else { + data.addAll(addDateHeaders(d)); + } + } else { + if (findFooter() >= 0) { + data.addAll(data.size() - 1, d); + } else { + data.addAll(d); + } + } + notifyItemRangeInserted(data.size() - d.size(), d.size()); + } + } + + private void updateShowHeader(int order) { + if (order == AppConstants.SORT_DATE) { + showDateHeaders = true; + } else { + showDateHeaders = false; + } + } + + public ListItem getItem(int pos) { + return data.get(pos); + } + + private List addDateHeaders(List data) { if (data.size() > 0) { - data.add(0, ListItem.createDateItem(data.get(0).getAdded())); + if (!hasDateHeader(data, data.get(0).getAdded())) { + data.add(0, ListItem.createDateItem(data.get(0).getAdded())); + } Calendar d1 = Calendar.getInstance(); d1.setTimeInMillis(data.get(0).getAdded()); Calendar d2 = Calendar.getInstance(); for (int i = 1; i < data.size(); i++) { d1.setTimeInMillis(data.get(i - 1).getAdded()); d2.setTimeInMillis(data.get(i).getAdded()); - if (!TimeUtils.isSameDay(d1, d2)) { + if (!TimeUtils.isSameDay(d1, d2) && !hasDateHeader(data, data.get(i).getAdded())) { data.add(i, ListItem.createDateItem(data.get(i).getAdded())); } } } + return data; } - public void deleteItem(long id) { - for (int i = 0; i < data.size(); i++) { - if (data.get(i).getId() == id) { - data.remove(i); - if (getAudioRecordsCount() == 0) { - data.clear(); - notifyDataSetChanged(); - } else { - notifyItemRemoved(i); - } - } - } - } - - public int getAudioRecordsCount() { +// public void deleteItem(long id) { +// for (int i = 0; i < data.size(); i++) { +// if (data.get(i).getId() == id) { +// data.remove(i); +// if (getAudioRecordsCount() == 0) { +// data.clear(); +// notifyDataSetChanged(); +// } else { +// notifyItemRemoved(i); +// } +// } +// } +// } + + int getAudioRecordsCount() { int count = 0; for (int i = 0; i < data.size(); i++) { if (data.get(i).getType() == ListItem.ITEM_TYPE_NORMAL) { @@ -244,7 +314,7 @@ public void hideFooter() { } } - public long getNextTo(long id) { + long getNextTo(long id) { if (id >= 0) { for (int i = 0; i < data.size() - 1; i++) { if (data.get(i).getId() == id) { @@ -259,7 +329,7 @@ public long getNextTo(long id) { return -1; } - public long getPrevTo(long id) { + long getPrevTo(long id) { if (id >= 0) { for (int i = 1; i < data.size(); i++) { if (data.get(i).getId() == id) { @@ -283,7 +353,7 @@ private int findFooter() { return -1; } - public void markAddedToBookmarks(int id) { + void markAddedToBookmarks(int id) { for (int i = 0; i < data.size(); i++) { if (data.get(i).getId() == id) { data.get(i).setBookmarked(true); @@ -292,7 +362,7 @@ public void markAddedToBookmarks(int id) { } } - public void markRemovedFromBookmarks(int id) { + void markRemovedFromBookmarks(int id) { for (int i = 0; i < data.size(); i++) { if (data.get(i).getId() == id) { data.get(i).setBookmarked(false); @@ -301,11 +371,26 @@ public void markRemovedFromBookmarks(int id) { } } - public void setItemClickListener(ItemClickListener itemClickListener) { + private boolean hasDateHeader(List data, long time) { + for (int i = data.size()-1; i>= 0; i--) { + if (data.get(i).getType() == ListItem.ITEM_TYPE_DATE) { + Calendar d1 = Calendar.getInstance(); + d1.setTimeInMillis(data.get(i).getAdded()); + Calendar d2 = Calendar.getInstance(); + d2.setTimeInMillis(time); + if (TimeUtils.isSameDay(d1, d2)) { + return true; + } + } + } + return false; + } + + void setItemClickListener(ItemClickListener itemClickListener) { this.itemClickListener = itemClickListener; } - public void setOnAddToBookmarkListener(OnAddToBookmarkListener onAddToBookmarkListener) { + void setOnAddToBookmarkListener(OnAddToBookmarkListener onAddToBookmarkListener) { this.onAddToBookmarkListener = onAddToBookmarkListener; } @@ -313,37 +398,44 @@ public interface ItemClickListener{ void onItemClick(View view, long id, String path, int position); } + void setOnItemOptionListener(OnItemOptionListener onItemOptionListener) { + this.onItemOptionListener = onItemOptionListener; + } public interface OnAddToBookmarkListener { void onAddToBookmarks(int id); void onRemoveFromBookmarks(int id); } + public interface OnItemOptionListener { + void onItemOptionSelected(int menuId, ListItem item); + } + public class ItemViewHolder extends RecyclerView.ViewHolder { TextView name; TextView description; TextView created; - // ImageView image; - ImageButton bookmark; + ImageButton btnBookmark; + ImageButton btnMore; SimpleWaveformView waveformView; View view; - public ItemViewHolder(View itemView) { + ItemViewHolder(View itemView) { super(itemView); - this.view = itemView; - this.name = itemView.findViewById(R.id.list_item_name); - this.description = itemView.findViewById(R.id.list_item_description); - this.created = itemView.findViewById(R.id.list_item_date); -// this.image = itemView.findViewById(R.id.list_item_image); - this.bookmark = itemView.findViewById(R.id.bookmark); - this.waveformView = itemView.findViewById(R.id.list_item_waveform); + view = itemView; + name = itemView.findViewById(R.id.list_item_name); + description = itemView.findViewById(R.id.list_item_description); + created = itemView.findViewById(R.id.list_item_date); + btnBookmark = itemView.findViewById(R.id.list_item_bookmark); + waveformView = itemView.findViewById(R.id.list_item_waveform); + btnMore = itemView.findViewById(R.id.list_item_more); } } public class UniversalViewHolder extends RecyclerView.ViewHolder { View view; - public UniversalViewHolder(View view) { + UniversalViewHolder(View view) { super(view); this.view = view; } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsContract.java b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsContract.java index 1d0a714a..f52397ee 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsContract.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsContract.java @@ -40,7 +40,9 @@ interface View extends Contract.View { void showWaveForm(int[] waveForm, long duration); void showDuration(String duration); - void showRecords(List records); + void showRecords(List records, int order); + void addRecords(List records, int order); + void showEmptyList(); void showEmptyBookmarksList(); @@ -51,11 +53,17 @@ interface View extends Contract.View { void onDeleteRecord(long id); + void hidePlayPanel(); + void addedToBookmarks(int id, boolean isActive); void removedFromBookmarks(int id, boolean isActive); + void showSortType(int type); + void bookmarksSelected(); void bookmarksUnselected(); + + void showRecordInfo(String name, String format, long duration, long size, String location); } interface UserActionsListener extends Contract.UserActionsListener { @@ -74,10 +82,18 @@ interface UserActionsListener extends Contract.UserActionsListener 0) { + view.onPlayProgress(mills, AndroidUtils.convertMillsToPx(mills, + AndroidUtils.dpToPx(dpPerSecond)), (int) (1000 * mills / duration)); + } } }}); } @@ -161,6 +168,9 @@ public void onError(AppException throwable) { view.showPlayStart(); } } + if (view != null) { + view.showSortType(prefs.getRecordsOrder()); + } } @Override @@ -182,9 +192,9 @@ public void clear() { @Override public void startPlayback() { if (!appRecorder.isRecording()) { - if (record != null) { + if (activeRecord != null) { if (!audioPlayer.isPlaying()) { - audioPlayer.setData(record.getPath()); + audioPlayer.setData(activeRecord.getPath()); } audioPlayer.playOrPause(); } @@ -218,25 +228,37 @@ public void playPrev() { @Override public void deleteActiveRecord() { - audioPlayer.stop(); + if (activeRecord != null) { + deleteRecord(activeRecord.getId(), activeRecord.getPath()); + } + } + + @Override + public void deleteRecord(final long id, final String path) { + if (activeRecord != null && activeRecord.getId() == id) { + audioPlayer.stop(); + } recordingsTasks.postRunnable(new Runnable() { @Override public void run() { - if (record != null) { - localRepository.deleteRecord(record.getId()); - fileRepository.deleteRecordFile(record.getPath()); + localRepository.deleteRecord((int)id); + fileRepository.deleteRecordFile(path); + if (activeRecord != null && activeRecord.getId() == id) { prefs.setActiveRecord(-1); - final long id = record.getId(); - record = null; dpPerSecond = AppConstants.SHORT_RECORD_DP_PER_SECOND; - AndroidUtils.runOnUIThread(new Runnable() { - @Override - public void run() { - if (view != null) { - view.onDeleteRecord(id); + } + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + view.onDeleteRecord(id); + if (activeRecord != null && activeRecord.getId() == id) { + view.hidePlayPanel(); + view.showMessage(R.string.record_deleted_successfully); + activeRecord = null; } } - }); - } + } + }); } }); } @@ -283,10 +305,10 @@ public void renameRecord(final long id, String n) { ext = AppConstants.M4A_EXTENSION; } if (fileRepository.renameFile(r.getPath(), name, ext)) { - record = new Record(r.getId(), nameWithExt, r.getDuration(), r.getCreated(), + activeRecord = new Record(r.getId(), nameWithExt, r.getDuration(), r.getCreated(), r.getAdded(), renamed.getAbsolutePath(), r.isBookmarked(), r.isWaveformProcessed(), r.getAmps()); - if (localRepository.updateRecord(record)) { + if (localRepository.updateRecord(activeRecord)) { AndroidUtils.runOnUIThread(new Runnable() { @Override public void run() { if (view != null) { @@ -330,6 +352,26 @@ record = new Record(r.getId(), nameWithExt, r.getDuration(), r.getCreated(), }}); } + @Override + public void copyToDownloads(final String path, final String name) { + if (view != null) { + //TODO: show copy progress + copyTasks.postRunnable(new Runnable() { + @Override + public void run() { + try { + FileUtil.copyFile(new File(path), FileUtil.createFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), name)); + //TODO: show success result + } catch (IOException e) { + Timber.v(e); + //TODO: show copy error + } + //TODO:hide progress + } + }); + } + } + @Override public void loadRecords() { if (view != null) { @@ -338,21 +380,22 @@ public void loadRecords() { loadingTasks.postRunnable(new Runnable() { @Override public void run() { - final List recordList = localRepository.getAllRecords(); - record = localRepository.getRecord((int) prefs.getActiveRecord()); - if (record != null) { - dpPerSecond = ARApplication.getDpPerSecond((float) record.getDuration() / 1000000f); + final int order = prefs.getRecordsOrder(); + final List recordList = localRepository.getRecords(0, order); + activeRecord = localRepository.getRecord((int) prefs.getActiveRecord()); + if (activeRecord != null) { + dpPerSecond = ARApplication.getDpPerSecond((float) activeRecord.getDuration() / 1000000f); } AndroidUtils.runOnUIThread(new Runnable() { @Override public void run() { if (view != null) { - view.showRecords(Mapper.recordsToListItems(recordList)); - if (record != null) { - view.showWaveForm(record.getAmps(), record.getDuration()); - view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(record.getDuration() / 1000)); - view.showRecordName(FileUtil.removeFileExtension(record.getName())); - if (record.isBookmarked()) { + view.showRecords(Mapper.recordsToListItems(recordList), order); + if (activeRecord != null) { + view.showWaveForm(activeRecord.getAmps(), activeRecord.getDuration()); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(activeRecord.getDuration() / 1000)); + view.showRecordName(FileUtil.removeFileExtension(activeRecord.getName())); + if (activeRecord.isBookmarked()) { view.bookmarksSelected(); } else { view.bookmarksUnselected(); @@ -372,6 +415,53 @@ public void run() { } } + @Override + public void updateRecordsOrder(int order) { + prefs.setRecordOrder(order); + view.showSortType(order); + loadRecords(); + } + + @Override + public void loadRecordsPage(final int page) { + if (view != null) { + view.showProgress(); + view.showPanelProgress(); + loadingTasks.postRunnable(new Runnable() { + @Override + public void run() { + final int order = prefs.getRecordsOrder(); + final List recordList = localRepository.getRecords(page, order); + activeRecord = localRepository.getRecord((int) prefs.getActiveRecord()); + if (activeRecord != null) { + dpPerSecond = ARApplication.getDpPerSecond((float) activeRecord.getDuration() / 1000000f); + } + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null) { + view.addRecords(Mapper.recordsToListItems(recordList), order); + if (activeRecord != null) { + view.showWaveForm(activeRecord.getAmps(), activeRecord.getDuration()); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(activeRecord.getDuration() / 1000)); + view.showRecordName(FileUtil.removeFileExtension(activeRecord.getName())); + if (activeRecord.isBookmarked()) { + view.bookmarksSelected(); + } else { + view.bookmarksUnselected(); + } + } + view.hideProgress(); + view.hidePanelProgress(); + view.bookmarksUnselected(); + } + } + }); + } + }); + } + } + public void loadBookmarks() { if (!showBookmarks) { loadRecords(); @@ -383,19 +473,19 @@ public void loadBookmarks() { @Override public void run() { final List recordList = localRepository.getBookmarks(); - record = localRepository.getRecord((int) prefs.getActiveRecord()); - if (record != null) { - dpPerSecond = ARApplication.getDpPerSecond((float) record.getDuration() / 1000000f); + activeRecord = localRepository.getRecord((int) prefs.getActiveRecord()); + if (activeRecord != null) { + dpPerSecond = ARApplication.getDpPerSecond((float) activeRecord.getDuration() / 1000000f); } AndroidUtils.runOnUIThread(new Runnable() { @Override public void run() { if (view != null) { - view.showRecords(Mapper.recordsToListItems(recordList)); - if (record != null) { - view.showWaveForm(record.getAmps(), record.getDuration()); - view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(record.getDuration() / 1000)); - view.showRecordName(FileUtil.removeFileExtension(record.getName())); + view.showRecords(Mapper.recordsToListItems(recordList), AppConstants.SORT_DATE); + if (activeRecord != null) { + view.showWaveForm(activeRecord.getAmps(), activeRecord.getDuration()); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(activeRecord.getDuration() / 1000)); + view.showRecordName(FileUtil.removeFileExtension(activeRecord.getName())); } view.hideProgress(); view.hidePanelProgress(); @@ -423,25 +513,27 @@ public void checkBookmarkActiveRecord() { recordingsTasks.postRunnable(new Runnable() { @Override public void run() { - if (record.isBookmarked()) { - localRepository.removeFromBookmarks(record.getId()); - } else { - localRepository.addToBookmarks(record.getId()); - } - record.setBookmark(!record.isBookmarked()); + if (activeRecord != null) { + if (activeRecord.isBookmarked()) { + localRepository.removeFromBookmarks(activeRecord.getId()); + } else { + localRepository.addToBookmarks(activeRecord.getId()); + } + activeRecord.setBookmark(!activeRecord.isBookmarked()); - AndroidUtils.runOnUIThread(new Runnable() { - @Override - public void run() { - if (view != null) { - if (record.isBookmarked()) { - view.addedToBookmarks(record.getId(), true); - } else { - view.removedFromBookmarks(record.getId(), true); + AndroidUtils.runOnUIThread(new Runnable() { + @Override + public void run() { + if (view != null && activeRecord != null) { + if (activeRecord.isBookmarked()) { + view.addedToBookmarks(activeRecord.getId(), true); + } else { + view.removedFromBookmarks(activeRecord.getId(), true); + } } } - } - }); + }); + } } }); } @@ -458,7 +550,7 @@ public void run() { @Override public void run() { if (view != null) { - view.addedToBookmarks(r.getId(), r.getId() == record.getId()); + view.addedToBookmarks(r.getId(), activeRecord != null && r.getId() == activeRecord.getId()); } } }); @@ -479,7 +571,7 @@ public void run() { @Override public void run() { if (view != null) { - view.removedFromBookmarks(r.getId(), r.getId() == record.getId()); + view.removedFromBookmarks(r.getId(), activeRecord != null && r.getId() == activeRecord.getId()); } } }); @@ -498,21 +590,21 @@ public void setActiveRecord(final long id, final RecordsContract.Callback callba loadingTasks.postRunnable(new Runnable() { @Override public void run() { - record = localRepository.getRecord((int) id); - if (record != null) { - dpPerSecond = ARApplication.getDpPerSecond((float) record.getDuration()/1000000f); + activeRecord = localRepository.getRecord((int) id); + if (activeRecord != null) { + dpPerSecond = ARApplication.getDpPerSecond((float) activeRecord.getDuration()/1000000f); AndroidUtils.runOnUIThread(new Runnable() { @Override public void run() { - if (view != null) { - view.showWaveForm(record.getAmps(), record.getDuration()); - view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(record.getDuration() / 1000)); - view.showRecordName(FileUtil.removeFileExtension(record.getName())); + if (view != null && activeRecord != null) { + view.showWaveForm(activeRecord.getAmps(), activeRecord.getDuration()); + view.showDuration(TimeUtils.formatTimeIntervalHourMinSec2(activeRecord.getDuration() / 1000)); + view.showRecordName(FileUtil.removeFileExtension(activeRecord.getName())); callback.onSuccess(); - if (record.isBookmarked()) { - view.addedToBookmarks(record.getId(), true); + if (activeRecord.isBookmarked()) { + view.addedToBookmarks(activeRecord.getId(), true); } else { - view.removedFromBookmarks(record.getId(), true); + view.removedFromBookmarks(activeRecord.getId(), true); } view.hidePanelProgress(); view.showPlayerPanel(); @@ -542,8 +634,8 @@ public long getActiveRecordId() { @Override public String getActiveRecordPath() { - if (record != null) { - return record.getPath(); + if (activeRecord != null) { + return activeRecord.getPath(); } else { return null; } @@ -551,10 +643,23 @@ public String getActiveRecordPath() { @Override public String getRecordName() { - if (record != null) { - return record.getName(); + if (activeRecord != null) { + return activeRecord.getName(); } else { return "Record"; } } + + @Override + public void onRecordInfo(String name, long duration, String location) { + String format; + if (location.contains(AppConstants.M4A_EXTENSION)) { + format = AppConstants.M4A_EXTENSION; + } else if (location.contains(AppConstants.WAV_EXTENSION)) { + format = AppConstants.WAV_EXTENSION; + } else { + format = ""; + } + view.showRecordInfo(name, format, duration, new File(location).length(), location); + } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsActivity.java b/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsActivity.java index d3c6369b..895ef774 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsActivity.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsActivity.java @@ -30,9 +30,12 @@ import android.text.Html; import android.text.SpannableStringBuilder; import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; import android.widget.AdapterView; import android.widget.CompoundButton; import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; @@ -42,6 +45,7 @@ import com.dimowner.audiorecorder.AppConstants; import com.dimowner.audiorecorder.ColorMap; import com.dimowner.audiorecorder.R; +import com.dimowner.audiorecorder.util.AndroidUtils; import java.util.ArrayList; import java.util.List; @@ -57,6 +61,7 @@ public class SettingsActivity extends Activity implements SettingsContract.View, private Switch swPublicDir; private Switch swRecordInStereo; private Switch swKeepScreenOn; + private Switch swAskToRename; private Spinner formatSelector; private Spinner sampleRateSelector; @@ -80,19 +85,31 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + LinearLayout toolbar = findViewById(R.id.toolbar); + toolbar.setPadding(0, AndroidUtils.getStatusBarHeight(getApplicationContext()), 0, 0); + + View space = findViewById(R.id.space); + ViewGroup.LayoutParams params = space.getLayoutParams(); + params.height = AndroidUtils.getNavigationBarHeight(getApplicationContext()); + space.setLayoutParams(params); + ImageButton btnBack = findViewById(R.id.btn_back); - TextView btnDeleteAll = findViewById(R.id.btnDeleteAll); +// TextView btnDeleteAll = findViewById(R.id.btnDeleteAll); TextView btnRate = findViewById(R.id.btnRate); TextView btnRequest = findViewById(R.id.btnRequest); TextView txtAbout = findViewById(R.id.txtAbout); txtAbout.setText(getAboutContent()); btnBack.setOnClickListener(this); - btnDeleteAll.setOnClickListener(this); +// btnDeleteAll.setOnClickListener(this); btnRate.setOnClickListener(this); btnRequest.setOnClickListener(this); swPublicDir = findViewById(R.id.swPublicDir); swRecordInStereo = findViewById(R.id.swRecordInStereo); swKeepScreenOn = findViewById(R.id.swKeepScreenOn); + swAskToRename = findViewById(R.id.swAskToRename); txtRecordsCount = findViewById(R.id.txt_records_count); txtTotalDuration= findViewById(R.id.txt_total_duration); @@ -117,6 +134,12 @@ public void onCheckedChanged(CompoundButton btn, boolean isChecked) { presenter.keepScreenOn(isChecked); } }); + swAskToRename.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton btn, boolean isChecked) { + presenter.askToRenameAfterRecordingStop(isChecked); + } + }); presenter = ARApplication.getInjector().provideSettingsPresenter(); @@ -262,27 +285,27 @@ public void onClick(View v) { case R.id.btnRate: rateApp(); break; - case R.id.btnDeleteAll: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.warning) - .setIcon(R.drawable.ic_delete_forever) - .setMessage(R.string.delete_all_records) - .setCancelable(false) - .setPositiveButton(R.string.btn_yes, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - presenter.deleteAllRecords(); - dialog.dismiss(); - } - }) - .setNegativeButton(R.string.btn_no, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.dismiss(); - } - }); - AlertDialog alert = builder.create(); - alert.show(); - break; +// case R.id.btnDeleteAll: +// AlertDialog.Builder builder = new AlertDialog.Builder(this); +// builder.setTitle(R.string.warning) +// .setIcon(R.drawable.ic_delete_forever) +// .setMessage(R.string.delete_all_records) +// .setCancelable(false) +// .setPositiveButton(R.string.btn_yes, new DialogInterface.OnClickListener() { +// public void onClick(DialogInterface dialog, int id) { +// presenter.deleteAllRecords(); +// dialog.dismiss(); +// } +// }) +// .setNegativeButton(R.string.btn_no, +// new DialogInterface.OnClickListener() { +// public void onClick(DialogInterface dialog, int id) { +// dialog.dismiss(); +// } +// }); +// AlertDialog alert = builder.create(); +// alert.show(); +// break; case R.id.btnRequest: requestFeature(); break; @@ -313,7 +336,9 @@ private void requestFeature() { "[" + getResources().getString(R.string.app_name) + "] - " + getResources().getString(R.string.request) ); try { - startActivity(Intent.createChooser(i, getResources().getString(R.string.send_email))); + Intent chooser = Intent.createChooser(i, getResources().getString(R.string.send_email)); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(chooser); } catch (android.content.ActivityNotFoundException ex) { showError(R.string.email_clients_not_found); } @@ -363,6 +388,11 @@ public void showRecordInStereo(boolean b) { swRecordInStereo.setChecked(b); } + @Override + public void showAskToRenameAfterRecordingStop(boolean b) { + swAskToRename.setChecked(b); + } + @Override public void showRecordingBitrate(int bitrate) { bitrateSelector.setSelection(bitrate); @@ -432,4 +462,9 @@ public void showError(String message) { public void showError(int resId) { Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); } + + @Override + public void showMessage(int resId) { + Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); + } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsContract.java b/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsContract.java index 4b039be2..c7d3868a 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsContract.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/settings/SettingsContract.java @@ -11,6 +11,8 @@ interface View extends Contract.View { void showKeepScreenOn(boolean b); void showRecordInStereo(boolean b); + void showAskToRenameAfterRecordingStop(boolean b); + void showRecordingBitrate(int bitrate); void showRecordingSampleRate(int rate); @@ -37,6 +39,8 @@ public interface UserActionsListener extends Contract.UserActionsListener= 0; i--) { +// path.lineTo(i * dpi, half + 1 + waveformData[i]); +// } +// path.lineTo(0, half); +// path.close(); +// canvas.drawPath(path, waveformPaint); + float dpi = AndroidUtils.dpToPx(1); - for (int i = 1; i < width; i++) { - path.lineTo(i * dpi, half - waveformData[i]); - } - for (int i = width - 1; i >= 0; i--) { - path.lineTo(i * dpi, half + 1 + waveformData[i]); + float[] lines = new float[width*4+4]; + int step = 0; + for (int i = 0; i < width; i++) { + lines[step] = i*dpi; + lines[step+1] = half + waveformData[i]; + lines[step+2] = i*dpi; + lines[step+3] = half - waveformData[i]; + step +=4; } - path.lineTo(0, half); - path.close(); - canvas.drawPath(path, waveformPaint); + //Horizontal zero line + lines[step] = 0; + lines[step+1] = half; + lines[step+2] = width*dpi; + lines[step+3] = half; + canvas.drawLines(lines, 0, lines.length, waveformPaint); } /** diff --git a/app/src/main/java/com/dimowner/audiorecorder/audio/SoundFile.java b/app/src/main/java/com/dimowner/audiorecorder/audio/SoundFile.java index d2c43a8c..32c24833 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/audio/SoundFile.java +++ b/app/src/main/java/com/dimowner/audiorecorder/audio/SoundFile.java @@ -31,6 +31,8 @@ import java.nio.ShortBuffer; import java.util.Arrays; +import timber.log.Timber; + /** * This class taken from Ringdroid app. * https://github.com/google/ringdroid @@ -39,6 +41,8 @@ public class SoundFile { private File mInputFile = null; + private boolean isFailed = false; + private float dpPerSec = AppConstants.SHORT_RECORD_DP_PER_SECOND; private int mFileSize; private int mSampleRate; @@ -62,7 +66,7 @@ private SoundFile() { } // Create and return a SoundFile object using the file fileName. - public static SoundFile create(String fileName) throws IOException, FileNotFoundException { + public static SoundFile create(String fileName) throws IOException, OutOfMemoryError, FileNotFoundException { // First check that the file exists and that its extension is supported. File f = new File(fileName); if (!f.exists()) { @@ -100,7 +104,7 @@ public int[] getFrameGains() { return mFrameGains; } - private void readFile(File inputFile) throws IOException { + private void readFile(File inputFile) throws IOException, OutOfMemoryError { MediaExtractor extractor = new MediaExtractor(); MediaFormat format = null; int i; @@ -124,11 +128,15 @@ private void readFile(File inputFile) throws IOException { mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); // Expected total number of samples per channel. - int expectedNumSamples = - (int) ((format.getLong(MediaFormat.KEY_DURATION) / 1000000.f) * mSampleRate + 0.5f); + int expectedNumSamples = 0; + try { + expectedNumSamples = (int) ((format.getLong(MediaFormat.KEY_DURATION) / 1000000.f) * mSampleRate + 0.5f); - //SoundFile duration. - duration = format.getLong(MediaFormat.KEY_DURATION); + //SoundFile duration. + duration = format.getLong(MediaFormat.KEY_DURATION); + } catch (Exception e) { + Timber.e(e); + } dpPerSec = ARApplication.getDpPerSecond((float) duration/1000000f); MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)); @@ -149,7 +157,13 @@ private void readFile(File inputFile) throws IOException { // For longer streams, the buffer size will be increased later on, calculating a rough // estimate of the total size needed to store all the samples in order to resize the buffer // only once. - mDecodedBytes = ByteBuffer.allocate(1 << 20); + try { + mDecodedBytes = ByteBuffer.allocate(1 << 20); + } catch (IllegalArgumentException e) { + Timber.e(e); + mDecodedBytes = ByteBuffer.allocate(1 << 10); + } + Boolean firstSampleData = true; while (true) { // read data from file and feed it to the decoder input buffers. @@ -215,6 +229,8 @@ private void readFile(File inputFile) throws IOException { if (retry == 0) { // Failed to allocate memory... Stop reading more data and finalize the // instance with the data decoded so far. + mFrameGains = new int[ARApplication.getLongWaveformSampleCount()]; + isFailed = true; break; } //ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize); @@ -257,31 +273,33 @@ private void readFile(File inputFile) throws IOException { codec.release(); codec = null; - // Temporary hack to make it work with the old version. - mNumFrames = mNumSamples / getSamplesPerFrame(); - if (mNumSamples % getSamplesPerFrame() != 0) { - mNumFrames++; - } - mFrameGains = new int[mNumFrames]; - int j; - int gain, value; - for (i = 0; i < mNumFrames; i++) { - gain = -1; - for (j = 0; j < getSamplesPerFrame(); j++) { - value = 0; - for (int k = 0; k < mChannels; k++) { - if (mDecodedSamples.remaining() > 0) { - value += Math.abs(mDecodedSamples.get()); + if (!isFailed) { + // Temporary hack to make it work with the old version. + mNumFrames = mNumSamples / getSamplesPerFrame(); + if (mNumSamples % getSamplesPerFrame() != 0) { + mNumFrames++; + } + mFrameGains = new int[mNumFrames]; + int j; + int gain, value; + for (i = 0; i < mNumFrames; i++) { + gain = -1; + for (j = 0; j < getSamplesPerFrame(); j++) { + value = 0; + for (int k = 0; k < mChannels; k++) { + if (mDecodedSamples.remaining() > 0) { + value += Math.abs(mDecodedSamples.get()); + } + } + value /= mChannels; + if (gain < value) { + gain = value; } } - value /= mChannels; - if (gain < value) { - gain = value; - } + mFrameGains[i] = (int) Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)... } - mFrameGains[i] = (int) Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)... + mDecodedSamples.rewind(); + // DumpSamples(); // Uncomment this line to dump the samples in a TSV file. } - mDecodedSamples.rewind(); - // DumpSamples(); // Uncomment this line to dump the samples in a TSV file. } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/audio/player/AudioPlayer.java b/app/src/main/java/com/dimowner/audiorecorder/audio/player/AudioPlayer.java index f95fed5c..67a67c13 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/audio/player/AudioPlayer.java +++ b/app/src/main/java/com/dimowner/audiorecorder/audio/player/AudioPlayer.java @@ -21,6 +21,7 @@ import com.dimowner.audiorecorder.AppConstants; import com.dimowner.audiorecorder.exception.AppException; +import com.dimowner.audiorecorder.exception.PermissionDeniedException; import com.dimowner.audiorecorder.exception.PlayerDataSourceException; import java.io.IOException; import java.util.ArrayList; @@ -70,16 +71,8 @@ public void setData(String data) { if (mediaPlayer != null && dataSource != null && dataSource.equals(data)) { //Do nothing } else { - try { - isPrepared = false; - mediaPlayer = new MediaPlayer(); - mediaPlayer.setDataSource(data); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - dataSource = data; - } catch (IOException e) { - Timber.e(e); - onError(new PlayerDataSourceException()); - } + dataSource = data; + restartPlayer(); } } @@ -90,53 +83,70 @@ private void restartPlayer() { mediaPlayer = new MediaPlayer(); mediaPlayer.setDataSource(dataSource); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException | IllegalStateException | SecurityException e) { Timber.e(e); - onError(new PlayerDataSourceException()); + if (e.getMessage().contains("Permission denied")) { + onError(new PermissionDeniedException()); + } else { + onError(new PlayerDataSourceException()); + } } } } @Override public void playOrPause() { - if (mediaPlayer != null) { - if (mediaPlayer.isPlaying()) { - pause(); - } else { - if (!isPrepared) { - try { - mediaPlayer.setOnPreparedListener(this); - mediaPlayer.prepareAsync(); - } catch (IllegalStateException ex) { - Timber.e(ex); - restartPlayer(); - mediaPlayer.setOnPreparedListener(this); - mediaPlayer.prepareAsync(); - } + try { + if (mediaPlayer != null) { + if (mediaPlayer.isPlaying()) { + pause(); } else { - mediaPlayer.start(); - mediaPlayer.seekTo((int) seekPos); - onStartPlay(); - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mp) { - stop(); - onStopPlay(); - } - }); - - timerProgress = new Timer(); - timerProgress.schedule(new TimerTask() { - @Override - public void run() { - if (mediaPlayer != null && mediaPlayer.isPlaying()) { - int curPos = mediaPlayer.getCurrentPosition(); - onPlayProgress(curPos); + if (!isPrepared) { + try { + mediaPlayer.setOnPreparedListener(this); + mediaPlayer.prepareAsync(); + } catch (IllegalStateException ex) { + Timber.e(ex); + restartPlayer(); + mediaPlayer.setOnPreparedListener(this); + try { + mediaPlayer.prepareAsync(); + } catch (IllegalStateException e) { + Timber.e(e); + restartPlayer(); } } - }, 0, AppConstants.VISUALIZATION_INTERVAL); + } else { + mediaPlayer.start(); + mediaPlayer.seekTo((int) seekPos); + onStartPlay(); + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + stop(); + onStopPlay(); + } + }); + + timerProgress = new Timer(); + timerProgress.schedule(new TimerTask() { + @Override + public void run() { + try { + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + int curPos = mediaPlayer.getCurrentPosition(); + onPlayProgress(curPos); + } + } catch(IllegalStateException e){ + Timber.e(e, "Player is not initialized!"); + } + } + }, 0, AppConstants.VISUALIZATION_INTERVAL); + } } } + } catch(IllegalStateException e){ + Timber.e(e, "Player is not initialized!"); } } @@ -164,9 +174,13 @@ public void onCompletion(MediaPlayer mp) { timerProgress.schedule(new TimerTask() { @Override public void run() { - if (mediaPlayer != null && mediaPlayer.isPlaying()) { - int curPos = mediaPlayer.getCurrentPosition(); - onPlayProgress(curPos); + try { + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + int curPos = mediaPlayer.getCurrentPosition(); + onPlayProgress(curPos); + } + } catch(IllegalStateException e){ + Timber.e(e, "Player is not initialized!"); } } }, 0, AppConstants.VISUALIZATION_INTERVAL); @@ -175,9 +189,13 @@ public void run() { @Override public void seek(long mills) { seekPos = mills; - if (mediaPlayer != null && mediaPlayer.isPlaying()) { - mediaPlayer.seekTo((int) seekPos); - onSeek((int) seekPos); + try { + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + mediaPlayer.seekTo((int) seekPos); + onSeek((int) seekPos); + } + } catch(IllegalStateException e){ + Timber.e(e, "Player is not initialized!"); } } @@ -214,7 +232,12 @@ public void stop() { @Override public boolean isPlaying() { - return mediaPlayer != null && mediaPlayer.isPlaying(); + try { + return mediaPlayer != null && mediaPlayer.isPlaying(); + } catch(IllegalStateException e){ + Timber.e(e, "Player is not initialized!"); + } + return false; } @Override diff --git a/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/AudioRecorder.java b/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/AudioRecorder.java index 26239579..8161f0f0 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/AudioRecorder.java +++ b/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/AudioRecorder.java @@ -106,7 +106,7 @@ public void startRecording() { if (recorderCallback != null) { recorderCallback.onStartRecord(); } - } catch (IllegalStateException e) { + } catch (RuntimeException e) { Timber.e(e, "startRecording() failed"); if (recorderCallback != null) { recorderCallback.onError(new RecorderInitException()); diff --git a/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/WavRecorder.java b/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/WavRecorder.java index f3f03310..d9537fa5 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/WavRecorder.java +++ b/app/src/main/java/com/dimowner/audiorecorder/audio/recorder/WavRecorder.java @@ -74,19 +74,30 @@ public void prepare(String outputFile, int channelCount, int sampleRate, int bit recordFile = new File(outputFile); if (recordFile.exists() && recordFile.isFile()) { int channel = channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO; - bufferSize = AudioRecord.getMinBufferSize(sampleRate, - channel, - AudioFormat.ENCODING_PCM_16BIT); - - recorder = new AudioRecord( - MediaRecorder.AudioSource.MIC, - sampleRate, - channel, - AudioFormat.ENCODING_PCM_16BIT, - bufferSize + try { + bufferSize = AudioRecord.getMinBufferSize(sampleRate, + channel, + AudioFormat.ENCODING_PCM_16BIT); + Timber.v("buffer size = %s", bufferSize); + if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) { + bufferSize = AudioRecord.getMinBufferSize(sampleRate, + channel, + AudioFormat.ENCODING_PCM_16BIT); + } + recorder = new AudioRecord( + MediaRecorder.AudioSource.MIC, + sampleRate, + channel, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize ); - - if (recorder.getState() == AudioRecord.STATE_INITIALIZED) { + } catch (IllegalArgumentException e) { + Timber.e(e, "sampleRate = " + sampleRate + " channel = " + channel + " bufferSize = " + bufferSize); + if (recorder != null) { + recorder.release(); + } + } + if (recorder != null && recorder.getState() == AudioRecord.STATE_INITIALIZED) { if (recorderCallback != null) { recorderCallback.onPrepareRecord(); } diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/Prefs.java b/app/src/main/java/com/dimowner/audiorecorder/data/Prefs.java index 62f256d2..b34f163d 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/Prefs.java +++ b/app/src/main/java/com/dimowner/audiorecorder/data/Prefs.java @@ -24,6 +24,9 @@ public interface Prefs { boolean isStoreDirPublic(); void setStoreDirPublic(boolean b); + boolean isAskToRenameAfterStopRecording(); + void setAskToRenameAfterStopRecording(boolean b); + long getActiveRecord(); void setActiveRecord(long id); @@ -47,4 +50,7 @@ public interface Prefs { void setSampleRate(int rate); int getSampleRate(); + + void setRecordOrder(int order); + int getRecordsOrder(); } diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/PrefsImpl.java b/app/src/main/java/com/dimowner/audiorecorder/data/PrefsImpl.java index f87e6cdb..6f764a0b 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/PrefsImpl.java +++ b/app/src/main/java/com/dimowner/audiorecorder/data/PrefsImpl.java @@ -30,6 +30,7 @@ public class PrefsImpl implements Prefs { private static final String PREF_KEY_IS_FIRST_RUN = "is_first_run"; private static final String PREF_KEY_IS_STORE_DIR_PUBLIC = "is_store_dir_public"; + private static final String PREF_KEY_IS_ASK_TO_RENAME_AFTER_STOP_RECORDING = "is_ask_rename_after_stop_recording"; private static final String PREF_KEY_ACTIVE_RECORD = "active_record"; private static final String PREF_KEY_RECORD_COUNTER = "record_counter"; private static final String PREF_KEY_THEME_COLORMAP_POSITION = "theme_color"; @@ -37,6 +38,7 @@ public class PrefsImpl implements Prefs { private static final String PREF_KEY_FORMAT = "pref_format"; private static final String PREF_KEY_BITRATE = "pref_bitrate"; private static final String PREF_KEY_SAMPLE_RATE = "pref_sample_rate"; + private static final String PREF_KEY_RECORDS_ORDER = "pref_records_order"; //Recording prefs. private static final String PREF_KEY_RECORD_CHANNEL_COUNT = "record_channel_count"; @@ -69,12 +71,15 @@ public boolean isFirstRun() { public void firstRunExecuted() { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean(PREF_KEY_IS_FIRST_RUN, false); + editor.putBoolean(PREF_KEY_IS_STORE_DIR_PUBLIC, true); + editor.putBoolean(PREF_KEY_IS_ASK_TO_RENAME_AFTER_STOP_RECORDING, true); editor.apply(); +// setStoreDirPublic(true); } @Override public boolean isStoreDirPublic() { - return sharedPreferences.contains(PREF_KEY_IS_STORE_DIR_PUBLIC) && sharedPreferences.getBoolean(PREF_KEY_IS_STORE_DIR_PUBLIC, false); + return sharedPreferences.contains(PREF_KEY_IS_STORE_DIR_PUBLIC) && sharedPreferences.getBoolean(PREF_KEY_IS_STORE_DIR_PUBLIC, true); } @Override @@ -84,6 +89,18 @@ public void setStoreDirPublic(boolean b) { editor.apply(); } + @Override + public boolean isAskToRenameAfterStopRecording() { + return sharedPreferences.contains(PREF_KEY_IS_ASK_TO_RENAME_AFTER_STOP_RECORDING) && sharedPreferences.getBoolean(PREF_KEY_IS_ASK_TO_RENAME_AFTER_STOP_RECORDING, true); + } + + @Override + public void setAskToRenameAfterStopRecording(boolean b) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(PREF_KEY_IS_ASK_TO_RENAME_AFTER_STOP_RECORDING, b); + editor.apply(); + } + @Override public long getActiveRecord() { return sharedPreferences.getLong(PREF_KEY_ACTIVE_RECORD, -1); @@ -179,4 +196,16 @@ public void setSampleRate(int rate) { public int getSampleRate() { return sharedPreferences.getInt(PREF_KEY_SAMPLE_RATE, AppConstants.RECORD_SAMPLE_RATE_44100); } + + @Override + public void setRecordOrder(int order) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(PREF_KEY_RECORDS_ORDER, order); + editor.apply(); + } + + @Override + public int getRecordsOrder() { + return sharedPreferences.getInt(PREF_KEY_RECORDS_ORDER, AppConstants.SORT_DATE); + } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/database/DataSource.java b/app/src/main/java/com/dimowner/audiorecorder/data/database/DataSource.java index d973afe0..b9fbd188 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/database/DataSource.java +++ b/app/src/main/java/com/dimowner/audiorecorder/data/database/DataSource.java @@ -26,6 +26,7 @@ import android.database.sqlite.SQLiteDatabase; import android.util.Log; +import com.dimowner.audiorecorder.AppConstants; import com.dimowner.audiorecorder.BuildConfig; /** @@ -135,6 +136,30 @@ public ArrayList getAll() { return convertCursor(cursor); } + /** + * Get records from database for table T. + * @return List that contains all records of table T. + */ + public ArrayList getRecords(int page) { + Cursor cursor = queryLocal("SELECT * FROM " + tableName + + " ORDER BY " + SQLiteHelper.COLUMN_DATE_ADDED + " DESC" + + " LIMIT " + AppConstants.DEFAULT_PER_PAGE + + " OFFSET " + (page-1) * AppConstants.DEFAULT_PER_PAGE); + return convertCursor(cursor); + } + + /** + * Get records from database for table T. + * @return List that contains all records of table T. + */ + public ArrayList getRecords(int page, String order) { + Cursor cursor = queryLocal("SELECT * FROM " + tableName + + " ORDER BY " + order + + " LIMIT " + AppConstants.DEFAULT_PER_PAGE + + " OFFSET " + (page-1) * AppConstants.DEFAULT_PER_PAGE); + return convertCursor(cursor); + } + /** * Delete all records from the table * @throws SQLException on error diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepository.java b/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepository.java index c0e54721..dc433bdc 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepository.java +++ b/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepository.java @@ -29,6 +29,10 @@ public interface LocalRepository { List getAllRecords(); + List getRecords(int page); + + List getRecords(int page, int order); + boolean deleteAllRecords(); Record getLastRecord(); @@ -41,7 +45,7 @@ public interface LocalRepository { long insertFile(String filePath, long duration, int[] waveform) throws IOException; - boolean updateWaveform(int id) throws IOException; + boolean updateWaveform(int id) throws IOException, OutOfMemoryError; void deleteRecord(int id); diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepositoryImpl.java b/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepositoryImpl.java index 7ae3a3f6..87f8bc67 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepositoryImpl.java +++ b/app/src/main/java/com/dimowner/audiorecorder/data/database/LocalRepositoryImpl.java @@ -19,6 +19,7 @@ import android.database.Cursor; import android.database.SQLException; +import com.dimowner.audiorecorder.AppConstants; import com.dimowner.audiorecorder.audio.SoundFile; import java.io.File; @@ -169,7 +170,7 @@ public long insertFile(String path, long duration, int[] waveform) throws IOExce } @Override - public boolean updateWaveform(int id) throws IOException { + public boolean updateWaveform(int id) throws IOException, OutOfMemoryError { Record record = getRecord(id); String path = record.getPath(); if (path != null && !path.isEmpty()) { @@ -215,6 +216,48 @@ public List getAllRecords() { return list; } + @Override + public List getRecords(int page) { + if (!dataSource.isOpen()) { + dataSource.open(); + } + List list = dataSource.getRecords(page); + //Remove not records with not existing audio files (which was lost or deleted) + for (int i = 0; i < list.size(); i++) { + if (!isFileExists(list.get(i).getPath())) { + dataSource.deleteItem(list.get(i).getId()); + } + } + return list; + } + + @Override + public List getRecords(int page, int order) { + if (!dataSource.isOpen()) { + dataSource.open(); + } + String orderStr; + switch (order) { + case AppConstants.SORT_NAME: + orderStr = SQLiteHelper.COLUMN_NAME + " ASC"; + break; + case AppConstants.SORT_DURATION: + orderStr = SQLiteHelper.COLUMN_DURATION + " DESC"; + break; + case AppConstants.SORT_DATE: + default: + orderStr = SQLiteHelper.COLUMN_DATE_ADDED + " DESC"; + } + List list = dataSource.getRecords(page, orderStr); + //Remove not records with not existing audio files (which was lost or deleted) + for (int i = 0; i < list.size(); i++) { + if (!isFileExists(list.get(i).getPath())) { + dataSource.deleteItem(list.get(i).getId()); + } + } + return list; + } + @Override public Record getLastRecord() { if (!dataSource.isOpen()) { diff --git a/app/src/main/java/com/dimowner/audiorecorder/exception/AppException.java b/app/src/main/java/com/dimowner/audiorecorder/exception/AppException.java index f6f42105..cf56ae36 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/exception/AppException.java +++ b/app/src/main/java/com/dimowner/audiorecorder/exception/AppException.java @@ -22,7 +22,9 @@ public abstract class AppException extends Exception { public static final int INVALID_OUTPUT_FILE = 2; public static final int RECORDER_INIT_EXCEPTION = 3; public static final int PLAYER_INIT_EXCEPTION = 4; - public static final int PLAYER_DATA_SOURCE_EXCEPTION= 5; + public static final int PLAYER_DATA_SOURCE_EXCEPTION = 5; + public static final int CANT_PROCESS_RECORD = 6; + public static final int READ_PERMISSION_DENIED = 7; public abstract int getType(); } diff --git a/app/src/main/java/com/dimowner/audiorecorder/exception/CantProcessRecord.java b/app/src/main/java/com/dimowner/audiorecorder/exception/CantProcessRecord.java new file mode 100644 index 00000000..977c8451 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/exception/CantProcessRecord.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018 Dmitriy Ponomarenko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dimowner.audiorecorder.exception; + +public class CantProcessRecord extends AppException { + @Override + public int getType() { + return AppException.CANT_PROCESS_RECORD; + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/exception/ErrorParser.java b/app/src/main/java/com/dimowner/audiorecorder/exception/ErrorParser.java index 3efdc091..f032741d 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/exception/ErrorParser.java +++ b/app/src/main/java/com/dimowner/audiorecorder/exception/ErrorParser.java @@ -33,6 +33,10 @@ public static int parseException(AppException e) { return R.string.error_player_data_source; } else if (e.getType() == AppException.PLAYER_INIT_EXCEPTION) { return R.string.error_failed_to_init_player; + } else if (e.getType() == AppException.CANT_PROCESS_RECORD) { + return R.string.error_process_waveform; + } else if (e.getType() == AppException.READ_PERMISSION_DENIED) { + return R.string.error_permission_denied; } return R.string.error_unknown; } diff --git a/app/src/main/java/com/dimowner/audiorecorder/exception/PermissionDeniedException.java b/app/src/main/java/com/dimowner/audiorecorder/exception/PermissionDeniedException.java new file mode 100644 index 00000000..1796be70 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/exception/PermissionDeniedException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018 Dmitriy Ponomarenko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dimowner.audiorecorder.exception; + +public class PermissionDeniedException extends AppException { + @Override + public int getType() { + return AppException.READ_PERMISSION_DENIED; + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/util/AndroidUtils.java b/app/src/main/java/com/dimowner/audiorecorder/util/AndroidUtils.java index 847dd773..18284152 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/util/AndroidUtils.java +++ b/app/src/main/java/com/dimowner/audiorecorder/util/AndroidUtils.java @@ -17,22 +17,42 @@ package com.dimowner.audiorecorder.util; import android.app.Activity; +import android.app.Dialog; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.media.MediaExtractor; import android.media.MediaFormat; +import android.net.Uri; import android.os.Build; +import android.support.v4.content.FileProvider; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; import android.view.Display; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; import com.dimowner.audiorecorder.ARApplication; +import com.dimowner.audiorecorder.R; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.text.DecimalFormat; import timber.log.Timber; @@ -107,6 +127,38 @@ public static int getStatusBarHeight(Context context) { return result; } + // A method to find height of the navigation bar + public static int getNavigationBarHeight(Context context) { + int result = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try { + if (hasNavBar(context)) { + int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + } + } catch (Resources.NotFoundException e) { + Timber.e(e); + return 0; + } + } + return result; + } + + //This method works not correctly + public static boolean hasNavBar (Context context) { +// int id = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); +// return id > 0 && context.getResources().getBoolean(id); +// boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); +// boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); +// return !hasMenuKey && !hasBackKey; + + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + boolean hasHomeKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME); + return !hasHomeKey && !hasBackKey; + } + public static void setTranslucent(Activity activity, boolean translucent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Window w = activity.getWindow(); @@ -212,4 +264,144 @@ public static long readRecordDuration(File file) { } return -1; } + + /** + * Moves icons from the PopupMenu MenuItems' icon fields into the menu title as a Spannable with the icon and title text. + */ + public static void insertMenuItemIcons(Context context, PopupMenu popupMenu) { + Menu menu = popupMenu.getMenu(); + if (hasIcon(menu)) { + for (int i = 0; i < menu.size(); i++) { + insertMenuItemIcon(context, menu.getItem(i)); + } + } + } + + /** + * @return true if the menu has at least one MenuItem with an icon. + */ + private static boolean hasIcon(Menu menu) { + for (int i = 0; i < menu.size(); i++) { + if (menu.getItem(i).getIcon() != null) return true; + } + return false; + } + + /** + * Converts the given MenuItem title into a Spannable containing both its icon and title. + */ + private static void insertMenuItemIcon(Context context, MenuItem menuItem) { + Drawable icon = menuItem.getIcon(); + + // If there no icon, we insert a transparent one to keep the title aligned with the items + // which do have icons. + if (icon == null) icon = new ColorDrawable(Color.TRANSPARENT); + + int iconSize = context.getResources().getDimensionPixelSize(R.dimen.menu_item_icon_size); + icon.setBounds(0, 0, iconSize, iconSize); + ImageSpan imageSpan = new ImageSpan(icon); + + // Add a space placeholder for the icon, before the title. + SpannableStringBuilder ssb = new SpannableStringBuilder(" " + menuItem.getTitle()); + + // Replace the space placeholder with the icon. + ssb.setSpan(imageSpan, 1, 2, 0); + menuItem.setTitle(ssb); + // Set the icon to null just in case, on some weird devices, they've customized Android to display + // the icon in the menu... we don't want two icons to appear. + menuItem.setIcon(null); + } + + public static void shareAudioFile(Context context, String sharePath, String name) { + if (sharePath != null) { + Uri fileUri = FileProvider.getUriForFile( + context, + context.getApplicationContext().getPackageName() + ".app_file_provider", + new File(sharePath) + ); + Intent share = new Intent(Intent.ACTION_SEND); + share.setType("audio/*"); + share.putExtra(Intent.EXTRA_STREAM, fileUri); + share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + Intent chooser = Intent.createChooser(share, context.getResources().getString(R.string.share_record, name)); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(chooser); + } else { + Timber.e("There no record selected!"); + Toast.makeText(context, R.string.please_select_record_to_share, Toast.LENGTH_LONG).show(); + } + } + + public static void openAudioFile(Context context, String sharePath, String name) { + if (sharePath != null) { + Uri fileUri = FileProvider.getUriForFile( + context, + context.getApplicationContext().getPackageName() + ".app_file_provider", + new File(sharePath) + ); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(fileUri, "audio/*"); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + Intent chooser = Intent.createChooser(intent, context.getResources().getString(R.string.open_record_with, name)); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(chooser); + } else { + Timber.e("There no record selected!"); + Toast.makeText(context, R.string.error_unknown, Toast.LENGTH_LONG).show(); + } + } + + public static void showDialog(Activity activity, int resTitle, int resContent, + View.OnClickListener positiveBtnListener, View.OnClickListener negativeBtnListener){ + showDialog(activity, -1, -1, resTitle, resContent, positiveBtnListener, negativeBtnListener); + } + + public static void showDialog(Activity activity, int positiveBtnTextRes, int negativeBtnTextRes, int resTitle, int resContent, + final View.OnClickListener positiveBtnListener, final View.OnClickListener negativeBtnListener){ + final Dialog dialog = new Dialog(activity); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCancelable(false); + View view = activity.getLayoutInflater().inflate(R.layout.dialog_layout, null, false); + ((TextView)view.findViewById(R.id.dialog_title)).setText(resTitle); + ((TextView)view.findViewById(R.id.dialog_content)).setText(resContent); + if (negativeBtnListener != null) { + Button negativeBtn = view.findViewById(R.id.dialog_negative_btn); + if (negativeBtnTextRes >=0) { + negativeBtn.setText(negativeBtnTextRes); + } + negativeBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + negativeBtnListener.onClick(v); + dialog.dismiss(); + } + }); + } else { + view.findViewById(R.id.dialog_negative_btn).setVisibility(View.GONE); + } + if (positiveBtnListener != null) { + Button positiveBtn = view.findViewById(R.id.dialog_positive_btn); + if (positiveBtnTextRes >=0) { + positiveBtn.setText(positiveBtnTextRes); + } + positiveBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + positiveBtnListener.onClick(v); + dialog.dismiss(); + } + }); + } else { + view.findViewById(R.id.dialog_positive_btn).setVisibility(View.GONE); + } + dialog.setContentView(view); + dialog.show(); + } + + public static String formatSize(long size) { + DecimalFormat formatter = new DecimalFormat("#.##"); + return formatter.format((float)size/(1024*1024)) + " Mb"; + } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/util/FileUtil.java b/app/src/main/java/com/dimowner/audiorecorder/util/FileUtil.java index 102d8a0a..eb471a10 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/util/FileUtil.java +++ b/app/src/main/java/com/dimowner/audiorecorder/util/FileUtil.java @@ -139,6 +139,38 @@ public static boolean copyFile(FileDescriptor fileToCopy, File newFile) throws I } } + /** + * Copy file. + * @param fileToCopy File to copy. + * @param newFile File in which will contain copied data. + * @return true if copy succeed, otherwise - false. + */ + public static boolean copyFile(File fileToCopy, File newFile) throws IOException { + Timber.v("copyFile toCOpy = " + fileToCopy.getAbsolutePath() + " newFile = " + newFile.getAbsolutePath()); + FileInputStream in = null; + FileOutputStream out = null; + try { + in = new FileInputStream(fileToCopy); + out = new FileOutputStream(newFile); + + if (copyLarge(in, out) > 0) { + return true; + } else { + Timber.e("Nothing was copied!"); + return false; + } + } catch (Exception e) { + return false; + } finally { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } + } + /** * Get free space for specified file * @param f Dir @@ -287,6 +319,11 @@ public static boolean isExternalStorageReadable() { Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)); } + public static boolean isFileInExternalStorage(String path) { + String external = Environment.getExternalStorageDirectory().getAbsolutePath(); + return path.contains(external); + } + public static File getPublicMusicStorageDir(String albumName) { File file = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_MUSIC), albumName); diff --git a/app/src/main/java/com/dimowner/audiorecorder/util/TimeUtils.java b/app/src/main/java/com/dimowner/audiorecorder/util/TimeUtils.java index 8a580c3a..017a017b 100755 --- a/app/src/main/java/com/dimowner/audiorecorder/util/TimeUtils.java +++ b/app/src/main/java/com/dimowner/audiorecorder/util/TimeUtils.java @@ -71,7 +71,7 @@ public static String formatTimeIntervalHourMinSec2(long length) { if (numHour == 0) { return String.format(Locale.getDefault(), "%02d:%02d", numMinutes, numSeconds % 60); } else { - return String.format(Locale.getDefault(), "%02d:%02d:%02d", numHour, numMinutes, numSeconds % 60); + return String.format(Locale.getDefault(), "%02d:%02d:%02d", numHour, numMinutes % 60, numSeconds % 60); } } diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml new file mode 100644 index 00000000..ae994688 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_calendar_today.xml b/app/src/main/res/drawable/ic_calendar_today.xml new file mode 100644 index 00000000..f267d069 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_today.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..650f3807 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 00000000..79db2ee7 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_transparent.xml b/app/src/main/res/drawable/ic_more_vert_transparent.xml new file mode 100644 index 00000000..5de1a38b --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_transparent.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_with.xml b/app/src/main/res/drawable/ic_open_with.xml new file mode 100644 index 00000000..78001ed2 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_with.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pencil.xml b/app/src/main/res/drawable/ic_pencil.xml index 73ed8d58..881a2bde 100644 --- a/app/src/main/res/drawable/ic_pencil.xml +++ b/app/src/main/res/drawable/ic_pencil.xml @@ -1,9 +1,9 @@ diff --git a/app/src/main/res/drawable/ic_pencil_small.xml b/app/src/main/res/drawable/ic_pencil_small.xml new file mode 100644 index 00000000..73ed8d58 --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil_small.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save_alt.xml b/app/src/main/res/drawable/ic_save_alt.xml new file mode 100644 index 00000000..5ae0dcb4 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_alt.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 00000000..43061183 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort_by_alpha.xml b/app/src/main/res/drawable/ic_sort_by_alpha.xml new file mode 100644 index 00000000..755bdb9e --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_by_alpha.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/raised_button_background.xml b/app/src/main/res/drawable/raised_button_background.xml new file mode 100644 index 00000000..14ec274d --- /dev/null +++ b/app/src/main/res/drawable/raised_button_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_info.xml b/app/src/main/res/layout/activity_info.xml new file mode 100644 index 00000000..edc99da0 --- /dev/null +++ b/app/src/main/res/layout/activity_info.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0d4ba76d..7d99746b 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -35,7 +35,7 @@ android:layout_height="wrap_content" android:contentDescription="@null" android:layout_gravity="start" - android:background="?android:selectableItemBackground" + android:background="?android:selectableItemBackgroundBorderless" android:padding="@dimen/spacing_normal" android:src="@drawable/ic_import"/> @@ -62,7 +62,7 @@ android:contentDescription="@null" android:layout_gravity="end" android:scaleType="center" - android:src="@drawable/ic_share"/> + android:src="@drawable/ic_more_vert"/> @@ -210,7 +210,7 @@ android:textColor="@color/text_primary_light" android:maxLines="1" android:textSize="@dimen/text_large" - android:drawableEnd="@drawable/ic_pencil" + android:drawableEnd="@drawable/ic_pencil_small" android:visibility="invisible" tools:text="@string/app_name" /> diff --git a/app/src/main/res/layout/activity_records.xml b/app/src/main/res/layout/activity_records.xml index 8343df22..274221e6 100644 --- a/app/src/main/res/layout/activity_records.xml +++ b/app/src/main/res/layout/activity_records.xml @@ -38,34 +38,62 @@ android:layout_height="wrap_content" android:contentDescription="@null" android:layout_gravity="start" - android:background="?android:selectableItemBackground" + android:background="?android:selectableItemBackgroundBorderless" android:padding="@dimen/spacing_normal" android:src="@drawable/ic_arrow_back"/> - + android:orientation="vertical"> + + + + + @@ -192,7 +220,7 @@ android:textColor="@color/text_primary_light" android:textSize="@dimen/text_xmedium" android:maxLines="1" - android:drawableEnd="@drawable/ic_pencil" + android:drawableEnd="@drawable/ic_pencil_small" tools:text="Record 2321"/> @@ -38,7 +36,7 @@ android:layout_height="wrap_content" android:contentDescription="@null" android:layout_gravity="start" - android:background="?android:selectableItemBackground" + android:background="?android:selectableItemBackgroundBorderless" android:padding="@dimen/spacing_normal" android:src="@drawable/ic_arrow_back"/> @@ -126,6 +124,33 @@ /> + + + + + + - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_layout.xml b/app/src/main/res/layout/dialog_layout.xml new file mode 100644 index 00000000..acbdddd9 --- /dev/null +++ b/app/src/main/res/layout/dialog_layout.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + +