diff --git a/app/build.gradle b/app/build.gradle
index 447e4cc1..eadc01cc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -130,7 +130,7 @@ dependencies {
implementation "com.mikepenz:iconics-core:5.2.8"
implementation "com.mikepenz:iconics-views:5.2.8"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
- implementation "eu.szkolny:szkolny-font:1dab7d64ed"
+ implementation "eu.szkolny:szkolny-font:1.3"
// Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0"
@@ -140,6 +140,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2"
implementation "com.github.bassaer:chatmessageview:2.0.1"
+ implementation "com.github.CanHub:Android-Image-Cropper:2.2.2"
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1"
implementation "com.github.jetradarmobile:android-snowfall:1.2.0"
implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31"
@@ -148,7 +149,6 @@ dependencies {
implementation "com.jaredrummler:colorpicker:1.1.0"
implementation "com.qifan.powerpermission:powerpermission-coroutines:1.3.0"
implementation "com.qifan.powerpermission:powerpermission:1.3.0"
- implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
implementation "com.wdullaer:materialdatetimepicker:4.2.3"
implementation "com.yuyh.json:jsonviewer:1.0.6"
implementation "io.coil-kt:coil:1.1.1"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6e8fc491..15919246 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -143,8 +143,7 @@
-
-
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
index d7855364..af15b543 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
@@ -1,12 +1,9 @@
package pl.szczodrzynski.edziennik
-import android.Manifest
-import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
import android.database.Cursor
@@ -29,7 +26,6 @@ import android.view.View
import android.view.WindowManager
import android.widget.*
import androidx.annotation.*
-import androidx.core.app.ActivityCompat
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
@@ -295,19 +291,6 @@ fun colorFromCssName(name: String): Int {
fun List.filterOutArchived() = this.filter { !it.archived }
-fun Activity.isStoragePermissionGranted(): Boolean {
- return if (Build.VERSION.SDK_INT >= 23) {
- if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
- true
- } else {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
- false
- }
- } else {
- true
- }
-}
-
fun Response?.getUnixDate(): Long {
val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix()
val pattern = "EEE, dd MMM yyyy HH:mm:ss Z"
@@ -1235,3 +1218,41 @@ operator fun Iterable>.get(key: K): V? {
}
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
+
+fun MutableList.after(what: E, insert: E) {
+ val index = indexOf(what)
+ if (index != -1)
+ add(index + 1, insert)
+}
+
+fun MutableList.before(what: E, insert: E) {
+ val index = indexOf(what)
+ if (index != -1)
+ add(index, insert)
+}
+
+fun MutableList.after(what: E, insert: Collection) {
+ val index = indexOf(what)
+ if (index != -1)
+ addAll(index + 1, insert)
+}
+
+fun MutableList.before(what: E, insert: Collection) {
+ val index = indexOf(what)
+ if (index != -1)
+ addAll(index, insert)
+}
+
+fun Context.getSyncInterval(interval: Int): String {
+ val hours = interval / 60 / 60
+ val minutes = interval / 60 % 60
+ val hoursText = if (hours > 0)
+ plural(R.plurals.time_till_hours, hours)
+ else
+ null
+ val minutesText = if (minutes > 0)
+ plural(R.plurals.time_till_minutes, minutes)
+ else
+ ""
+ return hoursText?.plus(" $minutesText") ?: minutesText
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
index cdb9778f..951a2cd7 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
@@ -10,13 +10,11 @@ import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle
-import android.os.Environment
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
@@ -56,7 +54,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
+import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment
@@ -79,7 +77,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
-import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
+import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
@@ -97,7 +95,6 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
-import java.io.File
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
@@ -110,8 +107,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
const val TAG = "MainActivity"
- const val REQUEST_LOGIN_ACTIVITY = 20222
-
const val DRAWER_PROFILE_ADD_NEW = 200
const val DRAWER_PROFILE_SYNC_ALL = 201
const val DRAWER_PROFILE_EXPORT_DATA = 202
@@ -199,7 +194,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.isStatic(true)
.isBelowSeparator(true)
- list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsNewFragment::class)
+ list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
.isInDrawer(true)
.isStatic(true)
@@ -257,6 +252,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
+ val requestHandler by lazy { MainActivityRequestHandler(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@@ -395,7 +391,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
drawerProfileLongClickListener = { _, profile, _, view ->
if (view != null && profile is ProfileDrawerItem) {
- showProfileContextMenu(profile, view)
+ launch {
+ val appProfile = withContext(Dispatchers.IO) {
+ App.db.profileDao().getByIdNow(profile.identifier.toInt())
+ } ?: return@launch
+ drawer.close()
+ ProfileConfigDialog(this@MainActivity, appProfile)
+ }
true
}
else {
@@ -475,28 +477,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
// APP BACKGROUND
- if (app.config.ui.appBackground != null) {
- try {
- app.config.ui.appBackground?.let {
- var bg = it
- val bgDir = File(Environment.getExternalStoragePublicDirectory("Szkolny.eu"), "bg")
- if (bgDir.exists()) {
- val files = bgDir.listFiles()
- val r = Random()
- val i = r.nextInt(files.size)
- bg = files[i].toString()
- }
- val linearLayout = b.root
- if (bg.endsWith(".gif")) {
- linearLayout.background = GifDrawable(bg)
- } else {
- linearLayout.background = BitmapDrawable.createFromPath(bg)
- }
- }
- } catch (e: IOException) {
- e.printStackTrace()
- }
- }
+ setAppBackground()
// IT'S WINTER MY DUDES
val today = Date.getToday()
@@ -578,7 +559,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
- .withIcon(CommunityMaterial.Icon.cmd_android_studio)
+ .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) })
}
}
@@ -586,7 +567,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
private var profileSettingClickListener = { id: Int, view: View? ->
when (id) {
DRAWER_PROFILE_ADD_NEW -> {
- startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_LOGIN_ACTIVITY)
+ requestHandler.requestLogin()
}
DRAWER_PROFILE_SYNC_ALL -> {
EdziennikTask.sync().enqueue(this)
@@ -834,7 +815,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
handleIntent(intent?.extras)
}
}
- private fun handleIntent(extras: Bundle?) {
+ fun handleIntent(extras: Bundle?) {
d(TAG, "handleIntent() {")
extras?.keySet()?.forEach { key ->
@@ -992,13 +973,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == REQUEST_LOGIN_ACTIVITY) {
- if (!app.config.loginFinished)
- finish()
- else {
- handleIntent(data?.extras)
- }
- }
+ requestHandler.handleResult(requestCode, resultCode, data)
}
/* _ _ _ _ _
@@ -1224,6 +1199,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}, 3000)
}
+ fun setAppBackground() {
+ try {
+ b.root.background = app.config.ui.appBackground?.let {
+ if (it.endsWith(".gif"))
+ GifDrawable(it)
+ else
+ BitmapDrawable.createFromPath(it)
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+
/* _____ _ _
| __ \ (_) |
| | | |_ __ __ ___ _____ _ __ _| |_ ___ _ __ ___ ___
@@ -1299,26 +1287,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.addProfileSettings(*drawerProfiles.toTypedArray())
}
- private fun showProfileContextMenu(profile: IProfile, view: View) {
- val profileId = profile.identifier.toInt()
- val popupMenu = PopupMenu(this, view)
- popupMenu.menu.add(0, 1, 1, R.string.profile_menu_open_settings)
- popupMenu.menu.add(0, 2, 2, R.string.profile_menu_remove)
- popupMenu.setOnMenuItemClickListener { item ->
- if (item.itemId == 1) {
- if (profileId != app.profile.id) {
- loadProfile(profileId, DRAWER_ITEM_SETTINGS)
- return@setOnMenuItemClickListener true
- }
- loadTarget(DRAWER_ITEM_SETTINGS, null)
- } else if (item.itemId == 2) {
- ProfileRemoveDialog(this, profileId, profile.name?.getText(this) ?: "?")
- }
- true
- }
- popupMenu.show()
- }
-
private val targetPopToHomeList = arrayListOf()
private var targetHomeId: Int = -1
override fun onBackPressed() {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt
new file mode 100644
index 00000000..a39a5872
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-3-23.
+ */
+
+package pl.szczodrzynski.edziennik
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.provider.OpenableColumns
+import com.canhub.cropper.CropImage
+import com.canhub.cropper.CropImageView
+import pl.szczodrzynski.edziennik.data.db.entity.Profile
+import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
+import java.io.File
+import java.io.FileOutputStream
+
+class MainActivityRequestHandler(val activity: MainActivity) {
+ companion object {
+ private const val REQUEST_LOGIN_ACTIVITY = 2000
+ private const val REQUEST_FILE_HEADER_BACKGROUND = 3000
+ private const val REQUEST_FILE_APP_BACKGROUND = 4000
+ private const val REQUEST_FILE_PROFILE_IMAGE = 5000
+ private const val REQUEST_CROP_HEADER_BACKGROUND = 3100
+ private const val REQUEST_CROP_APP_BACKGROUND = 4100
+ private const val REQUEST_CROP_PROFILE_IMAGE = 5100
+ }
+
+ private val app = activity.app
+ private val requestData = mutableMapOf()
+ private val listeners = mutableMapOf Unit>()
+
+ private val manager
+ get() = app.permissionManager
+
+ fun requestLogin() = activity.startActivityForResult(
+ Intent(activity, LoginActivity::class.java),
+ REQUEST_LOGIN_ACTIVITY
+ )
+
+ fun requestHeaderBackground(listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_HEADER_BACKGROUND] = listener
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_HEADER_BACKGROUND
+ )
+ }
+
+ fun requestAppBackground(listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_APP_BACKGROUND] = listener
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_APP_BACKGROUND
+ )
+ }
+
+ fun requestProfileImage(profile: Profile, listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_PROFILE_IMAGE] = listener
+ requestData[REQUEST_FILE_PROFILE_IMAGE] = profile
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_PROFILE_IMAGE
+ )
+ }
+
+ private fun getFileInfo(uri: Uri): Pair {
+ if (uri.scheme == "file") {
+ return (uri.lastPathSegment ?: "unknown") to null
+ }
+ val cursor = activity.contentResolver.query(
+ uri,
+ null,
+ null,
+ null,
+ null,
+ null
+ )
+
+ return cursor?.use {
+ if (it.moveToFirst()) {
+ val name = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+ val mimeIndex = it.getColumnIndex("mime_type")
+ val mimeType = if (mimeIndex != -1) it.getString(mimeIndex) else null
+
+ name to mimeType
+ } else
+ null
+ } ?: "unknown" to null
+ }
+
+ private fun shouldCrop(uri: Uri): Boolean {
+ val (filename, mimeType) = getFileInfo(uri)
+ return !filename.endsWith(".gif") && mimeType?.endsWith("/gif") != true
+ }
+
+ private fun saveFile(uri: Uri, name: String): String {
+ val (filename, _) = getFileInfo(uri)
+ val extension = filename.substringAfterLast('.')
+ val file = File(activity.filesDir, "$name.$extension")
+ activity.contentResolver.openInputStream(uri)?.use { input ->
+ FileOutputStream(file).use { output ->
+ input.copyTo(output)
+ }
+ }
+ return file.absolutePath
+ }
+
+ fun handleResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode != Activity.RESULT_OK)
+ return
+ var uri = CropImage.getPickImageResultUri(activity, data)
+ when (requestCode) {
+ REQUEST_LOGIN_ACTIVITY -> {
+ if (!app.config.loginFinished)
+ activity.finish()
+ else {
+ activity.handleIntent(data?.extras)
+ }
+ }
+ REQUEST_FILE_HEADER_BACKGROUND -> {
+ if (uri == null)
+ return // TODO: 2021-03-24 if the app returns no data
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setAspectRatio(512, 288)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .setRequestedSize(512, 288)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_HEADER_BACKGROUND)
+ } else {
+ val path = saveFile(uri, "header")
+ app.config.ui.headerBackground = path
+ listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
+ }
+ }
+ REQUEST_FILE_APP_BACKGROUND -> {
+ if (uri == null)
+ return
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_APP_BACKGROUND)
+ } else {
+ val path = saveFile(uri, "background")
+ app.config.ui.appBackground = path
+ listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
+ }
+ }
+ REQUEST_FILE_PROFILE_IMAGE -> {
+ if (uri == null)
+ return
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setAspectRatio(1, 1)
+ .setCropShape(CropImageView.CropShape.OVAL)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .setRequestedSize(512, 512)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_PROFILE_IMAGE)
+ } else {
+ val profile =
+ requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
+ val path = saveFile(uri, "profile${profile.id}")
+ profile.image = path
+ listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
+ }
+ }
+ REQUEST_CROP_HEADER_BACKGROUND -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val path = saveFile(uri, "header")
+ app.config.ui.headerBackground = path
+ listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
+ }
+ REQUEST_CROP_APP_BACKGROUND -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val path = saveFile(uri, "background")
+ app.config.ui.appBackground = path
+ listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
+ }
+ REQUEST_CROP_PROFILE_IMAGE -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val profile = requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
+ val path = saveFile(uri, "profile${profile.id}")
+ profile.image = path
+ listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt
new file mode 100644
index 00000000..13dcb6f9
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-3-20.
+ */
+
+package pl.szczodrzynski.edziennik.ui.dialogs.bell
+
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.widget.addTextChangedListener
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.utils.models.Time
+import kotlin.coroutines.CoroutineContext
+
+class BellSyncConfigDialog(
+ val activity: AppCompatActivity,
+ val onChangeListener: (() -> Unit)? = null,
+ val onShowListener: ((tag: String) -> Unit)? = null,
+ val onDismissListener: ((tag: String) -> Unit)? = null
+) : CoroutineScope {
+ companion object {
+ private const val TAG = "BellSyncConfigDialog"
+ }
+
+ private lateinit var app: App
+ private lateinit var dialog: AlertDialog
+
+ private val job = Job()
+ override val coroutineContext: CoroutineContext
+ get() = job + Dispatchers.Main
+
+ // local variables go here
+
+ private fun parse(input: String): Pair