forked from upstream-mirrors/quik
limit bubble display of text to 5000 chars.
add instruction status message when bubble char limit exceeded. add share text feature. make copy and share text menu options only available when there is text in selected messages. new FileProvider to share messages text files out of app's cache dir. add message subject to copy messages text feature. a lot of refactoring of MessagesAdapter class. add new FileUtils class. convert file logging to use new FileUtils class. change MediaRecorder to use new FileUtils class. housekeeping worker now cleans messages text cache files. housekeeping worker now checks cache files older than 2 hours before deleting.
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* This file is part of QKSMS.
|
||||
*
|
||||
* QKSMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QKSMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package dev.octoshrimpy.quik.extensions
|
||||
|
||||
import java.text.Normalizer
|
||||
|
||||
/**
|
||||
* Strip the accents from a string
|
||||
*/
|
||||
fun CharSequence.removeAccents(): String = Normalizer.normalize(this, Normalizer.Form.NFKD).replace(Regex("\\p{M}"), "")
|
||||
@@ -23,8 +23,8 @@ import android.media.AudioDeviceInfo
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import androidx.core.net.toFile
|
||||
import dev.octoshrimpy.quik.util.FileUtils
|
||||
import java.util.UUID
|
||||
|
||||
object MediaRecorderManager : MediaRecorder() {
|
||||
@@ -63,10 +63,16 @@ object MediaRecorderManager : MediaRecorder() {
|
||||
|
||||
fun startRecording(context: Context, preferredAudioDevice: AudioDeviceInfo? = null): Uri {
|
||||
return try {
|
||||
val file = File(
|
||||
context.cacheDir,
|
||||
AUDIO_FILE_PREFIX + UUID.randomUUID() + AUDIO_FILE_SUFFIX
|
||||
val (newUri, e) = FileUtils.create(
|
||||
FileUtils.Companion.Location.Cache,
|
||||
context,
|
||||
"$AUDIO_FILE_PREFIX${UUID.randomUUID()}$AUDIO_FILE_SUFFIX",
|
||||
""
|
||||
)
|
||||
if (e is Exception)
|
||||
throw e
|
||||
|
||||
uri = newUri
|
||||
|
||||
// ensure stopped before using again
|
||||
stopRecording()
|
||||
@@ -76,13 +82,12 @@ object MediaRecorderManager : MediaRecorder() {
|
||||
setOutputFormat(OutputFormat.THREE_GPP)
|
||||
setAudioEncoder(AudioEncoder.AMR_WB)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
preferredDevice = preferredAudioDevice
|
||||
}
|
||||
|
||||
recordingState = RecordingState.DataSourceConfigured
|
||||
|
||||
setOutputFile(file.path)
|
||||
setOutputFile(uri.toFile().path)
|
||||
|
||||
prepare()
|
||||
recordingState = RecordingState.Prepared
|
||||
@@ -90,8 +95,6 @@ object MediaRecorderManager : MediaRecorder() {
|
||||
start()
|
||||
recordingState = RecordingState.Recording
|
||||
|
||||
uri = file.toUri()
|
||||
|
||||
uri
|
||||
}
|
||||
catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.moez.QKSMS.util
|
||||
|
||||
class Constants {
|
||||
companion object {
|
||||
const val SAVED_MESSAGE_TEXT_FILE_PREFIX = "QuikSmsText-"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package dev.octoshrimpy.quik.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.core.net.toFile
|
||||
import dev.octoshrimpy.quik.extensions.isEmpty
|
||||
import java.io.File
|
||||
|
||||
class FileUtils {
|
||||
companion object {
|
||||
enum class Location {
|
||||
Downloads,
|
||||
ExternalCache,
|
||||
Cache,
|
||||
Files,
|
||||
Obb,
|
||||
CodeCache,
|
||||
NoBackup,
|
||||
}
|
||||
|
||||
fun create(
|
||||
location: Location,
|
||||
context: Context,
|
||||
filename: String,
|
||||
mimeType: String
|
||||
) =
|
||||
if (location == Location.Downloads) createDownloadsFile(context, filename, mimeType)
|
||||
else createLocation(location, context, filename)
|
||||
|
||||
private fun createDownloadsFile(
|
||||
context: Context,
|
||||
filename: String,
|
||||
mimeType: String
|
||||
): Pair<Uri, Exception?> =
|
||||
try {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // use media store
|
||||
context.contentResolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
contentValuesOf(
|
||||
MediaStore.MediaColumns.MIME_TYPE to mimeType,
|
||||
MediaStore.MediaColumns.RELATIVE_PATH to
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME to filename,
|
||||
)
|
||||
) ?: Uri.EMPTY
|
||||
} else { // use direct access to 'external' dir
|
||||
File(Environment.getExternalStorageDirectory(), filename).let {
|
||||
if (tryOrNull { it.createNewFile() } == true) Uri.fromFile(it)
|
||||
else Uri.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
if (uri.isEmpty())
|
||||
throw Exception("Opening file returned an empty uri")
|
||||
|
||||
Pair(uri, null)
|
||||
} catch (e: Exception) {
|
||||
Pair(Uri.EMPTY, e)
|
||||
}
|
||||
|
||||
private fun createLocation(
|
||||
location: Location,
|
||||
context: Context,
|
||||
filename: String
|
||||
): Pair<Uri, Exception?> =
|
||||
try {
|
||||
val uri = File(
|
||||
when (location) {
|
||||
Location.ExternalCache -> context.externalCacheDir
|
||||
Location.Files -> context.filesDir
|
||||
Location.Obb -> context.obbDir
|
||||
Location.CodeCache -> context.codeCacheDir
|
||||
Location.NoBackup -> context.noBackupFilesDir
|
||||
else -> context.cacheDir
|
||||
},
|
||||
filename
|
||||
).let {
|
||||
if (tryOrNull { it.createNewFile() } == true) Uri.fromFile(it)
|
||||
else Uri.EMPTY
|
||||
}
|
||||
|
||||
if (uri.isEmpty())
|
||||
throw Exception("Opening file returned an empty uri")
|
||||
|
||||
Pair(uri, null)
|
||||
} catch (e: Exception) {
|
||||
Pair(Uri.EMPTY, e)
|
||||
}
|
||||
|
||||
private fun writeBytes(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
bytes: ByteArray,
|
||||
mode: String
|
||||
): Exception? =
|
||||
try {
|
||||
context.contentResolver.openOutputStream(uri, mode)?.use { it.write(bytes) }
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e
|
||||
}
|
||||
|
||||
fun append(context: Context, uri: Uri, bytes: ByteArray): Exception? =
|
||||
writeBytes(context, uri, bytes, "wa")
|
||||
|
||||
fun write(context: Context, uri: Uri, bytes: ByteArray): Exception? =
|
||||
writeBytes(context, uri, bytes, "w")
|
||||
|
||||
fun truncateAndWrite(context: Context, uri: Uri, bytes: ByteArray): Exception? =
|
||||
writeBytes(context, uri, bytes, "wt")
|
||||
|
||||
fun createAndWrite(
|
||||
context: Context,
|
||||
location: Location,
|
||||
filename: String,
|
||||
mimeType: String,
|
||||
bytes: ByteArray
|
||||
): Pair<Uri, Exception?> =
|
||||
create(location, context, filename, mimeType).let { (uri, e) ->
|
||||
if (e is Exception) Pair(Uri.EMPTY, e)
|
||||
else {
|
||||
val we = writeBytes(context, uri, bytes, "w")
|
||||
if (we is Exception) {
|
||||
deleteFile(uri)
|
||||
Pair(Uri.EMPTY, we)
|
||||
}
|
||||
else
|
||||
Pair(uri, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFile(uri: Uri): Boolean {
|
||||
return try {
|
||||
if (uri == Uri.EMPTY) false
|
||||
else uri.toFile().delete()
|
||||
}
|
||||
catch (e: Exception) { false }
|
||||
}
|
||||
|
||||
fun renameTo(fromUri: Uri, toUri: Uri) {
|
||||
fromUri.toFile().renameTo(toUri.toFile())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.moez.QKSMS.util.Constants
|
||||
import androidx.work.workDataOf
|
||||
import com.moez.QKSMS.manager.MediaRecorderManager
|
||||
import com.moez.QKSMS.manager.MediaRecorderManager.AUDIO_FILE_PREFIX
|
||||
@@ -42,6 +43,8 @@ class HousekeepingWorker(appContext: Context, workerParams: WorkerParameters)
|
||||
companion object {
|
||||
private val WORKER_TAG: String = HousekeepingWorker::class.java.simpleName
|
||||
|
||||
private val someHoursAgo = (System.currentTimeMillis() - (2 * 60 * 60 * 1000))
|
||||
|
||||
fun register(context: Context) {
|
||||
// don't check return value because, well, we can't do much about a failure
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
@@ -49,12 +52,12 @@ class HousekeepingWorker(appContext: Context, workerParams: WorkerParameters)
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
PeriodicWorkRequest.Builder(
|
||||
HousekeepingWorker::class.java,
|
||||
24,
|
||||
TimeUnit.HOURS
|
||||
1,
|
||||
TimeUnit.DAYS
|
||||
)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
// idle device constraint kinda guarantees quik won't be in use
|
||||
// idle device constraint helps guarantees quik won't be in use
|
||||
// as files are deleted (primarily for deleting audio recordings)
|
||||
.setRequiresDeviceIdle(true)
|
||||
// good citizens don't use up low batteries
|
||||
@@ -76,7 +79,9 @@ class HousekeepingWorker(appContext: Context, workerParams: WorkerParameters)
|
||||
override fun doWork(): Result {
|
||||
removeOrphanedScheduledMessageAttachmentFiles()
|
||||
|
||||
removeOrphanedComposeAudioRecording()
|
||||
removeOrphanedComposeAudioRecordings()
|
||||
|
||||
removeSavedMessagesTexts()
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
@@ -97,15 +102,21 @@ class HousekeepingWorker(appContext: Context, workerParams: WorkerParameters)
|
||||
?.forEach { it.deleteRecursively() }
|
||||
}
|
||||
|
||||
private fun removeOrphanedComposeAudioRecording() {
|
||||
private fun removeOrphanedComposeAudioRecordings() =
|
||||
// find recording files in cache dir
|
||||
applicationContext.cacheDir.listFiles { entry ->
|
||||
entry.isFile &&
|
||||
entry.name.startsWith(AUDIO_FILE_PREFIX) &&
|
||||
entry.name.endsWith(MediaRecorderManager.AUDIO_FILE_SUFFIX)
|
||||
}
|
||||
// delete recording file
|
||||
?.forEach { it.delete() }
|
||||
}
|
||||
entry.name.startsWith(MediaRecorderManager.AUDIO_FILE_PREFIX) &&
|
||||
entry.name.endsWith(MediaRecorderManager.AUDIO_FILE_SUFFIX) &&
|
||||
(entry.lastModified() < someHoursAgo)
|
||||
}?.forEach { it.delete() } // delete recording file
|
||||
|
||||
private fun removeSavedMessagesTexts() =
|
||||
// find saved message text files in cache dir
|
||||
applicationContext.cacheDir.listFiles { entry ->
|
||||
entry.isFile &&
|
||||
entry.name.startsWith(Constants.SAVED_MESSAGE_TEXT_FILE_PREFIX) &&
|
||||
(entry.lastModified() < someHoursAgo)
|
||||
}?.forEach { it.delete() } // delete message text file
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user