diff --git a/app/build.gradle b/app/build.gradle index e407314..3409246 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,7 +63,7 @@ android { } dependencies { - implementation 'com.github.SimpleMobileTools:Simple-Commons:42733f39a4' + implementation 'com.github.SimpleMobileTools:Simple-Commons:f76d729b9d' implementation 'org.greenrobot:eventbus:3.3.1' implementation 'com.github.Armen101:AudioRecordView:1.0.4' implementation 'androidx.documentfile:documentfile:1.0.1' diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/MainActivity.kt index 47b24d9..a80ac09 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/MainActivity.kt @@ -12,6 +12,7 @@ import com.simplemobiletools.commons.models.FAQItem import com.simplemobiletools.voicerecorder.BuildConfig import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.adapters.ViewPagerAdapter +import com.simplemobiletools.voicerecorder.extensions.checkRecycleBinItems import com.simplemobiletools.voicerecorder.extensions.config import com.simplemobiletools.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE import com.simplemobiletools.voicerecorder.models.Events @@ -40,6 +41,10 @@ class MainActivity : SimpleActivity() { return } + if (savedInstanceState == null) { + checkRecycleBinItems() + } + handlePermission(PERMISSION_RECORD_AUDIO) { if (it) { tryInitVoiceRecorder() @@ -65,6 +70,9 @@ class MainActivity : SimpleActivity() { super.onResume() setupTabColors() updateMenuColors() + if (getPagerAdapter()?.showRecycleBin != config.useRecycleBin) { + setupViewPager() + } getPagerAdapter()?.onResume() } @@ -149,8 +157,12 @@ class MainActivity : SimpleActivity() { private fun setupViewPager() { main_tabs_holder.removeAllTabs() - val tabDrawables = arrayOf(R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector) - val tabLabels = arrayOf(R.string.recorder, R.string.player) + var tabDrawables = arrayOf(R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector) + var tabLabels = arrayOf(R.string.recorder, R.string.player) + if (config.useRecycleBin) { + tabDrawables += R.drawable.ic_delete_vector + tabLabels += R.string.recycle_bin + } tabDrawables.forEachIndexed { i, drawableId -> main_tabs_holder.newTab().setCustomView(R.layout.bottom_tablayout_item).apply { @@ -174,7 +186,7 @@ class MainActivity : SimpleActivity() { } ) - view_pager.adapter = ViewPagerAdapter(this) + view_pager.adapter = ViewPagerAdapter(this, config.useRecycleBin) view_pager.onPageChangeListener { main_tabs_holder.getTabAt(it)?.select() (view_pager.adapter as ViewPagerAdapter).finishActMode() diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/SettingsActivity.kt index abae0f9..0d2062f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/activities/SettingsActivity.kt @@ -3,24 +3,26 @@ package com.simplemobiletools.voicerecorder.activities import android.content.Intent import android.media.MediaRecorder import android.os.Bundle -import com.simplemobiletools.commons.dialogs.ChangeDateTimeFormatDialog -import com.simplemobiletools.commons.dialogs.FeatureLockedDialog -import com.simplemobiletools.commons.dialogs.FilePickerDialog -import com.simplemobiletools.commons.dialogs.RadioGroupDialog +import com.simplemobiletools.commons.dialogs.* import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.extensions.config +import com.simplemobiletools.voicerecorder.extensions.emptyTheRecycleBin +import com.simplemobiletools.voicerecorder.extensions.getAllRecordings import com.simplemobiletools.voicerecorder.helpers.BITRATES import com.simplemobiletools.voicerecorder.helpers.EXTENSION_M4A import com.simplemobiletools.voicerecorder.helpers.EXTENSION_MP3 import com.simplemobiletools.voicerecorder.helpers.EXTENSION_OGG +import com.simplemobiletools.voicerecorder.models.Events import kotlinx.android.synthetic.main.activity_settings.* -import java.util.* +import org.greenrobot.eventbus.EventBus +import java.util.Locale import kotlin.system.exitProcess class SettingsActivity : SimpleActivity() { + private var recycleBinContentSize = 0 override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true @@ -47,9 +49,11 @@ class SettingsActivity : SimpleActivity() { setupBitrate() setupAudioSource() setupRecordAfterLaunch() + setupUseRecycleBin() + setupEmptyRecycleBin() updateTextColors(settings_nested_scrollview) - arrayOf(settings_color_customization_section_label, settings_general_settings_label).forEach { + arrayOf(settings_color_customization_section_label, settings_general_settings_label, settings_recycle_bin_label).forEach { it.setTextColor(getProperPrimaryColor()) } } @@ -178,6 +182,48 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupUseRecycleBin() { + updateRecycleBinButtons() + settings_use_recycle_bin.isChecked = config.useRecycleBin + settings_use_recycle_bin_holder.setOnClickListener { + settings_use_recycle_bin.toggle() + config.useRecycleBin = settings_use_recycle_bin.isChecked + updateRecycleBinButtons() + } + } + + private fun updateRecycleBinButtons() { + settings_empty_recycle_bin_holder.beVisibleIf(config.useRecycleBin) + } + + private fun setupEmptyRecycleBin() { + ensureBackgroundThread { + try { + recycleBinContentSize = getAllRecordings(trashed = true).sumByInt { + it.size + } + } catch (ignored: Exception) { + } + + runOnUiThread { + settings_empty_recycle_bin_size.text = recycleBinContentSize.formatSize() + } + } + + settings_empty_recycle_bin_holder.setOnClickListener { + if (recycleBinContentSize == 0) { + toast(R.string.recycle_bin_empty) + } else { + ConfirmationDialog(this, "", R.string.empty_recycle_bin_confirmation, R.string.yes, R.string.no) { + emptyTheRecycleBin() + recycleBinContentSize = 0 + settings_empty_recycle_bin_size.text = 0.formatSize() + EventBus.getDefault().post(Events.RecordingTrashUpdated()) + } + } + } + } + private fun setupAudioSource() { settings_audio_source.text = config.getAudioSourceText(config.audioSource) settings_audio_source_holder.setOnClickListener { diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt index 53424ac..4a994bc 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt @@ -1,28 +1,28 @@ package com.simplemobiletools.voicerecorder.adapters -import android.provider.MediaStore -import android.provider.MediaStore.Audio.Media import android.view.* import android.widget.PopupMenu import android.widget.TextView -import androidx.core.net.toUri import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter -import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.isQPlus -import com.simplemobiletools.commons.helpers.isRPlus import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.voicerecorder.BuildConfig import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.activities.SimpleActivity +import com.simplemobiletools.voicerecorder.dialogs.DeleteConfirmationDialog import com.simplemobiletools.voicerecorder.dialogs.RenameRecordingDialog +import com.simplemobiletools.voicerecorder.extensions.config +import com.simplemobiletools.voicerecorder.extensions.deleteRecordings +import com.simplemobiletools.voicerecorder.extensions.moveRecordingsToRecycleBin import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener +import com.simplemobiletools.voicerecorder.models.Events import com.simplemobiletools.voicerecorder.models.Recording import kotlinx.android.synthetic.main.item_recording.view.* -import java.io.File +import org.greenrobot.eventbus.EventBus class RecordingsAdapter( activity: SimpleActivity, @@ -135,12 +135,21 @@ class RecordingsAdapter( resources.getQuantityString(R.plurals.delete_recordings, itemsCnt, itemsCnt) } - val baseString = R.string.delete_recordings_confirmation + val baseString = if (activity.config.useRecycleBin) { + R.string.move_to_recycle_bin_confirmation + } else { + R.string.delete_recordings_confirmation + } val question = String.format(resources.getString(baseString), items) - ConfirmationDialog(activity, question) { + DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin) { skipRecycleBin -> ensureBackgroundThread { - deleteMediaStoreRecordings() + val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin + if (toRecycleBin) { + moveMediaStoreRecordingsToRecycleBin() + } else { + deleteMediaStoreRecordings() + } } } } @@ -154,38 +163,26 @@ class RecordingsAdapter( val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList val positions = getSelectedItemPositions() - when { - isRPlus() -> { - val fileUris = recordingsToRemove.map { recording -> - "${Media.EXTERNAL_CONTENT_URI}/${recording.id.toLong()}".toUri() - } - - activity.deleteSDK30Uris(fileUris) { success -> - if (success) { - doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions) - } - } - } - isQPlus() -> { - recordingsToRemove.forEach { - val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - val selection = "${Media._ID} = ?" - val selectionArgs = arrayOf(it.id.toString()) - val result = activity.contentResolver.delete(uri, selection, selectionArgs) - - if (result == 0) { - recordingsToRemove.forEach { - activity.deleteFile(File(it.path).toFileDirItem(activity)) - } - } - } + activity.deleteRecordings(recordingsToRemove) { success -> + if (success) { doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions) } - else -> { - recordingsToRemove.forEach { - activity.deleteFile(File(it.path).toFileDirItem(activity)) - } + } + } + + private fun moveMediaStoreRecordingsToRecycleBin() { + if (selectedKeys.isEmpty()) { + return + } + + val oldRecordingIndex = recordings.indexOfFirst { it.id == currRecordingId } + val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList + val positions = getSelectedItemPositions() + + activity.moveRecordingsToRecycleBin(recordingsToRemove) { success -> + if (success) { doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions) + EventBus.getDefault().post(Events.RecordingTrashUpdated()) } } } @@ -269,16 +266,19 @@ class RecordingsAdapter( renameRecording() } } + R.id.cab_share -> { executeItemMenuOperation(recordingId) { shareRecordings() } } + R.id.cab_open_with -> { executeItemMenuOperation(recordingId) { openRecordingWith() } } + R.id.cab_delete -> { executeItemMenuOperation(recordingId, removeAfterCallback = false) { askConfirmDelete() diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/TrashAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/TrashAdapter.kt new file mode 100644 index 0000000..57d502a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/TrashAdapter.kt @@ -0,0 +1,219 @@ +package com.simplemobiletools.voicerecorder.adapters + +import android.view.* +import android.widget.PopupMenu +import android.widget.TextView +import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.dialogs.ConfirmationDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.commons.views.MyRecyclerView +import com.simplemobiletools.voicerecorder.R +import com.simplemobiletools.voicerecorder.activities.SimpleActivity +import com.simplemobiletools.voicerecorder.extensions.deleteRecordings +import com.simplemobiletools.voicerecorder.extensions.restoreRecordings +import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener +import com.simplemobiletools.voicerecorder.models.Events +import com.simplemobiletools.voicerecorder.models.Recording +import kotlinx.android.synthetic.main.item_recording.view.* +import org.greenrobot.eventbus.EventBus + +class TrashAdapter( + activity: SimpleActivity, + var recordings: ArrayList, + val refreshListener: RefreshRecordingsListener, + recyclerView: MyRecyclerView +) : + MyRecyclerViewAdapter(activity, recyclerView, {}), RecyclerViewFastScroller.OnPopupTextUpdate { + + init { + setupDragListener(true) + } + + override fun getActionMenuId() = R.menu.cab_trash + + override fun prepareActionMode(menu: Menu) {} + + override fun actionItemPressed(id: Int) { + if (selectedKeys.isEmpty()) { + return + } + + when (id) { + R.id.cab_restore -> restoreRecordings() + R.id.cab_delete -> askConfirmDelete() + R.id.cab_select_all -> selectAll() + } + } + + override fun getSelectableItemCount() = recordings.size + + override fun getIsItemSelectable(position: Int) = true + + override fun getItemSelectionKey(position: Int) = recordings.getOrNull(position)?.id + + override fun getItemKeyPosition(key: Int) = recordings.indexOfFirst { it.id == key } + + override fun onActionModeCreated() {} + + override fun onActionModeDestroyed() {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_recording, parent) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val recording = recordings[position] + holder.bindView(recording, true, true) { itemView, layoutPosition -> + setupView(itemView, recording) + } + bindViewHolder(holder) + } + + override fun getItemCount() = recordings.size + + private fun getItemWithKey(key: Int): Recording? = recordings.firstOrNull { it.id == key } + + fun updateItems(newItems: ArrayList) { + if (newItems.hashCode() != recordings.hashCode()) { + recordings = newItems + notifyDataSetChanged() + finishActMode() + } + } + + private fun restoreRecordings() { + if (selectedKeys.isEmpty()) { + return + } + + val recordingsToRestore = recordings.filter { selectedKeys.contains(it.id) } as ArrayList + val positions = getSelectedItemPositions() + + activity.restoreRecordings(recordingsToRestore) { success -> + if (success) { + doDeleteAnimation(recordingsToRestore, positions) + EventBus.getDefault().post(Events.RecordingTrashUpdated()) + } + } + } + + private fun askConfirmDelete() { + val itemsCnt = selectedKeys.size + val firstItem = getSelectedItems().firstOrNull() ?: return + val items = if (itemsCnt == 1) { + "\"${firstItem.title}\"" + } else { + resources.getQuantityString(R.plurals.delete_recordings, itemsCnt, itemsCnt) + } + + val baseString = R.string.delete_recordings_confirmation + val question = String.format(resources.getString(baseString), items) + + ConfirmationDialog(activity, question) { + ensureBackgroundThread { + deleteMediaStoreRecordings() + } + } + } + + private fun deleteMediaStoreRecordings() { + if (selectedKeys.isEmpty()) { + return + } + + val recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList + val positions = getSelectedItemPositions() + + activity.deleteRecordings(recordingsToRemove) { success -> + if (success) { + doDeleteAnimation(recordingsToRemove, positions) + } + } + } + + private fun doDeleteAnimation(recordingsToRemove: ArrayList, positions: ArrayList) { + recordings.removeAll(recordingsToRemove.toSet()) + activity.runOnUiThread { + if (recordings.isEmpty()) { + refreshListener.refreshRecordings() + finishActMode() + } else { + positions.sortDescending() + removeSelectedItems(positions) + } + } + } + + private fun getSelectedItems() = recordings.filter { selectedKeys.contains(it.id) } as ArrayList + + private fun setupView(view: View, recording: Recording) { + view.apply { + setupViewBackground(activity) + recording_frame?.isSelected = selectedKeys.contains(recording.id) + + arrayListOf(recording_title, recording_date, recording_duration, recording_size).forEach { + it.setTextColor(textColor) + } + + recording_title.text = recording.title + recording_date.text = recording.timestamp.formatDate(context) + recording_duration.text = recording.duration.getFormattedDuration() + recording_size.text = recording.size.formatSize() + + overflow_menu_icon.drawable.apply { + mutate() + setTint(activity.getProperTextColor()) + } + + overflow_menu_icon.setOnClickListener { + showPopupMenu(overflow_menu_anchor, recording) + } + } + } + + override fun onChange(position: Int) = recordings.getOrNull(position)?.title ?: "" + + private fun showPopupMenu(view: View, recording: Recording) { + if (selectedKeys.isNotEmpty()) { + selectedKeys.clear() + notifyDataSetChanged() + } + + finishActMode() + val theme = activity.getPopupMenuTheme() + val contextTheme = ContextThemeWrapper(activity, theme) + + PopupMenu(contextTheme, view, Gravity.END).apply { + inflate(getActionMenuId()) + menu.findItem(R.id.cab_select_all).isVisible = false + menu.findItem(R.id.cab_restore).title = resources.getString(R.string.restore_this_file) + setOnMenuItemClickListener { item -> + val recordingId = recording.id + when (item.itemId) { + R.id.cab_restore -> { + executeItemMenuOperation(recordingId, removeAfterCallback = false) { + restoreRecordings() + } + } + + R.id.cab_delete -> { + executeItemMenuOperation(recordingId, removeAfterCallback = false) { + askConfirmDelete() + } + } + } + + true + } + show() + } + } + + private fun executeItemMenuOperation(callId: Int, removeAfterCallback: Boolean = true, callback: () -> Unit) { + selectedKeys.add(callId) + callback() + if (removeAfterCallback) { + selectedKeys.remove(callId) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/ViewPagerAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/ViewPagerAdapter.kt index 0a67177..43f6579 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/ViewPagerAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/ViewPagerAdapter.kt @@ -8,12 +8,18 @@ import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.activities.SimpleActivity import com.simplemobiletools.voicerecorder.fragments.MyViewPagerFragment import com.simplemobiletools.voicerecorder.fragments.PlayerFragment +import com.simplemobiletools.voicerecorder.fragments.TrashFragment -class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() { +class ViewPagerAdapter(private val activity: SimpleActivity, val showRecycleBin: Boolean) : PagerAdapter() { private val mFragments = SparseArray() override fun instantiateItem(container: ViewGroup, position: Int): Any { - val layout = if (position == 0) R.layout.fragment_recorder else R.layout.fragment_player + val layout = when (position) { + 0 -> R.layout.fragment_recorder + 1 -> R.layout.fragment_player + 2 -> R.layout.fragment_trash + else -> throw IllegalArgumentException("Invalid position. Count = $count, requested position = $position") + } val view = activity.layoutInflater.inflate(layout, container, false) container.addView(view) @@ -26,7 +32,11 @@ class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() { container.removeView(item as View) } - override fun getCount() = 2 + override fun getCount() = if (showRecycleBin) { + 3 + } else { + 2 + } override fun isViewFromObject(view: View, item: Any) = view == item @@ -42,9 +52,17 @@ class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() { } } - fun finishActMode() = (mFragments[1] as? PlayerFragment)?.finishActMode() + fun finishActMode() { + (mFragments[1] as? PlayerFragment)?.finishActMode() + if (showRecycleBin) { + (mFragments[2] as? TrashFragment)?.finishActMode() + } + } fun searchTextChanged(text: String) { (mFragments[1] as? PlayerFragment)?.onSearchTextChanged(text) + if (showRecycleBin) { + (mFragments[2] as? TrashFragment)?.onSearchTextChanged(text) + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/dialogs/DeleteConfirmationDialog.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/dialogs/DeleteConfirmationDialog.kt new file mode 100644 index 0000000..6aa7119 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/dialogs/DeleteConfirmationDialog.kt @@ -0,0 +1,39 @@ +package com.simplemobiletools.voicerecorder.dialogs + +import android.app.Activity +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.extensions.beGoneIf +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.voicerecorder.R +import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.delete_remember_title +import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.skip_the_recycle_bin_checkbox + +class DeleteConfirmationDialog( + private val activity: Activity, + private val message: String, + private val showSkipRecycleBinOption: Boolean, + private val callback: (skipRecycleBin: Boolean) -> Unit +) { + + private var dialog: AlertDialog? = null + val view = activity.layoutInflater.inflate(R.layout.dialog_delete_confirmation, null)!! + + init { + view.delete_remember_title.text = message + view.skip_the_recycle_bin_checkbox.beGoneIf(!showSkipRecycleBinOption) + activity.getAlertDialogBuilder() + .setPositiveButton(R.string.yes) { _, _ -> dialogConfirmed() } + .setNegativeButton(R.string.no, null) + .apply { + activity.setupDialogStuff(view, this) { alertDialog -> + dialog = alertDialog + } + } + } + + private fun dialogConfirmed() { + dialog?.dismiss() + callback(view.skip_the_recycle_bin_checkbox.isChecked) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Activity.kt new file mode 100644 index 0000000..09207b9 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Activity.kt @@ -0,0 +1,173 @@ +package com.simplemobiletools.voicerecorder.extensions + +import android.content.ContentValues +import android.provider.MediaStore +import android.provider.MediaStore.Audio.Media +import androidx.core.net.toUri +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.deleteFile +import com.simplemobiletools.commons.extensions.getParentPath +import com.simplemobiletools.commons.extensions.toFileDirItem +import com.simplemobiletools.commons.helpers.* +import com.simplemobiletools.commons.models.FileDirItem +import com.simplemobiletools.voicerecorder.models.Recording +import java.io.File + +fun BaseSimpleActivity.deleteRecordings(recordingsToRemove: Collection, callback: (Boolean) -> Unit) { + when { + isRPlus() -> { + val fileUris = recordingsToRemove.map { recording -> + "${Media.EXTERNAL_CONTENT_URI}/${recording.id.toLong()}".toUri() + } + + deleteSDK30Uris(fileUris, callback) + } + + isQPlus() -> { + recordingsToRemove.forEach { + val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val selection = "${Media._ID} = ?" + val selectionArgs = arrayOf(it.id.toString()) + val result = contentResolver.delete(uri, selection, selectionArgs) + + if (result == 0) { + deleteFile(File(it.path).toFileDirItem(this)) + } + } + callback(true) + } + + else -> { + recordingsToRemove.forEach { + deleteFile(File(it.path).toFileDirItem(this)) + } + callback(true) + } + } +} + +fun BaseSimpleActivity.restoreRecordings(recordingsToRestore: Collection, callback: (Boolean) -> Unit) { + when { + isRPlus() -> { + val fileUris = recordingsToRestore.map { recording -> + "${Media.EXTERNAL_CONTENT_URI}/${recording.id.toLong()}".toUri() + } + + trashSDK30Uris(fileUris, false, callback) + } + + isQPlus() -> { + var wait = false + recordingsToRestore.forEach { + val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val selection = "${Media._ID} = ?" + val selectionArgs = arrayOf(it.id.toString()) + val values = ContentValues().apply { + put(Media.IS_TRASHED, 0) + } + val result = contentResolver.update(uri, values, selection, selectionArgs) + + if (result == 0) { + wait = true + copyMoveFilesTo( + fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)), + source = it.path.getParentPath(), + destination = config.saveRecordingsFolder, + isCopyOperation = false, + copyPhotoVideoOnly = false, + copyHidden = false + ) { + callback(true) + } + } + } + if (!wait) { + callback(true) + } + } + + else -> { + copyMoveFilesTo( + fileDirItems = recordingsToRestore.map { File(it.path).toFileDirItem(this) }.toMutableList() as ArrayList, + source = recordingsToRestore.first().path.getParentPath(), + destination = config.saveRecordingsFolder, + isCopyOperation = false, + copyPhotoVideoOnly = false, + copyHidden = false + ) { + callback(true) + } + } + } +} + +fun BaseSimpleActivity.moveRecordingsToRecycleBin(recordingsToMove: Collection, callback: (Boolean) -> Unit) { + when { + isRPlus() -> { + val fileUris = recordingsToMove.map { recording -> + "${Media.EXTERNAL_CONTENT_URI}/${recording.id.toLong()}".toUri() + } + + trashSDK30Uris(fileUris, true, callback) + } + + isQPlus() -> { + var wait = false + recordingsToMove.forEach { + val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val selection = "${Media._ID} = ?" + val selectionArgs = arrayOf(it.id.toString()) + val values = ContentValues().apply { + put(Media.IS_TRASHED, 1) + } + val result = contentResolver.update(uri, values, selection, selectionArgs) + + if (result == 0) { + wait = true + copyMoveFilesTo( + fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)), + source = it.path.getParentPath(), + destination = getOrCreateTrashFolder(), + isCopyOperation = false, + copyPhotoVideoOnly = false, + copyHidden = false + ) { + callback(true) + } + } + } + if (!wait) { + callback(true) + } + } + + else -> { + copyMoveFilesTo( + fileDirItems = recordingsToMove.map { File(it.path).toFileDirItem(this) }.toMutableList() as ArrayList, + source = recordingsToMove.first().path.getParentPath(), + destination = getOrCreateTrashFolder(), + isCopyOperation = false, + copyPhotoVideoOnly = false, + copyHidden = false + ) { + callback(true) + } + } + } +} + +fun BaseSimpleActivity.checkRecycleBinItems() { + if (config.useRecycleBin && config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) { + config.lastRecycleBinCheck = System.currentTimeMillis() + ensureBackgroundThread { + try { + deleteRecordings(getLegacyRecordings(trashed = true).filter { it.timestamp < System.currentTimeMillis() - MONTH_SECONDS * 1000L }) {} + } catch (e: Exception) { + } + } + } +} + +fun BaseSimpleActivity.emptyTheRecycleBin() { + deleteRecordings(getAllRecordings(trashed = true)) {} +} diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt index 17067f1..863de5c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt @@ -1,5 +1,6 @@ package com.simplemobiletools.voicerecorder.extensions +import android.annotation.SuppressLint import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context @@ -7,14 +8,18 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.Drawable +import android.media.MediaMetadataRetriever +import android.net.Uri import android.os.Environment -import com.simplemobiletools.commons.extensions.internalStoragePath -import com.simplemobiletools.commons.helpers.isQPlus +import android.provider.MediaStore +import android.provider.MediaStore.Audio.Media +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.voicerecorder.R -import com.simplemobiletools.voicerecorder.helpers.Config -import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING -import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider -import com.simplemobiletools.voicerecorder.helpers.TOGGLE_WIDGET_UI +import com.simplemobiletools.voicerecorder.helpers.* +import com.simplemobiletools.voicerecorder.models.Recording +import java.io.File +import kotlin.math.roundToLong val Context.config: Config get() = Config.newInstance(applicationContext) @@ -51,3 +56,147 @@ fun Context.getDefaultRecordingsRelativePath(): String { getString(R.string.app_name) } } + +@SuppressLint("InlinedApi") +fun Context.getMediaStoreRecordings(trashed: Boolean = false): ArrayList { + val recordings = ArrayList() + + val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val projection = arrayOf( + Media._ID, + Media.DISPLAY_NAME, + Media.DATE_ADDED, + Media.DURATION, + Media.SIZE + ) + + var selection = "${Media.OWNER_PACKAGE_NAME} = ?" + var selectionArgs = arrayOf(packageName) + val sortOrder = "${Media.DATE_ADDED} DESC" + + if (config.useRecycleBin) { + val trashedValue = if (trashed) 1 else 0 + selection += " AND ${Media.IS_TRASHED} = ?" + selectionArgs = selectionArgs.plus(trashedValue.toString()) + } + + queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor -> + val id = cursor.getIntValue(Media._ID) + val title = cursor.getStringValue(Media.DISPLAY_NAME) + val timestamp = cursor.getIntValue(Media.DATE_ADDED) + var duration = cursor.getLongValue(Media.DURATION) / 1000 + var size = cursor.getIntValue(Media.SIZE) + + if (duration == 0L) { + duration = getDurationFromUri(getAudioFileContentUri(id.toLong())) + } + + if (size == 0) { + size = getSizeFromUri(id.toLong()) + } + + val recording = Recording(id, title, "", timestamp, duration.toInt(), size) + recordings.add(recording) + } + + return recordings +} + +fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList { + val recordings = ArrayList() + val folder = if (trashed) { + trashFolder + } else { + config.saveRecordingsFolder + } + val files = File(folder).listFiles() ?: return recordings + + files.filter { it.isAudioFast() }.forEach { + val id = it.hashCode() + val title = it.name + val path = it.absolutePath + val timestamp = (it.lastModified() / 1000).toInt() + val duration = getDuration(it.absolutePath) ?: 0 + val size = it.length().toInt() + val recording = Recording(id, title, path, timestamp, duration, size) + recordings.add(recording) + } + return recordings +} + +fun Context.getSAFRecordings(trashed: Boolean = false): ArrayList { + val recordings = ArrayList() + val folder = if (trashed) { + trashFolder + } else { + config.saveRecordingsFolder + } + val files = getDocumentSdk30(folder)?.listFiles() ?: return recordings + + files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach { + val id = it.hashCode() + val title = it.name!! + val path = it.uri.toString() + val timestamp = (it.lastModified() / 1000).toInt() + val duration = getDurationFromUri(it.uri) + val size = it.length().toInt() + val recording = Recording(id, title, path, timestamp, duration.toInt(), size) + recordings.add(recording) + } + + recordings.sortByDescending { it.timestamp } + return recordings +} + +fun Context.getAllRecordings(trashed: Boolean = false): ArrayList { + val recordings = ArrayList() + return when { + isRPlus() -> { + recordings.addAll(getMediaStoreRecordings(trashed)) + recordings.addAll(getSAFRecordings(trashed)) + recordings + } + + isQPlus() -> { + recordings.addAll(getMediaStoreRecordings(trashed)) + recordings.addAll(getLegacyRecordings(trashed)) + recordings + } + + else -> { + recordings.addAll(getLegacyRecordings(trashed)) + recordings + } + } +} + +val Context.trashFolder + get() = "${config.saveRecordingsFolder}/.trash" + +fun Context.getOrCreateTrashFolder(): String { + val folder = File(trashFolder) + if (!folder.exists()) { + folder.mkdir() + } + return trashFolder +} + +private fun Context.getSizeFromUri(id: Long): Int { + val recordingUri = getAudioFileContentUri(id) + return try { + contentResolver.openInputStream(recordingUri)?.available() ?: 0 + } catch (e: Exception) { + 0 + } +} + +private fun Context.getDurationFromUri(uri: Uri): Long { + return try { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(this, uri) + val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!! + (time.toLong() / 1000.toDouble()).roundToLong() + } catch (e: Exception) { + 0L + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt index a753ba8..34ad7c2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt @@ -1,28 +1,24 @@ package com.simplemobiletools.voicerecorder.fragments -import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import android.media.AudioManager -import android.media.MediaMetadataRetriever import android.media.MediaPlayer import android.net.Uri import android.os.Handler import android.os.Looper import android.os.PowerManager import android.provider.DocumentsContract -import android.provider.MediaStore -import android.provider.MediaStore.Audio.Media import android.util.AttributeSet import android.widget.SeekBar import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.isQPlus -import com.simplemobiletools.commons.helpers.isRPlus import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.activities.SimpleActivity import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter import com.simplemobiletools.voicerecorder.extensions.config +import com.simplemobiletools.voicerecorder.extensions.getAllRecordings import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener import com.simplemobiletools.voicerecorder.models.Events @@ -31,9 +27,9 @@ import kotlinx.android.synthetic.main.fragment_player.view.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.io.File -import java.util.* -import kotlin.math.roundToLong +import java.util.Stack +import java.util.Timer +import java.util.TimerTask class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener { private val FAST_FORWARD_SKIP_MS = 10000 @@ -45,18 +41,19 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager private var lastSearchQuery = "" private var bus: EventBus? = null private var prevSavePath = "" + private var prevRecycleBinState = context.config.useRecycleBin private var playOnPreparation = true override fun onResume() { setupColors() - if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath) { + if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath || context.config.useRecycleBin != prevRecycleBinState) { itemsIgnoringSearch = getRecordings() setupAdapter(itemsIgnoringSearch) } else { getRecordingsAdapter()?.updateTextColor(context.getProperTextColor()) } - storePrevPath() + storePrevState() } override fun onDestroy() { @@ -78,7 +75,7 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager setupAdapter(itemsIgnoringSearch) initMediaPlayer() setupViews() - storePrevPath() + storePrevState() } private fun setupViews() { @@ -183,122 +180,11 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager } private fun getRecordings(): ArrayList { - val recordings = ArrayList() - return when { - isRPlus() -> { - recordings.addAll(getMediaStoreRecordings()) - recordings.addAll(getSAFRecordings()) - recordings - } - isQPlus() -> { - recordings.addAll(getMediaStoreRecordings()) - recordings.addAll(getLegacyRecordings()) - recordings - } - else -> { - recordings.addAll(getLegacyRecordings()) - recordings - } - }.apply { + return context.getAllRecordings().apply { sortByDescending { it.timestamp } } } - @SuppressLint("InlinedApi") - private fun getMediaStoreRecordings(): ArrayList { - val recordings = ArrayList() - - val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - val projection = arrayOf( - Media._ID, - Media.DISPLAY_NAME, - Media.DATE_ADDED, - Media.DURATION, - Media.SIZE - ) - - val selection = "${Media.OWNER_PACKAGE_NAME} = ?" - val selectionArgs = arrayOf(context.packageName) - val sortOrder = "${Media.DATE_ADDED} DESC" - - context.queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor -> - val id = cursor.getIntValue(Media._ID) - val title = cursor.getStringValue(Media.DISPLAY_NAME) - val timestamp = cursor.getIntValue(Media.DATE_ADDED) - var duration = cursor.getLongValue(Media.DURATION) / 1000 - var size = cursor.getIntValue(Media.SIZE) - - if (duration == 0L) { - duration = getDurationFromUri(getAudioFileContentUri(id.toLong())) - } - - if (size == 0) { - size = getSizeFromUri(id.toLong()) - } - - val recording = Recording(id, title, "", timestamp, duration.toInt(), size) - recordings.add(recording) - } - - return recordings - } - - private fun getLegacyRecordings(): ArrayList { - val recordings = ArrayList() - val files = File(context.config.saveRecordingsFolder).listFiles() ?: return recordings - - files.filter { it.isAudioFast() }.forEach { - val id = it.hashCode() - val title = it.name - val path = it.absolutePath - val timestamp = (it.lastModified() / 1000).toInt() - val duration = context.getDuration(it.absolutePath) ?: 0 - val size = it.length().toInt() - val recording = Recording(id, title, path, timestamp, duration, size) - recordings.add(recording) - } - return recordings - } - - private fun getSAFRecordings(): ArrayList { - val recordings = ArrayList() - val files = context.getDocumentSdk30(context.config.saveRecordingsFolder)?.listFiles() ?: return recordings - - files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach { - val id = it.hashCode() - val title = it.name!! - val path = it.uri.toString() - val timestamp = (it.lastModified() / 1000).toInt() - val duration = getDurationFromUri(it.uri) - val size = it.length().toInt() - val recording = Recording(id, title, path, timestamp, duration.toInt(), size) - recordings.add(recording) - } - - recordings.sortByDescending { it.timestamp } - return recordings - } - - private fun getDurationFromUri(uri: Uri): Long { - return try { - val retriever = MediaMetadataRetriever() - retriever.setDataSource(context, uri) - val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!! - (time.toLong() / 1000.toDouble()).roundToLong() - } catch (e: Exception) { - 0L - } - } - - private fun getSizeFromUri(id: Long): Int { - val recordingUri = getAudioFileContentUri(id) - return try { - context.contentResolver.openInputStream(recordingUri)?.available() ?: 0 - } catch (e: Exception) { - 0 - } - } - private fun initMediaPlayer() { player = MediaPlayer().apply { setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) @@ -336,9 +222,11 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager DocumentsContract.isDocumentUri(context, uri) -> { setDataSource(context, uri) } + recording.path.isEmpty() -> { setDataSource(context, getAudioFileContentUri(recording.id.toLong())) } + else -> { setDataSource(recording.path) } @@ -452,8 +340,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager private fun getRecordingsAdapter() = recordings_list.adapter as? RecordingsAdapter - private fun storePrevPath() { + private fun storePrevState() { prevSavePath = context!!.config.saveRecordingsFolder + prevRecycleBinState = context.config.useRecycleBin } private fun setupColors() { @@ -476,4 +365,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager fun recordingCompleted(event: Events.RecordingCompleted) { refreshRecordings() } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) { + refreshRecordings() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/TrashFragment.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/TrashFragment.kt new file mode 100644 index 0000000..faa0972 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/TrashFragment.kt @@ -0,0 +1,127 @@ +package com.simplemobiletools.voicerecorder.fragments + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.voicerecorder.R +import com.simplemobiletools.voicerecorder.activities.SimpleActivity +import com.simplemobiletools.voicerecorder.adapters.TrashAdapter +import com.simplemobiletools.voicerecorder.extensions.config +import com.simplemobiletools.voicerecorder.extensions.getAllRecordings +import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener +import com.simplemobiletools.voicerecorder.models.Events +import com.simplemobiletools.voicerecorder.models.Recording +import kotlinx.android.synthetic.main.fragment_trash.view.player_holder +import kotlinx.android.synthetic.main.fragment_trash.view.recordings_fastscroller +import kotlinx.android.synthetic.main.fragment_trash.view.recordings_list +import kotlinx.android.synthetic.main.fragment_trash.view.recordings_placeholder +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class TrashFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener { + private var itemsIgnoringSearch = ArrayList() + private var lastSearchQuery = "" + private var bus: EventBus? = null + private var prevSavePath = "" + + override fun onResume() { + setupColors() + if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath) { + itemsIgnoringSearch = getRecordings() + setupAdapter(itemsIgnoringSearch) + } else { + getRecordingsAdapter()?.updateTextColor(context.getProperTextColor()) + } + + storePrevPath() + } + + override fun onDestroy() { + bus?.unregister(this) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + bus = EventBus.getDefault() + bus!!.register(this) + setupColors() + itemsIgnoringSearch = getRecordings() + setupAdapter(itemsIgnoringSearch) + storePrevPath() + } + + override fun refreshRecordings() { + itemsIgnoringSearch = getRecordings() + setupAdapter(itemsIgnoringSearch) + } + + override fun playRecording(recording: Recording, playOnPrepared: Boolean) {} + + private fun setupAdapter(recordings: ArrayList) { + ensureBackgroundThread { + Handler(Looper.getMainLooper()).post { + recordings_fastscroller.beVisibleIf(recordings.isNotEmpty()) + recordings_placeholder.beVisibleIf(recordings.isEmpty()) + if (recordings.isEmpty()) { + val stringId = if (lastSearchQuery.isEmpty()) { + R.string.recycle_bin_empty + } else { + R.string.no_items_found + } + + recordings_placeholder.text = context.getString(stringId) + } + + val adapter = getRecordingsAdapter() + if (adapter == null) { + TrashAdapter(context as SimpleActivity, recordings, this, recordings_list) + .apply { + recordings_list.adapter = this + } + + if (context.areSystemAnimationsEnabled) { + recordings_list.scheduleLayoutAnimation() + } + } else { + adapter.updateItems(recordings) + } + } + } + } + + private fun getRecordings(): ArrayList { + return context.getAllRecordings(trashed = true).apply { + sortByDescending { it.timestamp } + } + } + + fun onSearchTextChanged(text: String) { + lastSearchQuery = text + val filtered = itemsIgnoringSearch.filter { it.title.contains(text, true) }.toMutableList() as ArrayList + setupAdapter(filtered) + } + + private fun getRecordingsAdapter() = recordings_list.adapter as? TrashAdapter + + private fun storePrevPath() { + prevSavePath = context!!.config.saveRecordingsFolder + } + + private fun setupColors() { + val properPrimaryColor = context.getProperPrimaryColor() + recordings_fastscroller.updateColors(properPrimaryColor) + context.updateTextColors(player_holder) + } + + fun finishActMode() = getRecordingsAdapter()?.finishActMode() + + @Subscribe(threadMode = ThreadMode.MAIN) + fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) { + refreshRecordings() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt index c0a9055..355d434 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt @@ -72,4 +72,12 @@ class Config(context: Context) : BaseConfig(context) { EXTENSION_OGG -> MediaRecorder.AudioEncoder.OPUS else -> MediaRecorder.AudioEncoder.AAC } + + var useRecycleBin: Boolean + get() = prefs.getBoolean(USE_RECYCLE_BIN, true) + set(useRecycleBin) = prefs.edit().putBoolean(USE_RECYCLE_BIN, useRecycleBin).apply() + + var lastRecycleBinCheck: Long + get() = prefs.getLong(LAST_RECYCLE_BIN_CHECK, 0L) + set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Constants.kt index 449d479..4912561 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Constants.kt @@ -36,6 +36,8 @@ const val EXTENSION = "extension" const val AUDIO_SOURCE = "audio_source" const val BITRATE = "bitrate" const val RECORD_AFTER_LAUNCH = "record_after_launch" +const val USE_RECYCLE_BIN = "use_recycle_bin" +const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check" @SuppressLint("InlinedApi") fun getAudioFileContentUri(id: Long): Uri { diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/models/Events.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/models/Events.kt index cc72d48..65475bf 100644 --- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/models/Events.kt +++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/models/Events.kt @@ -7,5 +7,6 @@ class Events { class RecordingStatus internal constructor(val status: Int) class RecordingAmplitude internal constructor(val amplitude: Int) class RecordingCompleted internal constructor() + class RecordingTrashUpdated internal constructor() class RecordingSaved internal constructor(val uri: Uri?) } diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 50dc335..89204e8 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -268,6 +268,55 @@ tools:text="128 kbps" /> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_delete_confirmation.xml b/app/src/main/res/layout/dialog_delete_confirmation.xml new file mode 100644 index 0000000..faa1ba7 --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_confirmation.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_trash.xml b/app/src/main/res/layout/fragment_trash.xml new file mode 100644 index 0000000..b82de55 --- /dev/null +++ b/app/src/main/res/layout/fragment_trash.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/menu/cab_trash.xml b/app/src/main/res/menu/cab_trash.xml new file mode 100644 index 0000000..47379c1 --- /dev/null +++ b/app/src/main/res/menu/cab_trash.xml @@ -0,0 +1,21 @@ + + + + + +