Merge pull request #205 from esensar/feature/recycle-bin

Add support for recycle bin
This commit is contained in:
Tibor Kaputa 2023-07-27 16:03:58 +02:00 committed by GitHub
commit 272f5a8b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1079 additions and 220 deletions

View File

@ -63,7 +63,7 @@ android {
}
dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:42733f39a4'
implementation 'com.github.SimpleMobileTools:Simple-Commons:fa61be64d8'
implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'com.github.Armen101:AudioRecordView:1.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'

View File

@ -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()
}
@ -110,7 +118,9 @@ class MainActivity : SimpleActivity() {
main_menu.setupMenu()
main_menu.onSearchOpenListener = {
view_pager.currentItem = 1
if (view_pager.currentItem == 0) {
view_pager.currentItem = 1
}
}
main_menu.onSearchTextChangedListener = { text ->
@ -149,8 +159,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 {
@ -164,7 +178,7 @@ class MainActivity : SimpleActivity() {
main_tabs_holder.onTabSelectionChanged(
tabUnselectedAction = {
updateBottomTabItemColors(it.customView, false)
if (it.position == 1) {
if (it.position == 1 || it.position == 2) {
main_menu.closeSearch()
}
},
@ -174,7 +188,8 @@ class MainActivity : SimpleActivity() {
}
)
view_pager.adapter = ViewPagerAdapter(this)
view_pager.adapter = ViewPagerAdapter(this, config.useRecycleBin)
view_pager.offscreenPageLimit = 2
view_pager.onPageChangeListener {
main_tabs_holder.getTabAt(it)?.select()
(view_pager.adapter as ViewPagerAdapter).finishActMode()

View File

@ -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 {

View File

@ -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<Recording>
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<Recording>
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()

View File

@ -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<Recording>,
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<Recording>) {
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<Recording>
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<Recording>
val positions = getSelectedItemPositions()
activity.deleteRecordings(recordingsToRemove) { success ->
if (success) {
doDeleteAnimation(recordingsToRemove, positions)
}
}
}
private fun doDeleteAnimation(recordingsToRemove: ArrayList<Recording>, positions: ArrayList<Int>) {
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<Recording>
private fun setupView(view: View, recording: Recording) {
view.apply {
setupViewBackground(activity)
recording_frame?.isSelected = selectedKeys.contains(recording.id)
arrayListOf<TextView>(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)
}
}
}

View File

@ -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<MyViewPagerFragment>()
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)
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,183 @@
package com.simplemobiletools.voicerecorder.extensions
import android.content.ContentValues
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
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.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.models.Recording
import java.io.File
fun BaseSimpleActivity.deleteRecordings(recordingsToRemove: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToRemove.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
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) {
val fileDirItem = File(it.path).toFileDirItem(this)
deleteFile(fileDirItem)
}
}
callback(true)
}
else -> {
recordingsToRemove.forEach {
val fileDirItem = File(it.path).toFileDirItem(this)
deleteFile(fileDirItem)
}
callback(true)
}
}
}
fun BaseSimpleActivity.restoreRecordings(recordingsToRestore: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToRestore.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
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<FileDirItem>,
source = recordingsToRestore.first().path.getParentPath(),
destination = config.saveRecordingsFolder,
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
}
fun BaseSimpleActivity.moveRecordingsToRecycleBin(recordingsToMove: Collection<Recording>, callback: (success: Boolean) -> Unit) {
when {
isRPlus() -> {
val fileUris = recordingsToMove.map { recording ->
getAudioFileContentUri(recording.id.toLong())
}
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<FileDirItem>,
source = recordingsToMove.first().path.getParentPath(),
destination = getOrCreateTrashFolder(),
isCopyOperation = false,
copyPhotoVideoOnly = false,
copyHidden = false
) {
callback(true)
}
}
}
}
fun BaseSimpleActivity.checkRecycleBinItems() {
if (isQPlus()) {
// System is handling recycle bin on Q+ devices
return
}
if (config.useRecycleBin && config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) {
config.lastRecycleBinCheck = System.currentTimeMillis()
ensureBackgroundThread {
try {
val recordingsToRemove = getLegacyRecordings(trashed = true).filter { it.timestamp < System.currentTimeMillis() - MONTH_SECONDS * 1000L }
if (recordingsToRemove.isNotEmpty()) {
deleteRecordings(recordingsToRemove) {}
}
} catch (e: Exception) {
}
}
}
}
fun BaseSimpleActivity.emptyTheRecycleBin() {
deleteRecordings(getAllRecordings(trashed = true)) {}
}

