Add support for recycle bin
This uses system recycle bin whenever possible, which will automatically delete files after 30 days. When not available, moves files to hidden `.trash` subdirectory and manually removes them after 30 days.
This commit is contained in:
parent
bc07f3c9fe
commit
5883e91bc4
18 changed files with 1004 additions and 182 deletions
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,15 +135,24 @@ 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 {
|
||||
val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin
|
||||
if (toRecycleBin) {
|
||||
moveMediaStoreRecordingsToRecycleBin()
|
||||
} else {
|
||||
deleteMediaStoreRecordings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMediaStoreRecordings() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
|
@ -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 ->
|
||||
activity.deleteRecordings(recordingsToRemove) { 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Recording>, 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<Recording>, 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<FileDirItem>,
|
||||
source = recordingsToRestore.first().path.getParentPath(),
|
||||
destination = config.saveRecordingsFolder,
|
||||
isCopyOperation = false,
|
||||
copyPhotoVideoOnly = false,
|
||||
copyHidden = false
|
||||
) {
|
||||
callback(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseSimpleActivity.moveRecordingsToRecycleBin(recordingsToMove: Collection<Recording>, 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<FileDirItem>,
|
||||
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)) {}
|
||||
}
|
|
@ -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<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 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<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(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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 +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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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>) {
|
||||
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<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() = 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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
26
app/src/main/res/layout/dialog_delete_confirmation.xml
Normal file
26
app/src/main/res/layout/dialog_delete_confirmation.xml
Normal 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>
|
38
app/src/main/res/layout/fragment_trash.xml
Normal file
38
app/src/main/res/layout/fragment_trash.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?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/player_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/recordings_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/recordings_fastscroller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/player_controls_wrapper"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
||||
android:id="@+id/recordings_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>
|
21
app/src/main/res/menu/cab_trash.xml
Normal file
21
app/src/main/res/menu/cab_trash.xml
Normal 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>
|
Loading…
Reference in a new issue