mirror of
https://github.com/SimpleMobileTools/Simple-Voice-Recorder.git
synced 2025-02-07 23:38:45 +01:00
Merge pull request #205 from esensar/feature/recycle-bin
Add support for recycle bin
This commit is contained in:
commit
272f5a8b21
@ -63,7 +63,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.github.SimpleMobileTools:Simple-Commons:42733f39a4'
|
implementation 'com.github.SimpleMobileTools:Simple-Commons:fa61be64d8'
|
||||||
implementation 'org.greenrobot:eventbus:3.3.1'
|
implementation 'org.greenrobot:eventbus:3.3.1'
|
||||||
implementation 'com.github.Armen101:AudioRecordView:1.0.4'
|
implementation 'com.github.Armen101:AudioRecordView:1.0.4'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
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.BuildConfig
|
||||||
import com.simplemobiletools.voicerecorder.R
|
import com.simplemobiletools.voicerecorder.R
|
||||||
import com.simplemobiletools.voicerecorder.adapters.ViewPagerAdapter
|
import com.simplemobiletools.voicerecorder.adapters.ViewPagerAdapter
|
||||||
|
import com.simplemobiletools.voicerecorder.extensions.checkRecycleBinItems
|
||||||
import com.simplemobiletools.voicerecorder.extensions.config
|
import com.simplemobiletools.voicerecorder.extensions.config
|
||||||
import com.simplemobiletools.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE
|
import com.simplemobiletools.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE
|
||||||
import com.simplemobiletools.voicerecorder.models.Events
|
import com.simplemobiletools.voicerecorder.models.Events
|
||||||
@ -40,6 +41,10 @@ class MainActivity : SimpleActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
checkRecycleBinItems()
|
||||||
|
}
|
||||||
|
|
||||||
handlePermission(PERMISSION_RECORD_AUDIO) {
|
handlePermission(PERMISSION_RECORD_AUDIO) {
|
||||||
if (it) {
|
if (it) {
|
||||||
tryInitVoiceRecorder()
|
tryInitVoiceRecorder()
|
||||||
@ -65,6 +70,9 @@ class MainActivity : SimpleActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
setupTabColors()
|
setupTabColors()
|
||||||
updateMenuColors()
|
updateMenuColors()
|
||||||
|
if (getPagerAdapter()?.showRecycleBin != config.useRecycleBin) {
|
||||||
|
setupViewPager()
|
||||||
|
}
|
||||||
getPagerAdapter()?.onResume()
|
getPagerAdapter()?.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +118,9 @@ class MainActivity : SimpleActivity() {
|
|||||||
main_menu.setupMenu()
|
main_menu.setupMenu()
|
||||||
|
|
||||||
main_menu.onSearchOpenListener = {
|
main_menu.onSearchOpenListener = {
|
||||||
view_pager.currentItem = 1
|
if (view_pager.currentItem == 0) {
|
||||||
|
view_pager.currentItem = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main_menu.onSearchTextChangedListener = { text ->
|
main_menu.onSearchTextChangedListener = { text ->
|
||||||
@ -149,8 +159,12 @@ class MainActivity : SimpleActivity() {
|
|||||||
|
|
||||||
private fun setupViewPager() {
|
private fun setupViewPager() {
|
||||||
main_tabs_holder.removeAllTabs()
|
main_tabs_holder.removeAllTabs()
|
||||||
val tabDrawables = arrayOf(R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector)
|
var tabDrawables = arrayOf(R.drawable.ic_microphone_vector, R.drawable.ic_headset_vector)
|
||||||
val tabLabels = arrayOf(R.string.recorder, R.string.player)
|
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 ->
|
tabDrawables.forEachIndexed { i, drawableId ->
|
||||||
main_tabs_holder.newTab().setCustomView(R.layout.bottom_tablayout_item).apply {
|
main_tabs_holder.newTab().setCustomView(R.layout.bottom_tablayout_item).apply {
|
||||||
@ -164,7 +178,7 @@ class MainActivity : SimpleActivity() {
|
|||||||
main_tabs_holder.onTabSelectionChanged(
|
main_tabs_holder.onTabSelectionChanged(
|
||||||
tabUnselectedAction = {
|
tabUnselectedAction = {
|
||||||
updateBottomTabItemColors(it.customView, false)
|
updateBottomTabItemColors(it.customView, false)
|
||||||
if (it.position == 1) {
|
if (it.position == 1 || it.position == 2) {
|
||||||
main_menu.closeSearch()
|
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 {
|
view_pager.onPageChangeListener {
|
||||||
main_tabs_holder.getTabAt(it)?.select()
|
main_tabs_holder.getTabAt(it)?.select()
|
||||||
(view_pager.adapter as ViewPagerAdapter).finishActMode()
|
(view_pager.adapter as ViewPagerAdapter).finishActMode()
|
||||||
|
@ -3,24 +3,26 @@ package com.simplemobiletools.voicerecorder.activities
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.simplemobiletools.commons.dialogs.ChangeDateTimeFormatDialog
|
import com.simplemobiletools.commons.dialogs.*
|
||||||
import com.simplemobiletools.commons.dialogs.FeatureLockedDialog
|
|
||||||
import com.simplemobiletools.commons.dialogs.FilePickerDialog
|
|
||||||
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
|
|
||||||
import com.simplemobiletools.commons.extensions.*
|
import com.simplemobiletools.commons.extensions.*
|
||||||
import com.simplemobiletools.commons.helpers.*
|
import com.simplemobiletools.commons.helpers.*
|
||||||
import com.simplemobiletools.commons.models.RadioItem
|
import com.simplemobiletools.commons.models.RadioItem
|
||||||
import com.simplemobiletools.voicerecorder.R
|
import com.simplemobiletools.voicerecorder.R
|
||||||
import com.simplemobiletools.voicerecorder.extensions.config
|
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.BITRATES
|
||||||
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_M4A
|
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_M4A
|
||||||
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_MP3
|
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_MP3
|
||||||
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_OGG
|
import com.simplemobiletools.voicerecorder.helpers.EXTENSION_OGG
|
||||||
|
import com.simplemobiletools.voicerecorder.models.Events
|
||||||
import kotlinx.android.synthetic.main.activity_settings.*
|
import kotlinx.android.synthetic.main.activity_settings.*
|
||||||
import java.util.*
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class SettingsActivity : SimpleActivity() {
|
class SettingsActivity : SimpleActivity() {
|
||||||
|
private var recycleBinContentSize = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
isMaterialActivity = true
|
isMaterialActivity = true
|
||||||
@ -47,9 +49,11 @@ class SettingsActivity : SimpleActivity() {
|
|||||||
setupBitrate()
|
setupBitrate()
|
||||||
setupAudioSource()
|
setupAudioSource()
|
||||||
setupRecordAfterLaunch()
|
setupRecordAfterLaunch()
|
||||||
|
setupUseRecycleBin()
|
||||||
|
setupEmptyRecycleBin()
|
||||||
updateTextColors(settings_nested_scrollview)
|
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())
|
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() {
|
private fun setupAudioSource() {
|
||||||
settings_audio_source.text = config.getAudioSourceText(config.audioSource)
|
settings_audio_source.text = config.getAudioSourceText(config.audioSource)
|
||||||
settings_audio_source_holder.setOnClickListener {
|
settings_audio_source_holder.setOnClickListener {
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
package com.simplemobiletools.voicerecorder.adapters
|
package com.simplemobiletools.voicerecorder.adapters
|
||||||
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Audio.Media
|
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||||
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
|
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
|
||||||
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
|
|
||||||
import com.simplemobiletools.commons.extensions.*
|
import com.simplemobiletools.commons.extensions.*
|
||||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||||
import com.simplemobiletools.commons.helpers.isQPlus
|
import com.simplemobiletools.commons.helpers.isQPlus
|
||||||
import com.simplemobiletools.commons.helpers.isRPlus
|
|
||||||
import com.simplemobiletools.commons.views.MyRecyclerView
|
import com.simplemobiletools.commons.views.MyRecyclerView
|
||||||
import com.simplemobiletools.voicerecorder.BuildConfig
|
import com.simplemobiletools.voicerecorder.BuildConfig
|
||||||
import com.simplemobiletools.voicerecorder.R
|
import com.simplemobiletools.voicerecorder.R
|
||||||
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
|
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
|
||||||
|
import com.simplemobiletools.voicerecorder.dialogs.DeleteConfirmationDialog
|
||||||
import com.simplemobiletools.voicerecorder.dialogs.RenameRecordingDialog
|
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.helpers.getAudioFileContentUri
|
||||||
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
|
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
|
||||||
|
import com.simplemobiletools.voicerecorder.models.Events
|
||||||
import com.simplemobiletools.voicerecorder.models.Recording
|
import com.simplemobiletools.voicerecorder.models.Recording
|
||||||
import kotlinx.android.synthetic.main.item_recording.view.*
|
import kotlinx.android.synthetic.main.item_recording.view.*
|
||||||
import java.io.File
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
class RecordingsAdapter(
|
class RecordingsAdapter(
|
||||||
activity: SimpleActivity,
|
activity: SimpleActivity,
|
||||||
@ -135,12 +135,21 @@ class RecordingsAdapter(
|
|||||||
resources.getQuantityString(R.plurals.delete_recordings, itemsCnt, itemsCnt)
|
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)
|
val question = String.format(resources.getString(baseString), items)
|
||||||
|
|
||||||
ConfirmationDialog(activity, question) {
|
DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin) { skipRecycleBin ->
|
||||||
ensureBackgroundThread {
|
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 recordingsToRemove = recordings.filter { selectedKeys.contains(it.id) } as ArrayList<Recording>
|
||||||
val positions = getSelectedItemPositions()
|
val positions = getSelectedItemPositions()
|
||||||
|
|
||||||
when {
|
activity.deleteRecordings(recordingsToRemove) { success ->
|
||||||
isRPlus() -> {
|
if (success) {
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
|
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)
|
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
|
||||||
|
EventBus.getDefault().post(Events.RecordingTrashUpdated())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,16 +266,19 @@ class RecordingsAdapter(
|
|||||||
renameRecording()
|
renameRecording()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.cab_share -> {
|
R.id.cab_share -> {
|
||||||
executeItemMenuOperation(recordingId) {
|
executeItemMenuOperation(recordingId) {
|
||||||
shareRecordings()
|
shareRecordings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.cab_open_with -> {
|
R.id.cab_open_with -> {
|
||||||
executeItemMenuOperation(recordingId) {
|
executeItemMenuOperation(recordingId) {
|
||||||
openRecordingWith()
|
openRecordingWith()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.cab_delete -> {
|
R.id.cab_delete -> {
|
||||||
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
|
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
|
||||||
askConfirmDelete()
|
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.activities.SimpleActivity
|
||||||
import com.simplemobiletools.voicerecorder.fragments.MyViewPagerFragment
|
import com.simplemobiletools.voicerecorder.fragments.MyViewPagerFragment
|
||||||
import com.simplemobiletools.voicerecorder.fragments.PlayerFragment
|
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>()
|
private val mFragments = SparseArray<MyViewPagerFragment>()
|
||||||
|
|
||||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
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)
|
val view = activity.layoutInflater.inflate(layout, container, false)
|
||||||
container.addView(view)
|
container.addView(view)
|
||||||
|
|
||||||
@ -26,7 +32,11 @@ class ViewPagerAdapter(private val activity: SimpleActivity) : PagerAdapter() {
|
|||||||
container.removeView(item as View)
|
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
|
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) {
|
fun searchTextChanged(text: String) {
|
||||||
(mFragments[1] as? PlayerFragment)?.onSearchTextChanged(text)
|
(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,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)) {}
|
||||||
|
}
|
@ -1,20 +1,28 @@
|
|||||||
package com.simplemobiletools.voicerecorder.extensions
|
package com.simplemobiletools.voicerecorder.extensions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import com.simplemobiletools.commons.extensions.internalStoragePath
|
import android.provider.MediaStore
|
||||||
import com.simplemobiletools.commons.helpers.isQPlus
|
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.R
|
||||||
import com.simplemobiletools.voicerecorder.helpers.Config
|
import com.simplemobiletools.voicerecorder.helpers.*
|
||||||
import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING
|
import com.simplemobiletools.voicerecorder.models.Recording
|
||||||
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
|
import java.io.File
|
||||||
import com.simplemobiletools.voicerecorder.helpers.TOGGLE_WIDGET_UI
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
val Context.config: Config get() = Config.newInstance(applicationContext)
|
val Context.config: Config get() = Config.newInstance(applicationContext)
|
||||||
|
|
||||||
@ -51,3 +59,182 @@ fun Context.getDefaultRecordingsRelativePath(): String {
|
|||||||
getString(R.string.app_name)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
package com.simplemobiletools.voicerecorder.fragments
|
package com.simplemobiletools.voicerecorder.fragments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Audio.Media
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import com.simplemobiletools.commons.extensions.*
|
import com.simplemobiletools.commons.extensions.*
|
||||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
|
||||||
import com.simplemobiletools.commons.helpers.isQPlus
|
import com.simplemobiletools.commons.helpers.isQPlus
|
||||||
import com.simplemobiletools.commons.helpers.isRPlus
|
|
||||||
import com.simplemobiletools.voicerecorder.R
|
import com.simplemobiletools.voicerecorder.R
|
||||||
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
|
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
|
||||||
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
|
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
|
||||||
import com.simplemobiletools.voicerecorder.extensions.config
|
import com.simplemobiletools.voicerecorder.extensions.config
|
||||||
|
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
|
||||||
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
|
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
|
||||||
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
|
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
|
||||||
import com.simplemobiletools.voicerecorder.models.Events
|
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.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import java.io.File
|
import java.util.Stack
|
||||||
import java.util.*
|
import java.util.Timer
|
||||||
import kotlin.math.roundToLong
|
import java.util.TimerTask
|
||||||
|
|
||||||
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
|
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
|
||||||
private val FAST_FORWARD_SKIP_MS = 10000
|
private val FAST_FORWARD_SKIP_MS = 10000
|
||||||
@ -45,18 +40,19 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
private var lastSearchQuery = ""
|
private var lastSearchQuery = ""
|
||||||
private var bus: EventBus? = null
|
private var bus: EventBus? = null
|
||||||
private var prevSavePath = ""
|
private var prevSavePath = ""
|
||||||
|
private var prevRecycleBinState = context.config.useRecycleBin
|
||||||
private var playOnPreparation = true
|
private var playOnPreparation = true
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
setupColors()
|
setupColors()
|
||||||
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath) {
|
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath || context.config.useRecycleBin != prevRecycleBinState) {
|
||||||
itemsIgnoringSearch = getRecordings()
|
itemsIgnoringSearch = getRecordings()
|
||||||
setupAdapter(itemsIgnoringSearch)
|
setupAdapter(itemsIgnoringSearch)
|
||||||
} else {
|
} else {
|
||||||
getRecordingsAdapter()?.updateTextColor(context.getProperTextColor())
|
getRecordingsAdapter()?.updateTextColor(context.getProperTextColor())
|
||||||
}
|
}
|
||||||
|
|
||||||
storePrevPath()
|
storePrevState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@ -78,7 +74,7 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
setupAdapter(itemsIgnoringSearch)
|
setupAdapter(itemsIgnoringSearch)
|
||||||
initMediaPlayer()
|
initMediaPlayer()
|
||||||
setupViews()
|
setupViews()
|
||||||
storePrevPath()
|
storePrevState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
@ -141,164 +137,49 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAdapter(recordings: ArrayList<Recording>) {
|
private fun setupAdapter(recordings: ArrayList<Recording>) {
|
||||||
ensureBackgroundThread {
|
recordings_fastscroller.beVisibleIf(recordings.isNotEmpty())
|
||||||
Handler(Looper.getMainLooper()).post {
|
recordings_placeholder.beVisibleIf(recordings.isEmpty())
|
||||||
recordings_fastscroller.beVisibleIf(recordings.isNotEmpty())
|
if (recordings.isEmpty()) {
|
||||||
recordings_placeholder.beVisibleIf(recordings.isEmpty())
|
val stringId = if (lastSearchQuery.isEmpty()) {
|
||||||
if (recordings.isEmpty()) {
|
if (isQPlus()) {
|
||||||
val stringId = if (lastSearchQuery.isEmpty()) {
|
R.string.no_recordings_found
|
||||||
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()
|
|
||||||
}
|
|
||||||
} else {
|
} 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> {
|
private fun getRecordings(): ArrayList<Recording> {
|
||||||
val recordings = ArrayList<Recording>()
|
return context.getAllRecordings().apply {
|
||||||
return when {
|
|
||||||
isRPlus() -> {
|
|
||||||
recordings.addAll(getMediaStoreRecordings())
|
|
||||||
recordings.addAll(getSAFRecordings())
|
|
||||||
recordings
|
|
||||||
}
|
|
||||||
isQPlus() -> {
|
|
||||||
recordings.addAll(getMediaStoreRecordings())
|
|
||||||
recordings.addAll(getLegacyRecordings())
|
|
||||||
recordings
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
recordings.addAll(getLegacyRecordings())
|
|
||||||
recordings
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
sortByDescending { it.timestamp }
|
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() {
|
private fun initMediaPlayer() {
|
||||||
player = MediaPlayer().apply {
|
player = MediaPlayer().apply {
|
||||||
setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||||
@ -336,9 +217,11 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
DocumentsContract.isDocumentUri(context, uri) -> {
|
DocumentsContract.isDocumentUri(context, uri) -> {
|
||||||
setDataSource(context, uri)
|
setDataSource(context, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
recording.path.isEmpty() -> {
|
recording.path.isEmpty() -> {
|
||||||
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
|
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
setDataSource(recording.path)
|
setDataSource(recording.path)
|
||||||
}
|
}
|
||||||
@ -452,8 +335,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
|
|
||||||
private fun getRecordingsAdapter() = recordings_list.adapter as? RecordingsAdapter
|
private fun getRecordingsAdapter() = recordings_list.adapter as? RecordingsAdapter
|
||||||
|
|
||||||
private fun storePrevPath() {
|
private fun storePrevState() {
|
||||||
prevSavePath = context!!.config.saveRecordingsFolder
|
prevSavePath = context!!.config.saveRecordingsFolder
|
||||||
|
prevRecycleBinState = context.config.useRecycleBin
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupColors() {
|
private fun setupColors() {
|
||||||
@ -476,4 +360,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
|
|||||||
fun recordingCompleted(event: Events.RecordingCompleted) {
|
fun recordingCompleted(event: Events.RecordingCompleted) {
|
||||||
refreshRecordings()
|
refreshRecordings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
|
||||||
|
refreshRecordings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -72,4 +72,12 @@ class Config(context: Context) : BaseConfig(context) {
|
|||||||
EXTENSION_OGG -> MediaRecorder.AudioEncoder.OPUS
|
EXTENSION_OGG -> MediaRecorder.AudioEncoder.OPUS
|
||||||
else -> MediaRecorder.AudioEncoder.AAC
|
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 AUDIO_SOURCE = "audio_source"
|
||||||
const val BITRATE = "bitrate"
|
const val BITRATE = "bitrate"
|
||||||
const val RECORD_AFTER_LAUNCH = "record_after_launch"
|
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")
|
@SuppressLint("InlinedApi")
|
||||||
fun getAudioFileContentUri(id: Long): Uri {
|
fun getAudioFileContentUri(id: Long): Uri {
|
||||||
|
@ -7,5 +7,6 @@ class Events {
|
|||||||
class RecordingStatus internal constructor(val status: Int)
|
class RecordingStatus internal constructor(val status: Int)
|
||||||
class RecordingAmplitude internal constructor(val amplitude: Int)
|
class RecordingAmplitude internal constructor(val amplitude: Int)
|
||||||
class RecordingCompleted internal constructor()
|
class RecordingCompleted internal constructor()
|
||||||
|
class RecordingTrashUpdated internal constructor()
|
||||||
class RecordingSaved internal constructor(val uri: Uri?)
|
class RecordingSaved internal constructor(val uri: Uri?)
|
||||||
}
|
}
|
||||||
|
@ -268,6 +268,55 @@
|
|||||||
tools:text="128 kbps" />
|
tools:text="128 kbps" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</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>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</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>
|
37
app/src/main/res/layout/fragment_trash.xml
Normal file
37
app/src/main/res/layout/fragment_trash.xml
Normal 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>
|
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…
x
Reference in New Issue
Block a user