View File

@ -1,20 +1,28 @@
package com.simplemobiletools.voicerecorder.extensions
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Bundle
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 +59,182 @@ fun Context.getDefaultRecordingsRelativePath(): String {
getString(R.string.app_name)
}
}
@SuppressLint("InlinedApi")
fun Context.getNewMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val projection = arrayOf(
Media._ID,
Media.DISPLAY_NAME,
Media.DATE_ADDED,
Media.DURATION,
Media.SIZE
)
val bundle = Bundle().apply {
putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(Media.DATE_ADDED))
putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${Media.OWNER_PACKAGE_NAME} = ?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(packageName))
if (config.useRecycleBin) {
val trashedValue = if (trashed) MediaStore.MATCH_ONLY else MediaStore.MATCH_EXCLUDE
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, trashedValue)
}
}
queryCursor(uri, projection, bundle, true) { cursor ->
val recording = readRecordingFromCursor(cursor)
recordings.add(recording)
}
return recordings
}
@SuppressLint("InlinedApi")
fun Context.getMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
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 recording = readRecordingFromCursor(cursor)
recordings.add(recording)
}
return recordings
}
fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
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<Recording> {
val recordings = ArrayList<Recording>()
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<Recording> {
val recordings = ArrayList<Recording>()
return when {
isRPlus() -> {
recordings.addAll(getNewMediaStoreRecordings(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.readRecordingFromCursor(cursor: Cursor): Recording {
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())
}
return Recording(id, title, "", timestamp, duration.toInt(), size)
}
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
}
}

View File

@ -1,28 +1,23 @@
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 +26,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 +40,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 +74,7 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
setupAdapter(itemsIgnoringSearch)
initMediaPlayer()
setupViews()
storePrevPath()
storePrevState()
}
private fun setupViews() {
@ -141,164 +137,49 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
private fun setupAdapter(recordings: ArrayList<Recording>) {
ensureBackgroundThread {
Handler(Looper.getMainLooper()).post {
recordings_fastscroller.beVisibleIf(recordings.isNotEmpty())
recordings_placeholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
if (isQPlus()) {
R.string.no_recordings_found
} else {
R.string.no_recordings_in_folder_found
}
} else {
R.string.no_items_found
}
recordings_placeholder.text = context.getString(stringId)
resetProgress(null)
player?.stop()
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
RecordingsAdapter(context as SimpleActivity, recordings, this, recordings_list) {
playRecording(it as Recording, true)
if (playedRecordingIDs.isEmpty() || playedRecordingIDs.peek() != it.id) {
playedRecordingIDs.push(it.id)
}
}.apply {
recordings_list.adapter = this
}
if (context.areSystemAnimationsEnabled) {
recordings_list.scheduleLayoutAnimation()
}
recordings_fastscroller.beVisibleIf(recordings.isNotEmpty())
recordings_placeholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
if (isQPlus()) {
R.string.no_recordings_found
} else {
adapter.updateItems(recordings)
R.string.no_recordings_in_folder_found
}
} else {
R.string.no_items_found
}
recordings_placeholder.text = context.getString(stringId)
resetProgress(null)
player?.stop()
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
RecordingsAdapter(context as SimpleActivity, recordings, this, recordings_list) {
playRecording(it as Recording, true)
if (playedRecordingIDs.isEmpty() || playedRecordingIDs.peek() != it.id) {
playedRecordingIDs.push(it.id)
}
}.apply {
recordings_list.adapter = this
}
if (context.areSystemAnimationsEnabled) {
recordings_list.scheduleLayoutAnimation()
}
} else {
adapter.updateItems(recordings)
}
}
private fun getRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
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<Recording> {
val recordings = ArrayList<Recording>()
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<Recording> {
val recordings = ArrayList<Recording>()
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<Recording> {
val recordings = ArrayList<Recording>()
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 +217,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 +335,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 +360,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
fun recordingCompleted(event: Events.RecordingCompleted) {
refreshRecordings()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
refreshRecordings()
}
}

View File

@ -0,0 +1,119 @@
package com.simplemobiletools.voicerecorder.fragments
import android.content.Context
import android.util.AttributeSet
import com.simplemobiletools.commons.extensions.*
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.trash_fastscroller
import kotlinx.android.synthetic.main.fragment_trash.view.trash_holder
import kotlinx.android.synthetic.main.fragment_trash.view.trash_list
import kotlinx.android.synthetic.main.fragment_trash.view.trash_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<Recording>()
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<Recording>) {
trash_fastscroller.beVisibleIf(recordings.isNotEmpty())
trash_placeholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
R.string.recycle_bin_empty
} else {
R.string.no_items_found
}
trash_placeholder.text = context.getString(stringId)
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
TrashAdapter(context as SimpleActivity, recordings, this, trash_list).apply {
trash_list.adapter = this
}
if (context.areSystemAnimationsEnabled) {
trash_list.scheduleLayoutAnimation()
}
} else {
adapter.updateItems(recordings)
}
}
private fun getRecordings(): ArrayList<Recording> {
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<Recording>
setupAdapter(filtered)
}
private fun getRecordingsAdapter() = trash_list.adapter as? TrashAdapter
private fun storePrevPath() {
prevSavePath = context!!.config.saveRecordingsFolder
}
private fun setupColors() {
val properPrimaryColor = context.getProperPrimaryColor()
trash_fastscroller.updateColors(properPrimaryColor)
context.updateTextColors(trash_holder)
}
fun finishActMode() = getRecordingsAdapter()?.finishActMode()
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
refreshRecordings()
}
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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?)
}

