Merge pull request #67 from ronniedroid/import_export_keywords

FEATURE: Import and export blocked keywords
This commit is contained in:
Naveen Singh 2024-12-25 21:43:04 +05:30 committed by GitHub
commit 417737b6a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 437 additions and 14 deletions

View file

@ -1,21 +1,47 @@
package org.fossify.messages.activities
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.extensions.*
import org.fossify.commons.dialogs.ExportBlockedNumbersDialog
import org.fossify.commons.dialogs.FilePickerDialog
import org.fossify.commons.extensions.beVisibleIf
import org.fossify.commons.extensions.getFileOutputStream
import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.getTempFile
import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.extensions.toFileDirItem
import org.fossify.commons.extensions.toast
import org.fossify.commons.extensions.underlineText
import org.fossify.commons.extensions.updateTextColors
import org.fossify.commons.extensions.viewBinding
import org.fossify.commons.helpers.APP_ICON_IDS
import org.fossify.commons.helpers.APP_LAUNCHER_NAME
import org.fossify.commons.helpers.ExportResult
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.PERMISSION_READ_STORAGE
import org.fossify.commons.helpers.PERMISSION_WRITE_STORAGE
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.helpers.isQPlus
import org.fossify.commons.interfaces.RefreshRecyclerViewListener
import org.fossify.messages.R
import org.fossify.messages.databinding.ActivityManageBlockedKeywordsBinding
import org.fossify.messages.dialogs.AddBlockedKeywordDialog
import org.fossify.messages.dialogs.ExportBlockedKeywordsDialog
import org.fossify.messages.dialogs.ManageBlockedKeywordsAdapter
import org.fossify.messages.extensions.config
import org.fossify.messages.extensions.toArrayList
import org.fossify.messages.helpers.BlockedKeywordsExporter
import org.fossify.messages.helpers.BlockedKeywordsImporter
import java.io.FileOutputStream
import java.io.OutputStream
class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewListener {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
@ -35,7 +61,10 @@ class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewL
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.manageBlockedKeywordsList, toolbar = binding.blockKeywordsToolbar)
setupMaterialScrollListener(
scrollingView = binding.manageBlockedKeywordsList,
toolbar = binding.blockKeywordsToolbar
)
updateTextColors(binding.manageBlockedKeywordsWrapper)
binding.manageBlockedKeywordsPlaceholder2.apply {
@ -60,11 +89,165 @@ class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewL
true
}
R.id.export_blocked_keywords -> {
tryExportBlockedNumbers()
true
}
R.id.import_blocked_keywords -> {
tryImportBlockedKeywords()
true
}
else -> false
}
}
}
private val exportActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
try {
val outputStream = uri?.let { contentResolver.openOutputStream(it) }
if (outputStream != null) {
exportBlockedKeywordsTo(outputStream)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
private val importActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
try {
if (uri != null) {
tryImportBlockedKeywordsFromFile(uri)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
private fun tryImportBlockedKeywords() {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
try {
importActivityResultLauncher.launch(type)
} catch (e: ActivityNotFoundException) {
toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
}
}
} else {
handlePermission(PERMISSION_READ_STORAGE) { isAllowed ->
if (isAllowed) {
pickFileToImportBlockedKeywords()
}
}
}
}
private fun pickFileToImportBlockedKeywords() {
FilePickerDialog(this) {
importBlockedKeywords(it)
}
}
private fun tryImportBlockedKeywordsFromFile(uri: Uri) {
when (uri.scheme) {
"file" -> importBlockedKeywords(uri.path!!)
"content" -> {
val tempFile = getTempFile("blocked", "blocked_keywords.txt")
if (tempFile == null) {
toast(org.fossify.commons.R.string.unknown_error_occurred)
return
}
try {
val inputStream = contentResolver.openInputStream(uri)
val out = FileOutputStream(tempFile)
inputStream!!.copyTo(out)
importBlockedKeywords(tempFile.absolutePath)
} catch (e: Exception) {
showErrorToast(e)
}
}
else -> toast(org.fossify.commons.R.string.invalid_file_format)
}
}
private fun importBlockedKeywords(path: String) {
ensureBackgroundThread {
val result = BlockedKeywordsImporter(this).importBlockedKeywords(path)
toast(
when (result) {
BlockedKeywordsImporter.ImportResult.IMPORT_OK -> org.fossify.commons.R.string.importing_successful
BlockedKeywordsImporter.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found
}
)
updateBlockedKeywords()
}
}
private fun exportBlockedKeywordsTo(outputStream: OutputStream?) {
ensureBackgroundThread {
val blockedKeywords = config.blockedKeywords.toArrayList()
if (blockedKeywords.isEmpty()) {
toast(org.fossify.commons.R.string.no_entries_for_exporting)
} else {
BlockedKeywordsExporter.exportBlockedKeywords(blockedKeywords, outputStream) {
toast(
when (it) {
ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful
else -> org.fossify.commons.R.string.exporting_failed
}
)
}
}
}
}
private fun tryExportBlockedNumbers() {
if (isQPlus()) {
ExportBlockedKeywordsDialog(this, config.lastBlockedKeywordExportPath, true) { file ->
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE)
try {
exportActivityResultLauncher.launch(file.name)
} catch (e: ActivityNotFoundException) {
toast(
org.fossify.commons.R.string.system_service_disabled,
Toast.LENGTH_LONG
)
} catch (e: Exception) {
showErrorToast(e)
}
}
}
} else {
handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed ->
if (isAllowed) {
ExportBlockedNumbersDialog(
this,
config.lastBlockedKeywordExportPath,
false
) { file ->
getFileOutputStream(file.toFileDirItem(this), true) { out ->
exportBlockedKeywordsTo(out)
}
}
}
}
}
}
override fun refreshItems() {
updateBlockedKeywords()
}
@ -73,7 +256,12 @@ class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewL
ensureBackgroundThread {
val blockedKeywords = config.blockedKeywords.sorted().toArrayList()
runOnUiThread {
ManageBlockedKeywordsAdapter(this, blockedKeywords, this, binding.manageBlockedKeywordsList) {
ManageBlockedKeywordsAdapter(
activity = this,
blockedKeywords = blockedKeywords,
listener = this,
recyclerView = binding.manageBlockedKeywordsList
) {
addOrEditBlockedKeyword(it as String)
}.apply {
binding.manageBlockedKeywordsList.adapter = this

View file

@ -0,0 +1,88 @@
package org.fossify.messages.dialogs
import androidx.appcompat.app.AlertDialog
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.dialogs.FilePickerDialog
import org.fossify.commons.extensions.beGone
import org.fossify.commons.extensions.getAlertDialogBuilder
import org.fossify.commons.extensions.getCurrentFormattedDateTime
import org.fossify.commons.extensions.getParentPath
import org.fossify.commons.extensions.humanizePath
import org.fossify.commons.extensions.internalStoragePath
import org.fossify.commons.extensions.isAValidFilename
import org.fossify.commons.extensions.setupDialogStuff
import org.fossify.commons.extensions.showKeyboard
import org.fossify.commons.extensions.toast
import org.fossify.commons.extensions.value
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.R
import org.fossify.messages.databinding.DialogExportBlockedKeywordsBinding
import org.fossify.messages.extensions.config
import org.fossify.messages.helpers.BLOCKED_KEYWORDS_EXPORT_EXTENSION
import java.io.File
class ExportBlockedKeywordsDialog(
val activity: BaseSimpleActivity,
val path: String,
val hidePath: Boolean,
callback: (file: File) -> Unit,
) {
private var realPath = path.ifEmpty { activity.internalStoragePath }
private val config = activity.config
init {
val view =
DialogExportBlockedKeywordsBinding.inflate(activity.layoutInflater, null, false).apply {
exportBlockedKeywordsFolder.text = activity.humanizePath(realPath)
exportBlockedKeywordsFilename.setText("${activity.getString(R.string.blocked_keywords)}_${activity.getCurrentFormattedDateTime()}")
if (hidePath) {
exportBlockedKeywordsFolderLabel.beGone()
exportBlockedKeywordsFolder.beGone()
} else {
exportBlockedKeywordsFolder.setOnClickListener {
FilePickerDialog(activity, realPath, false, showFAB = true) {
exportBlockedKeywordsFolder.text = activity.humanizePath(it)
realPath = it
}
}
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(org.fossify.commons.R.string.ok, null)
.setNegativeButton(org.fossify.commons.R.string.cancel, null)
.apply {
activity.setupDialogStuff(
view = view.root,
dialog = this,
titleId = R.string.export_blocked_keywords
) { alertDialog ->
alertDialog.showKeyboard(view.exportBlockedKeywordsFilename)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val filename = view.exportBlockedKeywordsFilename.value
when {
filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name)
filename.isAValidFilename() -> {
val file =
File(realPath, "$filename$BLOCKED_KEYWORDS_EXPORT_EXTENSION")
if (!hidePath && file.exists()) {
activity.toast(org.fossify.commons.R.string.name_taken)
return@setOnClickListener
}
ensureBackgroundThread {
config.lastBlockedKeywordExportPath =
file.absolutePath.getParentPath()
callback(file)
alertDialog.dismiss()
}
}
else -> activity.toast(org.fossify.commons.R.string.invalid_name)
}
}
}
}
}
}

View file

@ -0,0 +1,29 @@
package org.fossify.messages.helpers
import org.fossify.commons.helpers.ExportResult
import java.io.OutputStream
object BlockedKeywordsExporter {
fun exportBlockedKeywords(
blockedKeywords: ArrayList<String>,
outputStream: OutputStream?,
callback: (result: ExportResult) -> Unit,
) {
if (outputStream == null) {
callback.invoke(ExportResult.EXPORT_FAIL)
return
}
try {
outputStream.bufferedWriter().use { out ->
out.write(blockedKeywords.joinToString(BLOCKED_KEYWORDS_EXPORT_DELIMITER) {
it
})
}
callback.invoke(ExportResult.EXPORT_OK)
} catch (e: Exception) {
callback.invoke(ExportResult.EXPORT_FAIL)
}
}
}

View file

@ -0,0 +1,37 @@
package org.fossify.messages.helpers
import android.app.Activity
import org.fossify.commons.extensions.showErrorToast
import org.fossify.messages.extensions.config
import java.io.File
class BlockedKeywordsImporter(
private val activity: Activity,
) {
enum class ImportResult {
IMPORT_FAIL, IMPORT_OK
}
fun importBlockedKeywords(path: String): ImportResult {
return try {
val inputStream = File(path).inputStream()
val keywords = inputStream.bufferedReader().use {
val content = it.readText().trimEnd().split(BLOCKED_KEYWORDS_EXPORT_DELIMITER)
content
}
if (keywords.isNotEmpty()) {
keywords.forEach { keyword: String ->
activity.config.addBlockedKeyword(keyword)
}
ImportResult.IMPORT_OK
} else {
ImportResult.IMPORT_FAIL
}
} catch (e: Exception) {
activity.showErrorToast(e)
ImportResult.IMPORT_FAIL
}
}
}

View file

@ -18,11 +18,13 @@ class Config(context: Context) : BaseConfig(context) {
var showCharacterCounter: Boolean
get() = prefs.getBoolean(SHOW_CHARACTER_COUNTER, false)
set(showCharacterCounter) = prefs.edit().putBoolean(SHOW_CHARACTER_COUNTER, showCharacterCounter).apply()
set(showCharacterCounter) = prefs.edit()
.putBoolean(SHOW_CHARACTER_COUNTER, showCharacterCounter).apply()
var useSimpleCharacters: Boolean
get() = prefs.getBoolean(USE_SIMPLE_CHARACTERS, false)
set(useSimpleCharacters) = prefs.edit().putBoolean(USE_SIMPLE_CHARACTERS, useSimpleCharacters).apply()
set(useSimpleCharacters) = prefs.edit()
.putBoolean(USE_SIMPLE_CHARACTERS, useSimpleCharacters).apply()
var sendOnEnter: Boolean
get() = prefs.getBoolean(SEND_ON_ENTER, false)
@ -30,19 +32,23 @@ class Config(context: Context) : BaseConfig(context) {
var enableDeliveryReports: Boolean
get() = prefs.getBoolean(ENABLE_DELIVERY_REPORTS, false)
set(enableDeliveryReports) = prefs.edit().putBoolean(ENABLE_DELIVERY_REPORTS, enableDeliveryReports).apply()
set(enableDeliveryReports) = prefs.edit()
.putBoolean(ENABLE_DELIVERY_REPORTS, enableDeliveryReports).apply()
var sendLongMessageMMS: Boolean
get() = prefs.getBoolean(SEND_LONG_MESSAGE_MMS, false)
set(sendLongMessageMMS) = prefs.edit().putBoolean(SEND_LONG_MESSAGE_MMS, sendLongMessageMMS).apply()
set(sendLongMessageMMS) = prefs.edit().putBoolean(SEND_LONG_MESSAGE_MMS, sendLongMessageMMS)
.apply()
var sendGroupMessageMMS: Boolean
get() = prefs.getBoolean(SEND_GROUP_MESSAGE_MMS, false)
set(sendGroupMessageMMS) = prefs.edit().putBoolean(SEND_GROUP_MESSAGE_MMS, sendGroupMessageMMS).apply()
set(sendGroupMessageMMS) = prefs.edit()
.putBoolean(SEND_GROUP_MESSAGE_MMS, sendGroupMessageMMS).apply()
var lockScreenVisibilitySetting: Int
get() = prefs.getInt(LOCK_SCREEN_VISIBILITY, LOCK_SCREEN_SENDER_MESSAGE)
set(lockScreenVisibilitySetting) = prefs.edit().putInt(LOCK_SCREEN_VISIBILITY, lockScreenVisibilitySetting).apply()
set(lockScreenVisibilitySetting) = prefs.edit()
.putInt(LOCK_SCREEN_VISIBILITY, lockScreenVisibilitySetting).apply()
var mmsFileSizeLimit: Long
get() = prefs.getLong(MMS_FILE_SIZE_LIMIT, FILE_SIZE_600_KB)
@ -50,7 +56,8 @@ class Config(context: Context) : BaseConfig(context) {
var pinnedConversations: Set<String>
get() = prefs.getStringSet(PINNED_CONVERSATIONS, HashSet<String>())!!
set(pinnedConversations) = prefs.edit().putStringSet(PINNED_CONVERSATIONS, pinnedConversations).apply()
set(pinnedConversations) = prefs.edit()
.putStringSet(PINNED_CONVERSATIONS, pinnedConversations).apply()
fun addPinnedConversationByThreadId(threadId: Long) {
pinnedConversations = pinnedConversations.plus(threadId.toString())
@ -65,7 +72,8 @@ class Config(context: Context) : BaseConfig(context) {
}
fun removePinnedConversations(conversations: List<Conversation>) {
pinnedConversations = pinnedConversations.minus(conversations.map { it.threadId.toString() })
pinnedConversations =
pinnedConversations.minus(conversations.map { it.threadId.toString() })
}
var blockedKeywords: Set<String>
@ -110,15 +118,18 @@ class Config(context: Context) : BaseConfig(context) {
var lastRecycleBinCheck: Long
get() = prefs.getLong(LAST_RECYCLE_BIN_CHECK, 0L)
set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck).apply()
set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck)
.apply()
var isArchiveAvailable: Boolean
get() = prefs.getBoolean(IS_ARCHIVE_AVAILABLE, true)
set(isArchiveAvailable) = prefs.edit().putBoolean(IS_ARCHIVE_AVAILABLE, isArchiveAvailable).apply()
set(isArchiveAvailable) = prefs.edit().putBoolean(IS_ARCHIVE_AVAILABLE, isArchiveAvailable)
.apply()
var customNotifications: Set<String>
get() = prefs.getStringSet(CUSTOM_NOTIFICATIONS, HashSet<String>())!!
set(customNotifications) = prefs.edit().putStringSet(CUSTOM_NOTIFICATIONS, customNotifications).apply()
set(customNotifications) = prefs.edit()
.putStringSet(CUSTOM_NOTIFICATIONS, customNotifications).apply()
fun addCustomNotificationsByThreadId(threadId: Long) {
customNotifications = customNotifications.plus(threadId.toString())
@ -127,4 +138,9 @@ class Config(context: Context) : BaseConfig(context) {
fun removeCustomNotificationsByThreadId(threadId: Long) {
customNotifications = customNotifications.minus(threadId.toString())
}
var lastBlockedKeywordExportPath: String
get() = prefs.getString(LAST_BLOCKED_KEYWORD_EXPORT_PATH, "")!!
set(lastBlockedNumbersExportPath) = prefs.edit()
.putString(LAST_BLOCKED_KEYWORD_EXPORT_PATH, lastBlockedNumbersExportPath).apply()
}

View file

@ -26,6 +26,7 @@ const val SEND_GROUP_MESSAGE_MMS = "send_group_message_mms"
const val MMS_FILE_SIZE_LIMIT = "mms_file_size_limit"
const val PINNED_CONVERSATIONS = "pinned_conversations"
const val BLOCKED_KEYWORDS = "blocked_keywords"
const val LAST_BLOCKED_KEYWORD_EXPORT_PATH = "last_blocked_keyword_export_path"
const val EXPORT_SMS = "export_sms"
const val EXPORT_MMS = "export_mms"
const val JSON_FILE_EXTENSION = ".json"
@ -89,6 +90,9 @@ const val CAPTURE_AUDIO_INTENT = 46
const val PICK_DOCUMENT_INTENT = 47
const val PICK_CONTACT_INTENT = 48
const val BLOCKED_KEYWORDS_EXPORT_DELIMITER = ","
const val BLOCKED_KEYWORDS_EXPORT_EXTENSION = ".txt"
fun refreshMessages() {
EventBus.getDefault().post(Events.RefreshMessages())
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/export_blocked_keywords_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/export_blocked_keywords_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_margin"
android:paddingTop="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin">
<org.fossify.commons.views.MyTextView
android:id="@+id/export_blocked_keywords_folder_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/small_margin"
android:text="@string/folder"
android:textSize="@dimen/smaller_text_size" />
<org.fossify.commons.views.MyTextView
android:id="@+id/export_blocked_keywords_folder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/small_margin"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/small_margin"
android:paddingBottom="@dimen/activity_margin" />
<org.fossify.commons.views.MyTextInputLayout
android:id="@+id/export_blocked_keywords_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/filename_without_txt">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/export_blocked_keywords_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:singleLine="true"
android:textCursorDrawable="@null"
android:textSize="@dimen/normal_text_size" />
</org.fossify.commons.views.MyTextInputLayout>
</LinearLayout>
</ScrollView>

View file

@ -6,4 +6,12 @@
android:icon="@drawable/ic_plus_vector"
android:title="@string/add_a_blocked_keyword"
app:showAsAction="ifRoom" />
<item
android:id="@+id/import_blocked_keywords"
android:title="@string/import_blocked_keywords"
app:showAsAction="never" />
<item
android:id="@+id/export_blocked_keywords"
android:title="@string/export_blocked_keywords"
app:showAsAction="never" />
</menu>

View file

@ -106,6 +106,7 @@
<string name="send_long_message_mms">Send long messages as MMS</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="keywords">Keywords</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
@ -113,6 +114,8 @@
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="no_option_selected">You have to select at least one item</string>
<string name="export_blocked_keywords">Export blocked keywords</string>
<string name="import_blocked_keywords">Import blocked keywords</string>
<!-- Errors -->
<string name="empty_destination_address">Can\'t send message to an empty number</string>
<string name="unable_to_save_message">Unable to save message to the telephony database</string>