mirror of
https://github.com/FossifyOrg/Clock.git
synced 2026-01-19 05:07:55 -05:00
FEAT: added alram export and import functionality
This commit is contained in:
@@ -1,27 +1,31 @@
|
||||
package org.fossify.clock.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Icon
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import me.grantland.widget.AutofitHelper
|
||||
import org.fossify.clock.BuildConfig
|
||||
import org.fossify.clock.R
|
||||
import org.fossify.clock.adapters.ViewPagerAdapter
|
||||
import org.fossify.clock.databinding.ActivityMainBinding
|
||||
import org.fossify.clock.extensions.config
|
||||
import org.fossify.clock.extensions.getEnabledAlarms
|
||||
import org.fossify.clock.extensions.rescheduleEnabledAlarms
|
||||
import org.fossify.clock.extensions.updateWidgets
|
||||
import org.fossify.clock.extensions.*
|
||||
import org.fossify.clock.helpers.*
|
||||
import org.fossify.commons.databinding.BottomTablayoutItemBinding
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.*
|
||||
import org.fossify.commons.models.FAQItem
|
||||
import org.fossify.clock.dialogs.ExportAlarmsDialog
|
||||
import org.fossify.commons.dialogs.FilePickerDialog
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class MainActivity : SimpleActivity() {
|
||||
private var storedTextColor = 0
|
||||
@@ -29,6 +33,11 @@ class MainActivity : SimpleActivity() {
|
||||
private var storedPrimaryColor = 0
|
||||
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::inflate)
|
||||
|
||||
private companion object {
|
||||
private const val PICK_IMPORT_SOURCE_INTENT = 11
|
||||
private const val PICK_EXPORT_FILE_INTENT = 21
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
isMaterialActivity = true
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -133,6 +142,8 @@ class MainActivity : SimpleActivity() {
|
||||
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
|
||||
R.id.settings -> launchSettings()
|
||||
R.id.about -> launchAbout()
|
||||
R.id.export_alarms -> tryExportAlarms()
|
||||
R.id.import_alarms -> tryImportAlarms()
|
||||
else -> return@setOnMenuItemClickListener false
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
@@ -142,6 +153,8 @@ class MainActivity : SimpleActivity() {
|
||||
private fun refreshMenuItems() {
|
||||
binding.mainToolbar.menu.apply {
|
||||
findItem(R.id.sort).isVisible = binding.viewPager.currentItem == TAB_ALARM
|
||||
findItem(R.id.export_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM
|
||||
findItem(R.id.import_alarms).isVisible = binding.viewPager.currentItem == TAB_ALARM
|
||||
findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)
|
||||
}
|
||||
}
|
||||
@@ -171,8 +184,17 @@ class MainActivity : SimpleActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
if (requestCode == PICK_AUDIO_FILE_INTENT_ID && resultCode == RESULT_OK && resultData != null) {
|
||||
storeNewAlarmSound(resultData)
|
||||
when {
|
||||
requestCode == PICK_AUDIO_FILE_INTENT_ID && resultCode == RESULT_OK && resultData != null -> {
|
||||
storeNewAlarmSound(resultData)
|
||||
}
|
||||
requestCode == PICK_EXPORT_FILE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> {
|
||||
val outputStream = contentResolver.openOutputStream(resultData.data!!)
|
||||
exportAlarmsTo(outputStream)
|
||||
}
|
||||
requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null -> {
|
||||
tryImportAlarmsFromFile(resultData.data!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,4 +320,118 @@ class MainActivity : SimpleActivity() {
|
||||
|
||||
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
|
||||
}
|
||||
|
||||
private fun exportAlarmsTo(outputStream: OutputStream?) {
|
||||
ensureBackgroundThread {
|
||||
val alarms = dbHelper.getAlarms()
|
||||
if (alarms.isEmpty()) {
|
||||
toast(org.fossify.commons.R.string.no_entries_for_exporting)
|
||||
} else {
|
||||
AlarmsExporter.exportAlarms(alarms, outputStream) {
|
||||
toast(
|
||||
when (it) {
|
||||
ExportResult.EXPORT_OK -> org.fossify.commons.R.string.exporting_successful
|
||||
else -> org.fossify.commons.R.string.exporting_failed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryExportAlarms() {
|
||||
if (isQPlus()) {
|
||||
ExportAlarmsDialog(this, config.lastAlarmsExportPath, true) { file ->
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = "application/json"
|
||||
putExtra(Intent.EXTRA_TITLE, file.name)
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
try {
|
||||
startActivityForResult(this, PICK_EXPORT_FILE_INTENT)
|
||||
} 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) {
|
||||
ExportAlarmsDialog(this, config.lastAlarmsExportPath, false) { file ->
|
||||
getFileOutputStream(file.toFileDirItem(this), true) { out ->
|
||||
exportAlarmsTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryImportAlarms() {
|
||||
if (isQPlus()) {
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/json"
|
||||
|
||||
try {
|
||||
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
|
||||
} 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) {
|
||||
pickFileToImportAlarms()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickFileToImportAlarms() {
|
||||
FilePickerDialog(this) {
|
||||
importAlarms(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryImportAlarmsFromFile(uri: Uri) {
|
||||
when (uri.scheme) {
|
||||
"file" -> importAlarms(uri.path!!)
|
||||
"content" -> {
|
||||
val tempFile = getTempFile("alarms", "alarms.json")
|
||||
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)
|
||||
importAlarms(tempFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
}
|
||||
}
|
||||
|
||||
else -> toast(org.fossify.commons.R.string.invalid_file_format)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importAlarms(path: String) {
|
||||
ensureBackgroundThread {
|
||||
val result = AlarmsImporter(this, DBHelper.dbInstance!!).importAlarms(path)
|
||||
toast(
|
||||
when (result) {
|
||||
AlarmsImporter.ImportResult.IMPORT_OK ->
|
||||
org.fossify.commons.R.string.importing_successful
|
||||
AlarmsImporter.ImportResult.IMPORT_FAIL -> org.fossify.commons.R.string.no_items_found
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.fossify.clock.dialogs
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.fossify.commons.activities.BaseSimpleActivity
|
||||
import org.fossify.commons.dialogs.FilePickerDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.clock.R
|
||||
import org.fossify.clock.databinding.DialogExportAlarmsBinding
|
||||
import org.fossify.clock.extensions.config
|
||||
import org.fossify.clock.helpers.ALARMS_EXPORT_EXTENSION
|
||||
import java.io.File
|
||||
|
||||
class ExportAlarmsDialog(
|
||||
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 = DialogExportAlarmsBinding.inflate(activity.layoutInflater, null, false).apply {
|
||||
exportAlarmsFolder.text = activity.humanizePath(realPath)
|
||||
exportAlarmsFilename.setText("${activity.getString(R.string.export_alarms)}_${activity.getCurrentFormattedDateTime()}")
|
||||
|
||||
if (hidePath) {
|
||||
exportAlarmsFolderLabel.beGone()
|
||||
exportAlarmsFolder.beGone()
|
||||
} else {
|
||||
exportAlarmsFolder.setOnClickListener {
|
||||
FilePickerDialog(activity, realPath, false, showFAB = true) {
|
||||
exportAlarmsFolder.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.root, this, R.string.export_alarms) { alertDialog ->
|
||||
alertDialog.showKeyboard(view.exportAlarmsFilename)
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val filename = view.exportAlarmsFilename.value
|
||||
when {
|
||||
filename.isEmpty() -> activity.toast(org.fossify.commons.R.string.empty_name)
|
||||
filename.isAValidFilename() -> {
|
||||
val file = File(realPath, "$filename$ALARMS_EXPORT_EXTENSION")
|
||||
if (!hidePath && file.exists()) {
|
||||
activity.toast(org.fossify.commons.R.string.name_taken)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
ensureBackgroundThread {
|
||||
config.lastAlarmsExportPath = file.absolutePath.getParentPath()
|
||||
callback(file)
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
else -> activity.toast(org.fossify.commons.R.string.invalid_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.fossify.clock.helpers
|
||||
|
||||
import org.fossify.clock.models.Alarm
|
||||
import org.fossify.commons.helpers.ExportResult
|
||||
import java.io.OutputStream
|
||||
|
||||
object AlarmsExporter {
|
||||
fun exportAlarms(
|
||||
alarms: ArrayList<Alarm>,
|
||||
outputStream: OutputStream?,
|
||||
callback: (result: ExportResult) -> Unit,
|
||||
) {
|
||||
if (outputStream == null) {
|
||||
callback.invoke(ExportResult.EXPORT_FAIL)
|
||||
return
|
||||
}
|
||||
|
||||
val alarmsToExport = alarmsToJSON(alarms)
|
||||
|
||||
try {
|
||||
outputStream.bufferedWriter().use { out ->
|
||||
out.write(alarmsToExport)
|
||||
}
|
||||
callback.invoke(ExportResult.EXPORT_OK)
|
||||
} catch (e: Exception) {
|
||||
callback.invoke(ExportResult.EXPORT_FAIL)
|
||||
}
|
||||
}
|
||||
|
||||
private fun alarmsToJSON(alarms: List<Alarm>?): String {
|
||||
if (alarms.isNullOrEmpty()) {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
val jsonAlarms = mutableListOf<String>()
|
||||
for (alarm in alarms) {
|
||||
jsonAlarms.add(alarm.toJSON())
|
||||
}
|
||||
|
||||
return "[${jsonAlarms.joinToString(",")}]"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.fossify.clock.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import org.fossify.clock.models.Alarm
|
||||
import org.fossify.commons.extensions.showErrorToast
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
import java.io.File
|
||||
|
||||
class AlarmsImporter(
|
||||
private val activity: Activity,
|
||||
private val dbHelper: DBHelper,
|
||||
) {
|
||||
enum class ImportResult {
|
||||
IMPORT_FAIL, IMPORT_OK
|
||||
}
|
||||
|
||||
fun importAlarms(path: String): ImportResult {
|
||||
return try {
|
||||
val inputStream = File(path).inputStream()
|
||||
val jsonString = inputStream.bufferedReader().use { it.readText().trimEnd() }
|
||||
val jsonArray = JSONArray(jsonString)
|
||||
|
||||
val insertedCount = insertAlarmsFromJSON(jsonArray)
|
||||
if (insertedCount > 0) {
|
||||
ImportResult.IMPORT_OK
|
||||
} else {
|
||||
ImportResult.IMPORT_FAIL
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
ImportResult.IMPORT_FAIL
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertAlarmsFromJSON(jsonArray: JSONArray): Int {
|
||||
var insertedCount = 0
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
val alarm = parseAlarmFromJSON(jsonObject)
|
||||
val insertedId = dbHelper.insertAlarm(alarm)
|
||||
if (insertedId != -1) {
|
||||
insertedCount++
|
||||
}
|
||||
}
|
||||
return insertedCount
|
||||
}
|
||||
|
||||
private fun parseAlarmFromJSON(jsonObject: JSONObject): Alarm {
|
||||
val id = jsonObject.getInt("id")
|
||||
val timeInMinutes = jsonObject.getInt("timeInMinutes")
|
||||
val days = jsonObject.getInt("days")
|
||||
val isEnabled = jsonObject.getBoolean("isEnabled")
|
||||
val vibrate = jsonObject.getBoolean("vibrate")
|
||||
val soundTitle = jsonObject.getString("soundTitle")
|
||||
val soundUri = jsonObject.getString("soundUri")
|
||||
val label = jsonObject.getString("label")
|
||||
val oneShot = jsonObject.optBoolean("oneShot", false)
|
||||
|
||||
return Alarm(
|
||||
id,
|
||||
timeInMinutes,
|
||||
days,
|
||||
isEnabled,
|
||||
vibrate,
|
||||
soundTitle,
|
||||
soundUri,
|
||||
label,
|
||||
oneShot
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,4 +104,8 @@ class Config(context: Context) : BaseConfig(context) {
|
||||
var wasInitialWidgetSetUp: Boolean
|
||||
get() = prefs.getBoolean(WAS_INITIAL_WIDGET_SET_UP, false)
|
||||
set(wasInitialWidgetSetUp) = prefs.edit().putBoolean(WAS_INITIAL_WIDGET_SET_UP, wasInitialWidgetSetUp).apply()
|
||||
|
||||
var lastAlarmsExportPath: String
|
||||
get() = prefs.getString(LAST_ALARMS_EXPORT_PATH, "")!!
|
||||
set(lastBlockedNumbersExportPath) = prefs.edit().putString(LAST_ALARMS_EXPORT_PATH, lastBlockedNumbersExportPath).apply()
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually"
|
||||
const val ALARMS_SORT_BY = "alarms_sort_by"
|
||||
const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by"
|
||||
const val WAS_INITIAL_WIDGET_SET_UP = "was_initial_widget_set_up"
|
||||
const val ALARMS_EXPORT_EXTENSION = ".json"
|
||||
const val LAST_ALARMS_EXPORT_PATH = "last_alarms_export_path"
|
||||
|
||||
const val TABS_COUNT = 4
|
||||
const val EDITED_TIME_ZONE_SEPARATOR = ":"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.fossify.clock.models
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import org.json.JSONObject
|
||||
|
||||
@Keep
|
||||
data class Alarm(
|
||||
@@ -13,7 +14,22 @@ data class Alarm(
|
||||
var soundUri: String,
|
||||
var label: String,
|
||||
var oneShot: Boolean = false,
|
||||
)
|
||||
) {
|
||||
@Keep
|
||||
fun toJSON(): String {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("id", id)
|
||||
jsonObject.put("timeInMinutes", timeInMinutes)
|
||||
jsonObject.put("days", days)
|
||||
jsonObject.put("isEnabled", isEnabled)
|
||||
jsonObject.put("vibrate", vibrate)
|
||||
jsonObject.put("soundTitle", soundTitle)
|
||||
jsonObject.put("soundUri", soundUri)
|
||||
jsonObject.put("label", label)
|
||||
jsonObject.put("oneShot", oneShot)
|
||||
return jsonObject.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
data class ObfuscatedAlarm(
|
||||
|
||||
51
app/src/main/res/layout/dialog_export_alarms.xml
Normal file
51
app/src/main/res/layout/dialog_export_alarms.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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_alarms_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_alarms_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_alarms_folder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/small_margin"
|
||||
android:paddingStart="@dimen/small_margin"
|
||||
android:paddingEnd="@dimen/small_margin"
|
||||
android:paddingTop="@dimen/small_margin"
|
||||
android:paddingBottom="@dimen/activity_margin" />
|
||||
|
||||
<org.fossify.commons.views.MyTextInputLayout
|
||||
android:id="@+id/export_alarms_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/filename_without_json">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/export_alarms_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>
|
||||
@@ -11,6 +11,14 @@
|
||||
android:icon="@drawable/ic_settings_cog_vector"
|
||||
android:title="@string/settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/export_alarms"
|
||||
android:title="@string/export_alarms"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/import_alarms"
|
||||
android:title="@string/import_alarms"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/about"
|
||||
android:icon="@drawable/ic_info_vector"
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
<string name="add_timer">Add timer</string>
|
||||
<string name="upcoming_alarm">Upcoming alarm</string>
|
||||
<string name="early_alarm_dismissal">Early alarm dismissal</string>
|
||||
|
||||
<string name="import_alarms">Import alarms</string>
|
||||
<string name="export_alarms">Export alarms</string>
|
||||
<!-- Timer -->
|
||||
<string name="timers_notification_msg">Timers are running</string>
|
||||
<string name="timer_single_notification_label_msg">Timer for %s is running</string>
|
||||
|
||||
Reference in New Issue
Block a user