View File

@ -268,6 +268,55 @@
tools:text="128 kbps" />
</RelativeLayout>
<include
android:id="@+id/settings_general_settings_divider"
layout="@layout/divider" />
<TextView
android:id="@+id/settings_recycle_bin_label"
style="@style/SettingsSectionLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/recycle_bin" />
<RelativeLayout
android:id="@+id/settings_use_recycle_bin_holder"
style="@style/SettingsHolderCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/settings_use_recycle_bin"
style="@style/SettingsCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/move_items_into_recycle_bin" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/settings_empty_recycle_bin_holder"
style="@style/SettingsHolderTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_label"
style="@style/SettingsTextLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_recycle_bin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_size"
style="@style/SettingsTextValueStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/settings_empty_recycle_bin_label"
tools:text="0 B" />
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/delete_remember_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/big_margin"
android:paddingTop="@dimen/big_margin"
android:paddingRight="@dimen/big_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/delete_remember_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingBottom="@dimen/activity_margin"
android:text="@string/delete_recordings_confirmation"
android:textSize="@dimen/bigger_text_size" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/skip_the_recycle_bin_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/delete_remember_title"
android:text="@string/skip_the_recycle_bin" />
</RelativeLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.voicerecorder.fragments.TrashFragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/trash_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/trash_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.8"
android:gravity="center"
android:lineSpacingExtra="@dimen/small_margin"
android:padding="@dimen/activity_margin"
android:text="@string/recycle_bin_empty"
android:textSize="@dimen/bigger_text_size"
android:textStyle="italic"
android:visibility="visible" />
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/trash_fastscroller"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent">
<com.simplemobiletools.commons.views.MyRecyclerView
android:id="@+id/trash_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:layoutAnimation="@anim/layout_animation"
android:scrollbars="none"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager" />
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</com.simplemobiletools.voicerecorder.fragments.TrashFragment>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource,AlwaysShowAction">
<item
android:id="@+id/cab_delete"
android:icon="@drawable/ic_delete_vector"
android:showAsAction="always"
android:title="@string/delete" />
<item
android:id="@+id/cab_restore"
android:showAsAction="never"
android:title="@string/restore_selected_files"
app:showAsAction="never" />
<item
android:id="@+id/cab_select_all"
android:icon="@drawable/ic_select_all_vector"
android:title="@string/select_all"
app:showAsAction="ifRoom" />
</menu>