mirror of
https://github.com/home-assistant/android
synced 2024-10-07 00:29:32 +00:00
Jetpack Compose icon dialog (#3631)
* Add icon dialog based on Jetpack Compose TODO * Migrate to new icon dialog * Migrate old database * Don't wrap with drawablecompat * Rebase fixes and updates - Fix and update database migration - Fix dependencies - Fix shortcut icons - Fix ComposeView in AlertDialog not working by switching implementation to DialogFragment - Fix icons that no longer exist - ktlint * Visual compatibility - Automotive asset - Handle icon ids in shortcuts to prevent users losing icons when updating shortcuts - Add padding, color filter to shortcut icons to keep icons consistent with older icons - Increase button widget icon padding to keep sizing consistent - Add tip to dialog about searching in non-English languages * Fix line endings --------- Co-authored-by: Tiger Oakes <contact@tigeroakes.com>
This commit is contained in:
parent
938c515549
commit
d1b17aa606
|
@ -143,8 +143,6 @@ dependencies {
|
|||
|
||||
implementation("com.github.Dimezis:BlurView:version-1.6.6")
|
||||
implementation("org.altbeacon:android-beacon-library:2.19.5")
|
||||
implementation("com.maltaisn:icondialog:3.3.0")
|
||||
implementation("com.maltaisn:iconpack-community-material:5.3.45")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.22")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
|
||||
|
|
1
app/src/main/assets/mdi_id_map.json
Normal file
1
app/src/main/assets/mdi_id_map.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -13,12 +13,8 @@ import android.util.Log
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -35,6 +31,7 @@ import io.homeassistant.companion.android.database.qs.isSetup
|
|||
import io.homeassistant.companion.android.database.qs.numberedId
|
||||
import io.homeassistant.companion.android.settings.SettingsActivity
|
||||
import io.homeassistant.companion.android.settings.qs.updateActiveTileServices
|
||||
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -113,7 +110,7 @@ abstract class TileExtensions : TileService() {
|
|||
serverManager.integrationRepository(tileData.serverId).getEntityUpdates(listOf(tileData.entityId))?.collect {
|
||||
tile.state =
|
||||
if (it.state in validActiveStates) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
getTileIcon(tileData.iconId, it, applicationContext)?.let { icon ->
|
||||
getTileIcon(tileData.iconName, it, applicationContext)?.let { icon ->
|
||||
tile.icon = Icon.createWithBitmap(icon)
|
||||
}
|
||||
tile.updateTile()
|
||||
|
@ -147,7 +144,7 @@ abstract class TileExtensions : TileService() {
|
|||
val state: Entity<*>? =
|
||||
if (
|
||||
tileData.entityId.split(".")[0] in toggleDomainsWithLock ||
|
||||
tileData.iconId == null
|
||||
tileData.iconName == null
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
@ -170,7 +167,7 @@ abstract class TileExtensions : TileService() {
|
|||
tile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
|
||||
getTileIcon(tileData.iconId, state, context)?.let { icon ->
|
||||
getTileIcon(tileData.iconName, state, context)?.let { icon ->
|
||||
tile.icon = Icon.createWithBitmap(icon)
|
||||
}
|
||||
Log.d(TAG, "Tile data set for tile ID: $tileId")
|
||||
|
@ -308,7 +305,7 @@ abstract class TileExtensions : TileService() {
|
|||
tileId = tileId,
|
||||
added = true,
|
||||
serverId = 0,
|
||||
iconId = null,
|
||||
iconName = null,
|
||||
entityId = "",
|
||||
label = "",
|
||||
subtitle = null,
|
||||
|
@ -324,26 +321,17 @@ abstract class TileExtensions : TileService() {
|
|||
updateActiveTileServices(highestInUse, applicationContext)
|
||||
}
|
||||
|
||||
private fun getTileIcon(tileIconId: Int?, entity: Entity<*>?, context: Context): Bitmap? {
|
||||
private fun getTileIcon(tileIconName: String?, entity: Entity<*>?, context: Context): Bitmap? {
|
||||
// Create an icon pack and load all drawables.
|
||||
if (tileIconId != null) {
|
||||
if (iconPack == null) {
|
||||
val loader = IconPackLoader(context)
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack!!.loadDrawables(loader.drawableLoader)
|
||||
}
|
||||
|
||||
val iconDrawable = iconPack?.icons?.get(tileIconId)?.drawable
|
||||
if (iconDrawable != null) {
|
||||
return DrawableCompat.wrap(iconDrawable).toBitmap()
|
||||
}
|
||||
if (!tileIconName.isNullOrBlank()) {
|
||||
val icon = CommunityMaterial.getIconByMdiName(tileIconName) ?: return null
|
||||
val iconDrawable = IconicsDrawable(context, icon)
|
||||
return iconDrawable.toBitmap()
|
||||
} else {
|
||||
entity?.getIcon(context)?.let {
|
||||
return DrawableCompat.wrap(
|
||||
IconicsDrawable(context, it).apply {
|
||||
sizeDp = 48
|
||||
}
|
||||
).toBitmap()
|
||||
return IconicsDrawable(context, it).apply {
|
||||
sizeDp = 48
|
||||
}.toBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,7 +340,6 @@ abstract class TileExtensions : TileService() {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "TileExtensions"
|
||||
private var iconPack: IconPack? = null
|
||||
private val toggleDomains = listOf(
|
||||
"automation", "cover", "fan", "humidifier", "input_boolean", "light",
|
||||
"media_player", "remote", "siren", "switch"
|
||||
|
|
|
@ -8,20 +8,23 @@ import android.view.LayoutInflater
|
|||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.maltaisn.icondialog.IconDialog
|
||||
import com.maltaisn.icondialog.IconDialogSettings
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.settings.qs.views.ManageTilesView
|
||||
import io.homeassistant.companion.android.util.icondialog.IconDialog
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ManageTilesFragment : Fragment(), IconDialog.Callback {
|
||||
class ManageTilesFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TileFragment"
|
||||
|
@ -53,17 +56,24 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
val settings = IconDialogSettings {
|
||||
searchVisibility = IconDialog.SearchVisibility.ALWAYS
|
||||
}
|
||||
val iconDialog = IconDialog.newInstance(settings)
|
||||
|
||||
setContent {
|
||||
MdcTheme {
|
||||
var showingDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showingDialog) {
|
||||
IconDialog(
|
||||
onSelect = {
|
||||
onIconDialogIconsSelected(it)
|
||||
showingDialog = false
|
||||
},
|
||||
onDismissRequest = { showingDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
ManageTilesView(
|
||||
viewModel = viewModel,
|
||||
onShowIconDialog = { tag ->
|
||||
iconDialog.show(childFragmentManager, tag)
|
||||
onShowIconDialog = {
|
||||
showingDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -76,14 +86,8 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback {
|
|||
activity?.title = getString(commonR.string.tiles)
|
||||
}
|
||||
|
||||
override val iconDialogIconPack: IconPack
|
||||
get() = viewModel.iconPack
|
||||
|
||||
override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List<com.maltaisn.icondialog.data.Icon>) {
|
||||
Log.d(TAG, "Selected icon: ${icons.firstOrNull()}")
|
||||
val selectedIcon = icons.firstOrNull()
|
||||
if (selectedIcon != null) {
|
||||
viewModel.selectIcon(selectedIcon)
|
||||
}
|
||||
private fun onIconDialogIconsSelected(selectedIcon: IIcon) {
|
||||
Log.d(TAG, "Selected icon: ${selectedIcon.name}")
|
||||
viewModel.selectIcon(selectedIcon)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,24 +4,19 @@ import android.annotation.SuppressLint
|
|||
import android.app.Application
|
||||
import android.app.StatusBarManager
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.maltaisn.icondialog.data.Icon
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
|
@ -72,6 +67,8 @@ import io.homeassistant.companion.android.qs.Tile6Service
|
|||
import io.homeassistant.companion.android.qs.Tile7Service
|
||||
import io.homeassistant.companion.android.qs.Tile8Service
|
||||
import io.homeassistant.companion.android.qs.Tile9Service
|
||||
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
|
||||
import io.homeassistant.companion.android.util.icondialog.mdiName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
|
@ -139,8 +136,6 @@ class ManageTilesViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
lateinit var iconPack: IconPack
|
||||
|
||||
private val app = application
|
||||
|
||||
val slots = loadTileSlots(application.resources)
|
||||
|
@ -154,9 +149,7 @@ class ManageTilesViewModel @Inject constructor(
|
|||
private set
|
||||
var selectedServerId by mutableStateOf(ServerManager.SERVER_ID_ACTIVE)
|
||||
private set
|
||||
var selectedIconId by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
var selectedIconDrawable by mutableStateOf(AppCompatResources.getDrawable(application, commonR.drawable.ic_stat_ic_notification))
|
||||
var selectedIconId by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var selectedEntityId by mutableStateOf("")
|
||||
var tileLabel by mutableStateOf("")
|
||||
|
@ -165,6 +158,8 @@ class ManageTilesViewModel @Inject constructor(
|
|||
private set
|
||||
var selectedShouldVibrate by mutableStateOf(false)
|
||||
var tileAuthRequired by mutableStateOf(false)
|
||||
|
||||
var selectedIcon: IIcon? = null
|
||||
private var selectedTileId = 0
|
||||
private var selectedTileAdded = false
|
||||
|
||||
|
@ -202,16 +197,6 @@ class ManageTilesViewModel @Inject constructor(
|
|||
selectTile(slots.indexOf(selectedTile))
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val loader = IconPackLoader(getApplication())
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack.loadDrawables(loader.drawableLoader)
|
||||
withContext(Dispatchers.Main) {
|
||||
// The icon pack might not have been initialized when the tile data was loaded
|
||||
selectTile(slots.indexOf(selectedTile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTile(index: Int) {
|
||||
|
@ -259,21 +244,9 @@ class ManageTilesViewModel @Inject constructor(
|
|||
if (selectedIconId == null) selectIcon(null) // trigger drawable update
|
||||
}
|
||||
|
||||
fun selectIcon(icon: Icon?) {
|
||||
selectedIconId = icon?.id
|
||||
selectedIconDrawable = if (icon != null) {
|
||||
icon.drawable?.let { DrawableCompat.wrap(it) }
|
||||
} else {
|
||||
sortedEntities.firstOrNull { it.entityId == selectedEntityId }?.let {
|
||||
it.getIcon(app)?.let { iIcon ->
|
||||
DrawableCompat.wrap(
|
||||
IconicsDrawable(app, iIcon).apply {
|
||||
sizeDp = 20
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun selectIcon(icon: IIcon?) {
|
||||
selectedIconId = icon?.mdiName
|
||||
selectedIcon = icon ?: sortedEntities.firstOrNull { it.entityId == selectedEntityId }?.getIcon(app)
|
||||
}
|
||||
|
||||
private fun updateExistingTileFields(currentTile: TileEntity) {
|
||||
|
@ -283,13 +256,7 @@ class ManageTilesViewModel @Inject constructor(
|
|||
selectedShouldVibrate = currentTile.shouldVibrate
|
||||
tileAuthRequired = currentTile.authRequired
|
||||
selectIcon(
|
||||
currentTile.iconId?.let {
|
||||
if (::iconPack.isInitialized) {
|
||||
iconPack.getIcon(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
currentTile.iconName?.let { CommunityMaterial.getIconByMdiName(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -300,7 +267,7 @@ class ManageTilesViewModel @Inject constructor(
|
|||
tileId = selectedTile.id,
|
||||
serverId = selectedServerId,
|
||||
added = selectedTileAdded,
|
||||
iconId = selectedIconId,
|
||||
iconName = selectedIconId,
|
||||
entityId = selectedEntityId,
|
||||
label = tileLabel,
|
||||
subtitle = tileSubtitle,
|
||||
|
@ -315,11 +282,10 @@ class ManageTilesViewModel @Inject constructor(
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !selectedTileAdded) {
|
||||
val statusBarManager = app.getSystemService<StatusBarManager>()
|
||||
val service = idToTileService[selectedTile.id] ?: Tile1Service::class.java
|
||||
val icon = selectedIconDrawable?.let {
|
||||
it.toBitmapOrNull(it.intrinsicWidth, it.intrinsicHeight)?.let { bitmap ->
|
||||
android.graphics.drawable.Icon.createWithBitmap(bitmap)
|
||||
}
|
||||
} ?: android.graphics.drawable.Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification)
|
||||
val icon = selectedIcon?.let {
|
||||
val bitmap = IconicsDrawable(getApplication(), it).toBitmap()
|
||||
Icon.createWithBitmap(bitmap)
|
||||
} ?: Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification)
|
||||
|
||||
statusBarManager?.requestAddTileService(
|
||||
ComponentName(app, service),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.homeassistant.companion.android.settings.qs.views
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -31,13 +30,11 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.settings.qs.ManageTilesViewModel
|
||||
import io.homeassistant.companion.android.util.compose.ServerDropdownButton
|
||||
|
@ -160,12 +157,9 @@ fun ManageTilesView(
|
|||
OutlinedButton(
|
||||
onClick = { onShowIconDialog(viewModel.selectedTile.id) }
|
||||
) {
|
||||
val iconBitmap = remember(viewModel.selectedIconDrawable) {
|
||||
viewModel.selectedIconDrawable?.toBitmap()?.asImageBitmap()
|
||||
}
|
||||
iconBitmap?.let {
|
||||
Image(
|
||||
iconBitmap,
|
||||
viewModel.selectedIcon?.let { icon ->
|
||||
com.mikepenz.iconics.compose.Image(
|
||||
icon,
|
||||
contentDescription = stringResource(id = R.string.tile_icon),
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)),
|
||||
modifier = Modifier.size(20.dp)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.homeassistant.companion.android.settings.shortcuts
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -11,28 +10,24 @@ import android.view.Menu
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.maltaisn.icondialog.IconDialog
|
||||
import com.maltaisn.icondialog.IconDialogSettings
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.settings.shortcuts.views.ManageShortcutsView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.homeassistant.companion.android.util.icondialog.IconDialog
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@AndroidEntryPoint
|
||||
class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
|
||||
class ManageShortcutsSettingsFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
const val MAX_SHORTCUTS = 5
|
||||
|
@ -41,19 +36,10 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
|
|||
}
|
||||
|
||||
val viewModel: ManageShortcutsViewModel by viewModels()
|
||||
private lateinit var iconPack: IconPack
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val loader = IconPackLoader(requireContext())
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack.loadDrawables(loader.drawableLoader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
|
@ -71,14 +57,20 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
val settings = IconDialogSettings {
|
||||
searchVisibility = IconDialog.SearchVisibility.ALWAYS
|
||||
}
|
||||
val iconDialog = IconDialog.newInstance(settings)
|
||||
|
||||
setContent {
|
||||
MdcTheme {
|
||||
ManageShortcutsView(viewModel = viewModel, iconDialog = iconDialog, childFragment = childFragmentManager)
|
||||
var showingTag by remember { mutableStateOf<String?>(null) }
|
||||
showingTag?.let { tag ->
|
||||
IconDialog(
|
||||
onSelect = {
|
||||
onIconDialogIconsSelected(tag, it)
|
||||
showingTag = null
|
||||
},
|
||||
onDismissRequest = { showingTag = null }
|
||||
)
|
||||
}
|
||||
|
||||
ManageShortcutsView(viewModel = viewModel, showIconDialog = { showingTag = it })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,44 +84,17 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
|
|||
activity?.title = getString(commonR.string.shortcuts)
|
||||
}
|
||||
|
||||
override val iconDialogIconPack: IconPack
|
||||
get() = iconPack
|
||||
private fun onIconDialogIconsSelected(tag: String, selectedIcon: IIcon) {
|
||||
Log.d(TAG, "Selected icon: $selectedIcon")
|
||||
|
||||
override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List<com.maltaisn.icondialog.data.Icon>) {
|
||||
Log.d(TAG, "Selected icon: ${icons.firstOrNull()}")
|
||||
val selectedIcon = icons.firstOrNull()
|
||||
if (selectedIcon != null) {
|
||||
val iconDrawable = selectedIcon.drawable
|
||||
if (iconDrawable != null) {
|
||||
val icon = DrawableCompat.wrap(iconDrawable)
|
||||
icon.setColorFilter(resources.getColor(commonR.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
||||
when (dialog.tag) {
|
||||
"shortcut_1" -> {
|
||||
viewModel.shortcuts[0].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[0].drawable.value = icon
|
||||
}
|
||||
"shortcut_2" -> {
|
||||
viewModel.shortcuts[1].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[1].drawable.value = icon
|
||||
}
|
||||
"shortcut_3" -> {
|
||||
viewModel.shortcuts[2].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[2].drawable.value = icon
|
||||
}
|
||||
"shortcut_4" -> {
|
||||
viewModel.shortcuts[3].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[3].drawable.value = icon
|
||||
}
|
||||
"shortcut_5" -> {
|
||||
viewModel.shortcuts[4].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[4].drawable.value = icon
|
||||
}
|
||||
else -> {
|
||||
viewModel.shortcuts[5].selectedIcon.value = selectedIcon.id
|
||||
viewModel.shortcuts[5].drawable.value = icon
|
||||
}
|
||||
}
|
||||
}
|
||||
val index = when (tag) {
|
||||
"shortcut_1" -> 0
|
||||
"shortcut_2" -> 1
|
||||
"shortcut_3" -> 2
|
||||
"shortcut_4" -> 3
|
||||
"shortcut_5" -> 4
|
||||
else -> 5
|
||||
}
|
||||
viewModel.shortcuts[index].selectedIcon.value = selectedIcon
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,35 +4,41 @@ import android.app.Application
|
|||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.IconicsSize
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.padding
|
||||
import com.mikepenz.iconics.utils.size
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import io.homeassistant.companion.android.database.IconDialogCompat
|
||||
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
|
||||
import io.homeassistant.companion.android.util.icondialog.mdiName
|
||||
import io.homeassistant.companion.android.webview.WebViewActivity
|
||||
import io.homeassistant.companion.android.widgets.assist.AssistShortcutActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
|
@ -44,14 +50,13 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
|
||||
val app = application
|
||||
private val TAG = "ShortcutViewModel"
|
||||
private lateinit var iconPack: IconPack
|
||||
private var shortcutManager = application.applicationContext.getSystemService<ShortcutManager>()!!
|
||||
val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported
|
||||
var pinnedShortcuts = shortcutManager.pinnedShortcuts
|
||||
.filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) }
|
||||
.toMutableList()
|
||||
private set
|
||||
var dynamicShortcuts: MutableList<ShortcutInfo> = shortcutManager.dynamicShortcuts
|
||||
var dynamicShortcuts = mutableListOf<ShortcutInfo>()
|
||||
private set
|
||||
|
||||
var servers by mutableStateOf(serverManager.defaultServers)
|
||||
|
@ -61,15 +66,16 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
|
||||
private val currentServerId = serverManager.getServer()?.id ?: 0
|
||||
|
||||
private val iconIdToName: Map<Int, String> by lazy { IconDialogCompat(app.assets).loadAllIcons() }
|
||||
|
||||
data class Shortcut(
|
||||
var id: MutableState<String?>,
|
||||
var serverId: MutableState<Int>,
|
||||
var selectedIcon: MutableState<Int>,
|
||||
var selectedIcon: MutableState<IIcon?>,
|
||||
var label: MutableState<String>,
|
||||
var desc: MutableState<String>,
|
||||
var path: MutableState<String>,
|
||||
var type: MutableState<String>,
|
||||
var drawable: MutableState<Drawable?>,
|
||||
var delete: MutableState<Boolean>
|
||||
)
|
||||
|
||||
|
@ -90,6 +96,7 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
updateDynamicShortcuts()
|
||||
Log.d(TAG, "We have ${dynamicShortcuts.size} dynamic shortcuts")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -102,12 +109,11 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
Shortcut(
|
||||
mutableStateOf(""),
|
||||
mutableStateOf(currentServerId),
|
||||
mutableStateOf(0),
|
||||
mutableStateOf(null),
|
||||
mutableStateOf(""),
|
||||
mutableStateOf(""),
|
||||
mutableStateOf(""),
|
||||
mutableStateOf("lovelace"),
|
||||
mutableStateOf(AppCompatResources.getDrawable(application, R.drawable.ic_stat_ic_notification_blue)),
|
||||
mutableStateOf(false)
|
||||
)
|
||||
)
|
||||
|
@ -119,26 +125,31 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun createShortcut(shortcutId: String, serverId: Int, shortcutLabel: String, shortcutDesc: String, shortcutPath: String, bitmap: Bitmap? = null, iconId: Int) {
|
||||
fun createShortcut(shortcutId: String, serverId: Int, shortcutLabel: String, shortcutDesc: String, shortcutPath: String, icon: IIcon?) {
|
||||
Log.d(TAG, "Attempt to add shortcut $shortcutId")
|
||||
val intent = Intent(
|
||||
WebViewActivity.newInstance(getApplication(), shortcutPath, serverId).addFlags(
|
||||
WebViewActivity.newInstance(app, shortcutPath, serverId).addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
)
|
||||
intent.action = shortcutPath
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
intent.putExtra("iconId", iconId)
|
||||
icon?.let { intent.putExtra("iconName", icon.mdiName) }
|
||||
|
||||
val shortcut = ShortcutInfo.Builder(getApplication(), shortcutId)
|
||||
val shortcut = ShortcutInfo.Builder(app, shortcutId)
|
||||
.setShortLabel(shortcutLabel)
|
||||
.setLongLabel(shortcutDesc)
|
||||
.setIcon(
|
||||
if (bitmap != null) {
|
||||
if (icon != null) {
|
||||
val bitmap = IconicsDrawable(app, icon).apply {
|
||||
size = IconicsSize.dp(48)
|
||||
padding = IconicsSize.dp(2)
|
||||
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(app, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
||||
}.toBitmap()
|
||||
Icon.createWithBitmap(bitmap)
|
||||
} else {
|
||||
Icon.createWithResource(getApplication(), R.drawable.ic_stat_ic_notification_blue)
|
||||
Icon.createWithResource(app, R.drawable.ic_stat_ic_notification_blue)
|
||||
}
|
||||
)
|
||||
.setIntent(intent)
|
||||
|
@ -146,7 +157,7 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
|
||||
if (shortcutId.startsWith("shortcut")) {
|
||||
shortcutManager.addDynamicShortcuts(listOf(shortcut))
|
||||
dynamicShortcuts = shortcutManager.dynamicShortcuts
|
||||
updateDynamicShortcuts()
|
||||
} else {
|
||||
var isNewPinned = true
|
||||
for (item in pinnedShortcuts) {
|
||||
|
@ -154,7 +165,7 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
isNewPinned = false
|
||||
Log.d(TAG, "Updating pinned shortcut: $shortcutId")
|
||||
shortcutManager.updateShortcuts(listOf(shortcut))
|
||||
Toast.makeText(getApplication(), R.string.shortcut_updated, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(app, R.string.shortcut_updated, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,64 +180,54 @@ class ManageShortcutsViewModel @Inject constructor(
|
|||
|
||||
fun deleteShortcut(shortcutId: String) {
|
||||
shortcutManager.removeDynamicShortcuts(listOf(shortcutId))
|
||||
dynamicShortcuts = shortcutManager.dynamicShortcuts
|
||||
updateDynamicShortcuts()
|
||||
}
|
||||
|
||||
fun setPinnedShortcutData(shortcutId: String) {
|
||||
fun setPinnedShortcutData(shortcutId: String) = viewModelScope.launch {
|
||||
for (item in pinnedShortcuts) {
|
||||
if (item.id == shortcutId) {
|
||||
shortcuts.last().id.value = item.id
|
||||
shortcuts.last().serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId
|
||||
shortcuts.last().label.value = item.shortLabel.toString()
|
||||
shortcuts.last().desc.value = item.longLabel.toString()
|
||||
shortcuts.last().path.value = item.intent?.action.toString()
|
||||
shortcuts.last().selectedIcon.value = item.intent?.extras?.getInt("iconId").toString().toIntOrNull() ?: 0
|
||||
if (shortcuts.last().selectedIcon.value != 0) {
|
||||
shortcuts.last().drawable.value = getTileIcon(shortcuts.last().selectedIcon.value)
|
||||
}
|
||||
if (shortcuts.last().path.value.startsWith("entityId:")) {
|
||||
shortcuts.last().type.value = "entityId"
|
||||
} else {
|
||||
shortcuts.last().type.value = "lovelace"
|
||||
}
|
||||
shortcuts.last().setData(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setDynamicShortcutData(shortcutId: String, index: Int) {
|
||||
private fun updateDynamicShortcuts() {
|
||||
dynamicShortcuts = shortcutManager.dynamicShortcuts.sortedBy { it.id }.toMutableList()
|
||||
}
|
||||
|
||||
private fun setDynamicShortcutData(shortcutId: String, index: Int) = viewModelScope.launch {
|
||||
if (dynamicShortcuts.isNotEmpty()) {
|
||||
for (item in dynamicShortcuts) {
|
||||
if (item.id == shortcutId) {
|
||||
Log.d(TAG, "setting ${item.id} data")
|
||||
shortcuts[index].serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId
|
||||
shortcuts[index].label.value = item.shortLabel.toString()
|
||||
shortcuts[index].desc.value = item.longLabel.toString()
|
||||
shortcuts[index].path.value = item.intent?.action.toString()
|
||||
shortcuts[index].selectedIcon.value = item.intent?.extras?.getInt("iconId").toString().toIntOrNull() ?: 0
|
||||
if (shortcuts[index].selectedIcon.value != 0) {
|
||||
shortcuts[index].drawable.value = getTileIcon(shortcuts[index].selectedIcon.value)
|
||||
}
|
||||
if (shortcuts[index].path.value.startsWith("entityId:")) {
|
||||
shortcuts[index].type.value = "entityId"
|
||||
} else {
|
||||
shortcuts[index].type.value = "lovelace"
|
||||
}
|
||||
shortcuts[index].setData(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTileIcon(tileIconId: Int): Drawable? {
|
||||
val loader = IconPackLoader(getApplication())
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack.loadDrawables(loader.drawableLoader)
|
||||
val iconDrawable = iconPack.icons[tileIconId]?.drawable
|
||||
if (iconDrawable != null) {
|
||||
val icon = DrawableCompat.wrap(iconDrawable)
|
||||
icon.setColorFilter(app.resources.getColor(R.color.colorAccent), PorterDuff.Mode.SRC_IN)
|
||||
return icon
|
||||
private suspend fun Shortcut.setData(item: ShortcutInfo) {
|
||||
serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId
|
||||
label.value = item.shortLabel.toString()
|
||||
desc.value = item.longLabel.toString()
|
||||
path.value = item.intent?.action.toString()
|
||||
selectedIcon.value = if (item.intent?.extras?.containsKey("iconName") == true) {
|
||||
item.intent?.extras?.getString("iconName")?.let { CommunityMaterial.getIconByMdiName(it) }
|
||||
} else if (item.intent?.extras?.containsKey("iconId") == true) {
|
||||
withContext(Dispatchers.IO) {
|
||||
item.intent?.extras?.getInt("iconId")?.takeIf { it != 0 }?.let {
|
||||
CommunityMaterial.getIconByMdiName("mdi:${iconIdToName.getValue(it)}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (path.value.startsWith("entityId:")) {
|
||||
type.value = "entityId"
|
||||
} else {
|
||||
type.value = "lovelace"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun updatePinnedShortcuts() {
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
|
@ -25,16 +26,13 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.maltaisn.icondialog.IconDialog
|
||||
import com.mikepenz.iconics.compose.IconicsPainter
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment
|
||||
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsViewModel
|
||||
|
@ -44,8 +42,7 @@ import io.homeassistant.companion.android.util.compose.ServerDropdownButton
|
|||
@Composable
|
||||
fun ManageShortcutsView(
|
||||
viewModel: ManageShortcutsViewModel,
|
||||
iconDialog: IconDialog,
|
||||
childFragment: FragmentManager
|
||||
showIconDialog: (tag: String) -> Unit
|
||||
) {
|
||||
LazyColumn(contentPadding = PaddingValues(16.dp)) {
|
||||
item {
|
||||
|
@ -67,8 +64,7 @@ fun ManageShortcutsView(
|
|||
CreateShortcutView(
|
||||
i = i,
|
||||
viewModel = viewModel,
|
||||
iconDialog = iconDialog,
|
||||
childFragment = childFragment
|
||||
showIconDialog = showIconDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +72,11 @@ fun ManageShortcutsView(
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@Composable
|
||||
private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, iconDialog: IconDialog, childFragment: FragmentManager) {
|
||||
private fun CreateShortcutView(
|
||||
i: Int,
|
||||
viewModel: ManageShortcutsViewModel,
|
||||
showIconDialog: (tag: String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var expandedEntity by remember { mutableStateOf(false) }
|
||||
var expandedPinnedShortcuts by remember { mutableStateOf(false) }
|
||||
|
@ -155,17 +155,21 @@ private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, icon
|
|||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
OutlinedButton(onClick = {
|
||||
iconDialog.show(childFragment, shortcutId)
|
||||
showIconDialog(shortcutId)
|
||||
}) {
|
||||
val icon = viewModel.shortcuts[i].drawable.value?.let { DrawableCompat.wrap(it) }
|
||||
icon?.toBitmap()?.asImageBitmap()
|
||||
?.let {
|
||||
Image(
|
||||
it,
|
||||
contentDescription = stringResource(id = R.string.shortcut_icon),
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent))
|
||||
)
|
||||
}
|
||||
val icon = viewModel.shortcuts[i].selectedIcon.value
|
||||
val painter = if (icon != null) {
|
||||
remember(icon) { IconicsPainter(icon) }
|
||||
} else {
|
||||
painterResource(R.drawable.ic_stat_ic_notification_blue)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = stringResource(id = R.string.shortcut_icon),
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,7 +273,6 @@ private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, icon
|
|||
shortcut.label.value,
|
||||
shortcut.desc.value,
|
||||
shortcut.path.value,
|
||||
shortcut.drawable.value?.toBitmap(),
|
||||
shortcut.selectedIcon.value
|
||||
)
|
||||
},
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
|
||||
const val MDI_PREFIX = "mdi:"
|
||||
|
||||
/**
|
||||
* Gets the MDI name of an Iconics icon.
|
||||
* MDI format is used by Home Assistant (ie "mdi:account-alert"),
|
||||
* compared to Iconic's [IIcon.name] format (ie "cmd_account_alert").
|
||||
*/
|
||||
val IIcon.mdiName: String
|
||||
get() = name.replace("${CommunityMaterial.mappingPrefix}_", MDI_PREFIX).replace('_', '-')
|
||||
|
||||
fun CommunityMaterial.getIconByMdiName(mdiName: String): IIcon? {
|
||||
val name = mdiName.replace(MDI_PREFIX, "${mappingPrefix}_").replace('-', '_')
|
||||
return try {
|
||||
getIcon(name)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Icon doesn't exist (anymore)
|
||||
null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
|
||||
@Composable
|
||||
fun IconDialogContent(
|
||||
iconFilter: IconFilter = DefaultIconFilter(),
|
||||
onSelect: (IIcon) -> Unit
|
||||
) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
Column {
|
||||
IconDialogSearch(value = searchQuery, onValueChange = { searchQuery = it })
|
||||
IconDialogGrid(
|
||||
typeface = CommunityMaterial,
|
||||
searchQuery = searchQuery,
|
||||
iconFilter = iconFilter,
|
||||
onClick = onSelect
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconDialog(
|
||||
iconFilter: IconFilter = DefaultIconFilter(),
|
||||
onSelect: (IIcon) -> Unit,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(480.dp)
|
||||
.height(500.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
IconDialogContent(iconFilter = iconFilter, onSelect = onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IconDialogPreview() {
|
||||
MdcTheme {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(480.dp)
|
||||
.height(500.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
IconDialogContent(onSelect = {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import kotlin.math.min
|
||||
|
||||
class IconDialogFragment(callback: (IIcon) -> Unit) : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "IconDialogFragment"
|
||||
}
|
||||
|
||||
private val onSelect = callback
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).also {
|
||||
it.clipToPadding = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
(view as ComposeView).setContent {
|
||||
MdcTheme {
|
||||
IconDialogContent(
|
||||
onSelect = onSelect
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val params = dialog?.window?.attributes ?: return
|
||||
params.width = min(
|
||||
(resources.displayMetrics.widthPixels * 0.9).toInt(),
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 480f, resources.displayMetrics).toInt()
|
||||
)
|
||||
params.height = min(
|
||||
(resources.displayMetrics.heightPixels * 0.9).toInt(),
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 500f, resources.displayMetrics).toInt()
|
||||
)
|
||||
dialog?.window?.attributes = params
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import com.mikepenz.iconics.compose.Image
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.ITypeface
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Display a grid of icons, letting the user select one.
|
||||
* @param icons List of icons to display
|
||||
* @param onClick Invoked when the user clicks on the given icon
|
||||
*/
|
||||
@Composable
|
||||
fun IconDialogGrid(
|
||||
icons: List<IIcon>,
|
||||
tint: Color = MaterialTheme.colors.onSurface,
|
||||
onClick: (IIcon) -> Unit
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(icons) { icon ->
|
||||
IconButton(onClick = { onClick(icon) }) {
|
||||
Image(
|
||||
asset = icon,
|
||||
colorFilter = ColorFilter.tint(tint),
|
||||
// https://material.io/design/iconography/system-icons.html#color
|
||||
alpha = 0.54f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a grid of icons, letting the user select one.
|
||||
* @param typeface Icon typeface that includes all possible icons.
|
||||
* @param searchQuery Search term used to filter icons from the [typeface].
|
||||
* @param iconFilter Adjust filtering logic for the search process.
|
||||
* @param onClick Invoked when the user clicks on the given icon
|
||||
*/
|
||||
@Composable
|
||||
fun IconDialogGrid(
|
||||
typeface: ITypeface,
|
||||
searchQuery: String,
|
||||
iconFilter: IconFilter = DefaultIconFilter(),
|
||||
tint: Color = MaterialTheme.colors.onSurface,
|
||||
onClick: (IIcon) -> Unit
|
||||
) {
|
||||
var icons by remember { mutableStateOf<List<IIcon>>(emptyList()) }
|
||||
LaunchedEffect(typeface, searchQuery) {
|
||||
icons = withContext(Dispatchers.IO) { iconFilter.queryIcons(typeface, searchQuery) }
|
||||
}
|
||||
|
||||
IconDialogGrid(icons = icons, tint = tint, onClick = onClick)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IconDialogGridPreview() {
|
||||
MdcTheme {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(480.dp)
|
||||
.height(500.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
IconDialogGrid(
|
||||
icons = CommunityMaterial.icons.map { name -> CommunityMaterial.getIcon(name) },
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import io.homeassistant.companion.android.common.R
|
||||
|
||||
@Composable
|
||||
fun IconDialogSearch(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val isEnglish by remember { mutableStateOf(Locale.current.language == "en") }
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
label = {
|
||||
Text(text = stringResource(if (isEnglish) R.string.search_icons else R.string.search_icons_in_english))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Filled.Search, contentDescription = null)
|
||||
},
|
||||
trailingIcon = if (value.isNotBlank()) {
|
||||
{
|
||||
IconButton(onClick = { onValueChange("") }) {
|
||||
Icon(Icons.Filled.Clear, contentDescription = stringResource(R.string.clear_search))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IconDialogSearchPreview() {
|
||||
MdcTheme {
|
||||
Surface {
|
||||
IconDialogSearch(value = "account", onValueChange = {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package io.homeassistant.companion.android.util.icondialog
|
||||
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.ITypeface
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Normalize [this] string, removing all diacritics, all unicode characters, hyphens,
|
||||
* apostrophes and more. Resulting text has only lowercase latin letters and digits.
|
||||
*/
|
||||
private fun String.normalize(locale: Locale): String {
|
||||
val normalized = this.lowercase(locale).trim().let { Normalizer.normalize(it, Normalizer.Form.NFKD) }
|
||||
return normalized.filter { c -> c in 'a'..'z' || c in 'а'..'я' || c in '0'..'9' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to filter the icons for search and sort them afterwards.
|
||||
* Icon filter must be parcelable to be put in bundle.
|
||||
*/
|
||||
interface IconFilter {
|
||||
|
||||
/**
|
||||
* Get a list of all matching icons for a search [query], in no specific order.
|
||||
*/
|
||||
fun queryIcons(pack: ITypeface, query: String? = null): List<IIcon>
|
||||
}
|
||||
|
||||
class DefaultIconFilter(
|
||||
/**
|
||||
* Regex used to split the query into multiple search terms.
|
||||
* Can also be null to not split the query.
|
||||
*/
|
||||
private val termSplitPattern: Regex? = """[;,\s]""".toRegex(),
|
||||
/**
|
||||
* Whether to normalize search query or not, using [String.normalize].
|
||||
*/
|
||||
private val queryNormalized: Boolean = true
|
||||
) : IconFilter {
|
||||
|
||||
/**
|
||||
* Get a list of all matching icons for a search [query].
|
||||
* Base implementation only returns the complete list of icons in the pack,
|
||||
* sorted by ID. Subclasses take care of actual searching and must always ensure
|
||||
* that the returned list is sorted by ID.
|
||||
*/
|
||||
override fun queryIcons(pack: ITypeface, query: String?): List<IIcon> {
|
||||
val icons = pack.icons
|
||||
|
||||
if (query == null || query.isBlank()) {
|
||||
// No search query, return all icons.
|
||||
return icons.map { key -> pack.getIcon(key) }
|
||||
}
|
||||
|
||||
// Split query into terms.
|
||||
val trimmedQuery = query.trim()
|
||||
val terms = (termSplitPattern?.let { trimmedQuery.split(it) } ?: listOf(trimmedQuery))
|
||||
.map {
|
||||
if (queryNormalized) {
|
||||
it.normalize(Locale.ROOT)
|
||||
} else {
|
||||
it.lowercase(Locale.ROOT)
|
||||
}
|
||||
}.filter { it.isNotBlank() }
|
||||
|
||||
// Remove all icons that don't match any of the search terms.
|
||||
return icons
|
||||
.filter { icon -> matchesSearch(icon, terms) }
|
||||
.map { key -> pack.getIcon(key) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an [icon] name matches any of the search [terms].
|
||||
*/
|
||||
private fun matchesSearch(icon: String, terms: List<String>): Boolean {
|
||||
val name = if (queryNormalized) icon.normalize(Locale.ROOT) else icon
|
||||
return terms.any { it in name }
|
||||
}
|
||||
}
|
|
@ -21,9 +21,11 @@ import androidx.core.graphics.toColorInt
|
|||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.IconicsSize
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.padding
|
||||
import com.mikepenz.iconics.utils.size
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
|
@ -31,6 +33,7 @@ import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
|
|||
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
|
||||
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
|
||||
import io.homeassistant.companion.android.util.getAttribute
|
||||
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
|
||||
import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -56,7 +59,7 @@ class ButtonWidget : AppWidgetProvider() {
|
|||
internal const val EXTRA_SERVICE = "EXTRA_SERVICE"
|
||||
internal const val EXTRA_SERVICE_DATA = "EXTRA_SERVICE_DATA"
|
||||
internal const val EXTRA_LABEL = "EXTRA_LABEL"
|
||||
internal const val EXTRA_ICON = "EXTRA_ICON"
|
||||
internal const val EXTRA_ICON_NAME = "EXTRA_ICON_NAME"
|
||||
internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
|
||||
internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR"
|
||||
internal const val EXTRA_REQUIRE_AUTHENTICATION = "EXTRA_REQUIRE_AUTHENTICATION"
|
||||
|
@ -71,8 +74,6 @@ class ButtonWidget : AppWidgetProvider() {
|
|||
@Inject
|
||||
lateinit var buttonWidgetDao: ButtonWidgetDao
|
||||
|
||||
private var iconPack: IconPack? = null
|
||||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onUpdate(
|
||||
|
@ -176,12 +177,6 @@ class ButtonWidget : AppWidgetProvider() {
|
|||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
}
|
||||
|
||||
// Create an icon pack and load all drawables.
|
||||
if (iconPack == null) {
|
||||
val loader = IconPackLoader(context)
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack!!.loadDrawables(loader.drawableLoader)
|
||||
}
|
||||
val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable()
|
||||
return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_button_wrapper_dynamiccolor else R.layout.widget_button_wrapper_default).apply {
|
||||
// Theming
|
||||
|
@ -196,39 +191,41 @@ class ButtonWidget : AppWidgetProvider() {
|
|||
setLabelVisibility(this, widget)
|
||||
|
||||
// Content
|
||||
val iconId = widget?.iconId ?: 988171 // Lightning bolt
|
||||
val iconData = widget?.iconName?.let { CommunityMaterial.getIconByMdiName(it) }
|
||||
?: CommunityMaterial.Icon2.cmd_flash // Lightning bolt
|
||||
|
||||
val iconDrawable = iconPack?.icons?.get(iconId)?.drawable
|
||||
if (iconDrawable != null) {
|
||||
val icon = DrawableCompat.wrap(iconDrawable)
|
||||
if (widget?.backgroundType == WidgetBackgroundType.TRANSPARENT) {
|
||||
setInt(R.id.widgetImageButton, "setColorFilter", textColor)
|
||||
}
|
||||
|
||||
// Determine reasonable dimensions for drawing vector icon as a bitmap
|
||||
val aspectRatio = iconDrawable.intrinsicWidth / iconDrawable.intrinsicHeight.toDouble()
|
||||
val awo = if (widget != null) AppWidgetManager.getInstance(context).getAppWidgetOptions(widget.id) else null
|
||||
val maxWidth = (
|
||||
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_MAX_ICON_SIZE)
|
||||
?: DEFAULT_MAX_ICON_SIZE
|
||||
).coerceAtLeast(16)
|
||||
val maxHeight = (
|
||||
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_MAX_ICON_SIZE)
|
||||
?: DEFAULT_MAX_ICON_SIZE
|
||||
).coerceAtLeast(16)
|
||||
val width: Int
|
||||
val height: Int
|
||||
if (maxWidth > maxHeight) {
|
||||
width = maxWidth
|
||||
height = (maxWidth * (1 / aspectRatio)).toInt()
|
||||
} else {
|
||||
width = (maxHeight * aspectRatio).toInt()
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
// Render the icon into the Button's ImageView
|
||||
setImageViewBitmap(R.id.widgetImageButton, icon.toBitmap(width, height))
|
||||
val iconDrawable = IconicsDrawable(context, iconData).apply {
|
||||
padding = IconicsSize.dp(2)
|
||||
size = IconicsSize.dp(24)
|
||||
}
|
||||
val icon = DrawableCompat.wrap(iconDrawable)
|
||||
if (widget?.backgroundType == WidgetBackgroundType.TRANSPARENT) {
|
||||
setInt(R.id.widgetImageButton, "setColorFilter", textColor)
|
||||
}
|
||||
|
||||
// Determine reasonable dimensions for drawing vector icon as a bitmap
|
||||
val aspectRatio = iconDrawable.intrinsicWidth / iconDrawable.intrinsicHeight.toDouble()
|
||||
val awo = if (widget != null) AppWidgetManager.getInstance(context).getAppWidgetOptions(widget.id) else null
|
||||
val maxWidth = (
|
||||
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_MAX_ICON_SIZE)
|
||||
?: DEFAULT_MAX_ICON_SIZE
|
||||
).coerceAtLeast(16)
|
||||
val maxHeight = (
|
||||
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_MAX_ICON_SIZE)
|
||||
?: DEFAULT_MAX_ICON_SIZE
|
||||
).coerceAtLeast(16)
|
||||
val width: Int
|
||||
val height: Int
|
||||
if (maxWidth > maxHeight) {
|
||||
width = maxWidth
|
||||
height = (maxWidth * (1 / aspectRatio)).toInt()
|
||||
} else {
|
||||
width = (maxHeight * aspectRatio).toInt()
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
// Render the icon into the Button's ImageView
|
||||
setImageViewBitmap(R.id.widgetImageButton, icon.toBitmap(width, height))
|
||||
|
||||
setOnClickPendingIntent(
|
||||
R.id.widgetImageButtonLayout,
|
||||
|
@ -364,7 +361,7 @@ class ButtonWidget : AppWidgetProvider() {
|
|||
val serviceData: String? = extras.getString(EXTRA_SERVICE_DATA)
|
||||
val label: String? = extras.getString(EXTRA_LABEL)
|
||||
val requireAuthentication: Boolean = extras.getBoolean(EXTRA_REQUIRE_AUTHENTICATION)
|
||||
val icon: Int = extras.getInt(EXTRA_ICON)
|
||||
val icon: String = extras.getString(EXTRA_ICON_NAME) ?: "mdi:flash"
|
||||
val backgroundType: WidgetBackgroundType = extras.getSerializable(EXTRA_BACKGROUND_TYPE) as WidgetBackgroundType
|
||||
val textColor: String? = extras.getString(EXTRA_TEXT_COLOR)
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.app.PendingIntent
|
|||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
|
@ -21,21 +23,18 @@ import android.widget.Spinner
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.maltaisn.icondialog.IconDialog
|
||||
import com.maltaisn.icondialog.IconDialogSettings
|
||||
import com.maltaisn.icondialog.data.Icon
|
||||
import com.maltaisn.icondialog.pack.IconPack
|
||||
import com.maltaisn.icondialog.pack.IconPackLoader
|
||||
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.Service
|
||||
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
|
||||
|
@ -43,6 +42,9 @@ import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
|
|||
import io.homeassistant.companion.android.databinding.WidgetButtonConfigureBinding
|
||||
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
|
||||
import io.homeassistant.companion.android.util.getHexForColor
|
||||
import io.homeassistant.companion.android.util.icondialog.IconDialogFragment
|
||||
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
|
||||
import io.homeassistant.companion.android.util.icondialog.mdiName
|
||||
import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity
|
||||
import io.homeassistant.companion.android.widgets.common.ServiceFieldBinder
|
||||
import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter
|
||||
|
@ -52,10 +54,9 @@ import javax.inject.Inject
|
|||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.Callback {
|
||||
class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
||||
companion object {
|
||||
private const val TAG: String = "ButtonWidgetConfigAct"
|
||||
private const val ICON_DIALOG_TAG = "icon-dialog"
|
||||
private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.button.ButtonWidgetConfigureActivity.PIN_WIDGET_CALLBACK"
|
||||
}
|
||||
|
||||
|
@ -63,8 +64,6 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
|
|||
lateinit var buttonWidgetDao: ButtonWidgetDao
|
||||
override val dao get() = buttonWidgetDao
|
||||
|
||||
private lateinit var iconPack: IconPack
|
||||
|
||||
private var services = mutableMapOf<Int, HashMap<String, Service>>()
|
||||
private var entities = mutableMapOf<Int, HashMap<String, Entity<Any>>>()
|
||||
private var dynamicFields = ArrayList<ServiceFieldBinder>()
|
||||
|
@ -258,9 +257,6 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
|
|||
|
||||
setupServerSelect(buttonWidget?.serverId)
|
||||
|
||||
// Create an icon pack loader with application context.
|
||||
val loader = IconPackLoader(this)
|
||||
|
||||
serviceAdapter = SingleItemArrayAdapter<Service>(this) {
|
||||
if (it != null) getServiceString(it) else ""
|
||||
}
|
||||
|
@ -349,16 +345,20 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
|
|||
// Do this off the main thread, takes a second or two...
|
||||
runOnUiThread {
|
||||
// Create an icon pack and load all drawables.
|
||||
iconPack = createMaterialDesignIconPack(loader)
|
||||
iconPack.loadDrawables(loader.drawableLoader)
|
||||
val settings = IconDialogSettings {
|
||||
searchVisibility = IconDialog.SearchVisibility.ALWAYS
|
||||
}
|
||||
val iconDialog = IconDialog.newInstance(settings)
|
||||
val iconId = buttonWidget?.iconId ?: 62017
|
||||
onIconDialogIconsSelected(iconDialog, listOf(iconPack.icons[iconId]!!))
|
||||
val iconName = buttonWidget?.iconName ?: "mdi:flash"
|
||||
val icon = CommunityMaterial.getIconByMdiName(iconName) ?: CommunityMaterial.Icon2.cmd_flash
|
||||
onIconDialogIconsSelected(icon)
|
||||
binding.widgetConfigIconSelector.setOnClickListener {
|
||||
iconDialog.show(supportFragmentManager, ICON_DIALOG_TAG)
|
||||
var alertDialog: DialogFragment? = null
|
||||
|
||||
alertDialog = IconDialogFragment(
|
||||
callback = {
|
||||
onIconDialogIconsSelected(it)
|
||||
alertDialog?.dismiss()
|
||||
}
|
||||
)
|
||||
|
||||
alertDialog.show(supportFragmentManager, IconDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -400,23 +400,12 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
|
|||
}
|
||||
}
|
||||
|
||||
override val iconDialogIconPack: IconPack?
|
||||
get() = iconPack
|
||||
private fun onIconDialogIconsSelected(selectedIcon: IIcon) {
|
||||
binding.widgetConfigIconSelector.tag = selectedIcon.mdiName
|
||||
val iconDrawable = IconicsDrawable(this, selectedIcon)
|
||||
iconDrawable.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(this, commonR.color.colorIcon), PorterDuff.Mode.SRC_IN)
|
||||
|
||||
override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List<Icon>) {
|
||||
Log.d(TAG, "Selected icon: ${icons.firstOrNull()}")
|
||||
val selectedIcon = icons.firstOrNull()
|
||||
if (selectedIcon != null) {
|
||||
binding.widgetConfigIconSelector.tag = selectedIcon.id
|
||||
val iconDrawable = selectedIcon.drawable
|
||||
if (iconDrawable != null) {
|
||||
val icon = DrawableCompat.wrap(iconDrawable)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
DrawableCompat.setTint(icon, resources.getColor(commonR.color.colorIcon, theme))
|
||||
}
|
||||
binding.widgetConfigIconSelector.setImageBitmap(icon.toBitmap())
|
||||
}
|
||||
}
|
||||
binding.widgetConfigIconSelector.setImageBitmap(iconDrawable.toBitmap())
|
||||
}
|
||||
|
||||
private fun onAddWidget() {
|
||||
|
@ -459,8 +448,8 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
|
|||
binding.label.text.toString()
|
||||
)
|
||||
intent.putExtra(
|
||||
ButtonWidget.EXTRA_ICON,
|
||||
binding.widgetConfigIconSelector.tag as Int
|
||||
ButtonWidget.EXTRA_ICON_NAME,
|
||||
binding.widgetConfigIconSelector.tag as String
|
||||
)
|
||||
|
||||
// Analyze and send service data
|
||||
|
|
|
@ -45,6 +45,9 @@ android {
|
|||
java {
|
||||
srcDirs("../app/src/main/java")
|
||||
}
|
||||
assets {
|
||||
srcDirs("../app/src/main/assets")
|
||||
}
|
||||
res {
|
||||
srcDirs("../app/src/main/res")
|
||||
}
|
||||
|
@ -169,8 +172,6 @@ dependencies {
|
|||
|
||||
implementation("com.github.Dimezis:BlurView:version-1.6.6")
|
||||
implementation("org.altbeacon:android-beacon-library:2.19.5")
|
||||
implementation("com.maltaisn:icondialog:3.3.0")
|
||||
implementation("com.maltaisn:iconpack-community-material:5.3.45")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.22")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
|
||||
|
|
|
@ -0,0 +1,994 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 41,
|
||||
"identityHash": "5ccc437900bf203ab7db2e782ffbd7f9",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "sensor_attributes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sensorId",
|
||||
"columnName": "sensor_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "value_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sensor_id",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "authentication_list",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sensors",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`, `server_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "registered",
|
||||
"columnName": "registered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "NULL"
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "state",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSentState",
|
||||
"columnName": "last_sent_state",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "NULL"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSentIcon",
|
||||
"columnName": "last_sent_icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "NULL"
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateType",
|
||||
"columnName": "state_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceClass",
|
||||
"columnName": "device_class",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "unitOfMeasurement",
|
||||
"columnName": "unit_of_measurement",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateClass",
|
||||
"columnName": "state_class",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityCategory",
|
||||
"columnName": "entity_category",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "coreRegistration",
|
||||
"columnName": "core_registration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appRegistration",
|
||||
"columnName": "app_registration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"server_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sensor_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sensorId",
|
||||
"columnName": "sensor_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "value_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entries",
|
||||
"columnName": "entries",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sensor_id",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "button_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconName",
|
||||
"columnName": "icon_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domain",
|
||||
"columnName": "domain",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "service",
|
||||
"columnName": "service",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceData",
|
||||
"columnName": "service_data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "requireAuthentication",
|
||||
"columnName": "require_authentication",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "camera_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "media_player_controls_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSkip",
|
||||
"columnName": "show_skip",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSeek",
|
||||
"columnName": "show_seek",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showVolume",
|
||||
"columnName": "show_volume",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSource",
|
||||
"columnName": "show_source",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "static_widget",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attributeIds",
|
||||
"columnName": "attribute_ids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "textSize",
|
||||
"columnName": "text_size",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateSeparator",
|
||||
"columnName": "state_separator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attributeSeparator",
|
||||
"columnName": "attribute_separator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdate",
|
||||
"columnName": "last_update",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "template_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "template",
|
||||
"columnName": "template",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "textSize",
|
||||
"columnName": "text_size",
|
||||
"affinity": "REAL",
|
||||
"notNull": true,
|
||||
"defaultValue": "12.0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdate",
|
||||
"columnName": "last_update",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "notification_history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL, `server_id` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "received",
|
||||
"columnName": "received",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data",
|
||||
"columnName": "data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "qs_tiles",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tileId",
|
||||
"columnName": "tile_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "added",
|
||||
"columnName": "added",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "server_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconName",
|
||||
"columnName": "icon_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitle",
|
||||
"columnName": "subtitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldVibrate",
|
||||
"columnName": "should_vibrate",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "authRequired",
|
||||
"columnName": "auth_required",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "favorites",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "favorite_cache",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "friendlyName",
|
||||
"columnName": "friendly_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "entity_state_complications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showTitle",
|
||||
"columnName": "show_title",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "servers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `prioritize_internal` INTEGER NOT NULL, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "_name",
|
||||
"columnName": "_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nameOverride",
|
||||
"columnName": "name_override",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "_version",
|
||||
"columnName": "_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "listOrder",
|
||||
"columnName": "list_order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceName",
|
||||
"columnName": "device_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.externalUrl",
|
||||
"columnName": "external_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.internalUrl",
|
||||
"columnName": "internal_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.cloudUrl",
|
||||
"columnName": "cloud_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.webhookId",
|
||||
"columnName": "webhook_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.secret",
|
||||
"columnName": "secret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.cloudhookUrl",
|
||||
"columnName": "cloudhook_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.useCloud",
|
||||
"columnName": "use_cloud",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.internalSsids",
|
||||
"columnName": "internal_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "connection.prioritizeInternal",
|
||||
"columnName": "prioritize_internal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "session.accessToken",
|
||||
"columnName": "access_token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "session.refreshToken",
|
||||
"columnName": "refresh_token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "session.tokenExpiration",
|
||||
"columnName": "token_expiration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "session.tokenType",
|
||||
"columnName": "token_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "session.installId",
|
||||
"columnName": "install_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user.id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user.name",
|
||||
"columnName": "user_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user.isOwner",
|
||||
"columnName": "user_is_owner",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user.isAdmin",
|
||||
"columnName": "user_is_admin",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "websocketSetting",
|
||||
"columnName": "websocket_setting",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensorUpdateFrequency",
|
||||
"columnName": "sensor_update_frequency",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ccc437900bf203ab7db2e782ffbd7f9')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package io.homeassistant.companion.android.database
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
|
@ -14,6 +17,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.OnConflictStrategy
|
||||
|
@ -65,7 +69,6 @@ import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
|
|||
import io.homeassistant.companion.android.database.widget.WidgetBackgroundTypeConverter
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.UUID
|
||||
import kotlin.collections.ArrayList
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@Database(
|
||||
|
@ -87,7 +90,7 @@ import io.homeassistant.companion.android.common.R as commonR
|
|||
Server::class,
|
||||
Setting::class
|
||||
],
|
||||
version = 40,
|
||||
version = 41,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 24, to = 25),
|
||||
AutoMigration(from = 25, to = 26),
|
||||
|
@ -175,12 +178,25 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
MIGRATION_20_21,
|
||||
MIGRATION_21_22,
|
||||
MIGRATION_22_23,
|
||||
MIGRATION_23_24
|
||||
MIGRATION_23_24,
|
||||
Migration40to41(context.assets)
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun <T> Cursor.map(transform: (Cursor) -> T): List<T> {
|
||||
return if (moveToFirst()) {
|
||||
val results = mutableListOf<T>()
|
||||
do {
|
||||
results.add(transform(this))
|
||||
} while (moveToNext())
|
||||
results
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
|
@ -210,22 +226,22 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
|
||||
private val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
try {
|
||||
val contentValues: ArrayList<ContentValues> = ArrayList()
|
||||
val widgets = database.query("SELECT * FROM `static_widget`")
|
||||
widgets.use {
|
||||
if (widgets.count > 0) {
|
||||
while (widgets.moveToNext()) {
|
||||
val cv = ContentValues()
|
||||
cv.put("id", widgets.getInt(widgets.getColumnIndex("id")))
|
||||
cv.put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id")))
|
||||
cv.put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id")))
|
||||
cv.put("label", widgets.getString(widgets.getColumnIndex("label")))
|
||||
cv.put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size")))
|
||||
cv.put("state_separator", widgets.getString(widgets.getColumnIndex("separator")))
|
||||
cv.put("attribute_separator", " ")
|
||||
contentValues.add(cv)
|
||||
val contentValues = widgets.map { widgets ->
|
||||
ContentValues().apply {
|
||||
put("id", widgets.getInt(widgets.getColumnIndex("id")))
|
||||
put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id")))
|
||||
put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id")))
|
||||
put("label", widgets.getString(widgets.getColumnIndex("label")))
|
||||
put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size")))
|
||||
put("state_separator", widgets.getString(widgets.getColumnIndex("separator")))
|
||||
put("attribute_separator", " ")
|
||||
}
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `static_widget`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `static_widget` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` FLOAT NOT NULL DEFAULT '30', `state_separator` TEXT NOT NULL DEFAULT '', `attribute_separator` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))")
|
||||
|
@ -245,44 +261,38 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
|
||||
private val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val cursor = database.query("SELECT * FROM sensors")
|
||||
val sensors = mutableListOf<ContentValues>()
|
||||
var migrationSuccessful = false
|
||||
var migrationFailed = false
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
while (cursor.moveToNext()) {
|
||||
sensors.add(
|
||||
ContentValues().also {
|
||||
it.put("id", cursor.getString(cursor.getColumnIndex("unique_id")))
|
||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||
it.put(
|
||||
"registered",
|
||||
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||
)
|
||||
it.put("state", "")
|
||||
it.put("state_type", "")
|
||||
it.put("type", "")
|
||||
it.put("icon", "")
|
||||
it.put("name", "")
|
||||
it.put("device_class", "")
|
||||
}
|
||||
)
|
||||
val sensors = try {
|
||||
database.query("SELECT * FROM sensors").use { cursor ->
|
||||
cursor.map {
|
||||
ContentValues().also {
|
||||
it.put("id", cursor.getString(cursor.getColumnIndex("unique_id")))
|
||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||
it.put(
|
||||
"registered",
|
||||
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||
)
|
||||
it.put("state", "")
|
||||
it.put("state_type", "")
|
||||
it.put("type", "")
|
||||
it.put("icon", "")
|
||||
it.put("name", "")
|
||||
it.put("device_class", "")
|
||||
}
|
||||
}
|
||||
migrationSuccessful = true
|
||||
}
|
||||
cursor.close()
|
||||
} catch (e: Exception) {
|
||||
migrationFailed = true
|
||||
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||
null
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
||||
if (migrationSuccessful) {
|
||||
sensors.forEach {
|
||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
|
||||
sensors?.forEach {
|
||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
if (migrationFailed) {
|
||||
notifyMigrationFailed()
|
||||
|
@ -305,44 +315,38 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
|
||||
private val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val cursor = database.query("SELECT * FROM sensors")
|
||||
val sensors = mutableListOf<ContentValues>()
|
||||
var migrationSuccessful = false
|
||||
var migrationFailed = false
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
while (cursor.moveToNext()) {
|
||||
sensors.add(
|
||||
ContentValues().also {
|
||||
it.put("id", cursor.getString(cursor.getColumnIndex("id")))
|
||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||
it.put(
|
||||
"registered",
|
||||
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||
)
|
||||
it.put("state", "")
|
||||
it.put("last_sent_state", "")
|
||||
it.put("state_type", "")
|
||||
it.put("type", "")
|
||||
it.put("icon", "")
|
||||
it.put("name", "")
|
||||
}
|
||||
)
|
||||
val sensors = try {
|
||||
database.query("SELECT * FROM sensors").use { cursor ->
|
||||
cursor.map {
|
||||
ContentValues().also {
|
||||
it.put("id", cursor.getString(cursor.getColumnIndex("id")))
|
||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||
it.put(
|
||||
"registered",
|
||||
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||
)
|
||||
it.put("state", "")
|
||||
it.put("last_sent_state", "")
|
||||
it.put("state_type", "")
|
||||
it.put("type", "")
|
||||
it.put("icon", "")
|
||||
it.put("name", "")
|
||||
}
|
||||
}
|
||||
migrationSuccessful = true
|
||||
}
|
||||
cursor.close()
|
||||
} catch (e: Exception) {
|
||||
migrationFailed = true
|
||||
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||
null
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
||||
if (migrationSuccessful) {
|
||||
sensors.forEach {
|
||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
|
||||
sensors?.forEach {
|
||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
if (migrationFailed) {
|
||||
notifyMigrationFailed()
|
||||
|
@ -388,6 +392,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
|
||||
private val MIGRATION_16_17 = object : Migration(16, 17) {
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val cursor = database.query("SELECT * FROM sensor_settings")
|
||||
val sensorSettings = mutableListOf<ContentValues>()
|
||||
|
@ -472,19 +477,17 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
)
|
||||
}
|
||||
migrationSuccessful = true
|
||||
}
|
||||
cursor.close()
|
||||
} catch (e: Exception) {
|
||||
migrationFailed = true
|
||||
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||
null
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `sensor_settings`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_settings` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL DEFAULT 'string', `entries` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT '1', PRIMARY KEY(`sensor_id`, `name`))")
|
||||
if (migrationSuccessful) {
|
||||
sensorSettings.forEach {
|
||||
database.insert("sensor_settings", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
|
||||
sensorSettings?.forEach {
|
||||
database.insert("sensor_settings", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
if (migrationFailed) {
|
||||
notifyMigrationFailed()
|
||||
|
@ -811,6 +814,86 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
|
||||
class Migration40to41(assets: AssetManager) : Migration(40, 41) {
|
||||
private val iconIdToName: Map<Int, String> by lazy { IconDialogCompat(assets).loadAllIcons() }
|
||||
|
||||
private fun Cursor.getIconName(columnIndex: Int): String {
|
||||
val iconId = getInt(columnIndex)
|
||||
return "mdi:${iconIdToName.getValue(iconId)}"
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
var migrationFailed = false
|
||||
val widgets = try {
|
||||
database.query("SELECT * FROM `button_widgets`").use { cursor ->
|
||||
cursor.map {
|
||||
ContentValues().apply {
|
||||
put("id", cursor.getString(cursor.getColumnIndex("id")))
|
||||
put("server_id", cursor.getInt(cursor.getColumnIndex("server_id")))
|
||||
put("domain", cursor.getString(cursor.getColumnIndex("domain")))
|
||||
put("service", cursor.getString(cursor.getColumnIndex("service")))
|
||||
put("service_data", cursor.getString(cursor.getColumnIndex("service_data")))
|
||||
put("label", cursor.getStringOrNull(cursor.getColumnIndex("label")))
|
||||
put("background_type", cursor.getString(cursor.getColumnIndex("background_type")))
|
||||
put("text_color", cursor.getStringOrNull(cursor.getColumnIndex("text_color")))
|
||||
put("require_authentication", cursor.getInt(cursor.getColumnIndex("require_authentication")))
|
||||
|
||||
put("icon_name", cursor.getIconName(cursor.getColumnIndex("icon_id")))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
migrationFailed = true
|
||||
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||
null
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `button_widgets`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `button_widgets` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))")
|
||||
widgets?.forEach {
|
||||
database.insert("button_widgets", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
Log.d(TAG, "Migrated ${widgets?.size ?: "no"} button widgets to MDI icon names")
|
||||
|
||||
val tiles = try {
|
||||
database.query("SELECT * FROM `qs_tiles`").use { cursor ->
|
||||
cursor.map {
|
||||
ContentValues().apply {
|
||||
put("id", cursor.getString(cursor.getColumnIndex("id")))
|
||||
put("tile_id", cursor.getString(cursor.getColumnIndex("tile_id")))
|
||||
put("added", cursor.getInt(cursor.getColumnIndex("added")))
|
||||
put("server_id", cursor.getInt(cursor.getColumnIndex("server_id")))
|
||||
put("entity_id", cursor.getString(cursor.getColumnIndex("entity_id")))
|
||||
put("label", cursor.getString(cursor.getColumnIndex("label")))
|
||||
put("subtitle", cursor.getStringOrNull(cursor.getColumnIndex("subtitle")))
|
||||
put("should_vibrate", cursor.getInt(cursor.getColumnIndex("should_vibrate")))
|
||||
put("auth_required", cursor.getInt(cursor.getColumnIndex("auth_required")))
|
||||
|
||||
val oldIconColumn = cursor.getColumnIndex("icon_id")
|
||||
if (!cursor.isNull(oldIconColumn)) {
|
||||
put("icon_name", cursor.getIconName(oldIconColumn))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
migrationFailed = true
|
||||
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||
null
|
||||
}
|
||||
database.execSQL("DROP TABLE IF EXISTS `qs_tiles`")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `qs_tiles` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)")
|
||||
tiles?.forEach {
|
||||
database.insert("qs_tiles", OnConflictStrategy.REPLACE, it)
|
||||
}
|
||||
Log.d(TAG, "Migrated ${tiles?.size ?: "no"} QS tiles to MDI icon names")
|
||||
|
||||
if (migrationFailed) {
|
||||
notifyMigrationFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = appContext.getSystemService<NotificationManager>()!!
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package io.homeassistant.companion.android.database
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.util.JsonReader
|
||||
import android.util.NoSuchPropertyException
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Translation layer for IDs used by the old icondialog package to material icon names.
|
||||
*/
|
||||
class IconDialogCompat @Inject constructor(
|
||||
private val assets: AssetManager
|
||||
) {
|
||||
/**
|
||||
* Loads map of icon IDs to regular icon names.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun loadAllIcons(): Map<Int, String> {
|
||||
val inputStream = assets.open("mdi_id_map.json")
|
||||
return JsonReader(inputStream.reader()).use { reader ->
|
||||
val result = mutableMapOf<Int, String>()
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
val iconName = reader.nextName()
|
||||
val iconId = reader.nextInt()
|
||||
result[iconId] = iconName
|
||||
}
|
||||
reader.endObject()
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads map of icon IDs to regular icon names in a background thread.
|
||||
*/
|
||||
suspend fun loadAllIconsAsync() = coroutineScope {
|
||||
async(Dispatchers.IO) { loadAllIcons() }
|
||||
}
|
||||
|
||||
suspend fun streamingIconLookup(iconId: Int): String {
|
||||
val iconName = withContext(Dispatchers.IO) {
|
||||
val inputStream = assets.open("mdi_id_map.json")
|
||||
JsonReader(inputStream.reader()).use { reader ->
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
val iconName = reader.nextName()
|
||||
val id = reader.nextInt()
|
||||
if (iconId == id) {
|
||||
return@use iconName
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
throw NoSuchPropertyException("ID $iconId is not valid")
|
||||
}
|
||||
}
|
||||
return iconName
|
||||
}
|
||||
}
|
|
@ -14,8 +14,9 @@ data class TileEntity(
|
|||
val added: Boolean,
|
||||
@ColumnInfo(name = "server_id", defaultValue = "0")
|
||||
val serverId: Int,
|
||||
@ColumnInfo(name = "icon_id")
|
||||
val iconId: Int?,
|
||||
/** Icon name, such as "mdi:account-alert" */
|
||||
@ColumnInfo(name = "icon_name")
|
||||
val iconName: String?,
|
||||
@ColumnInfo(name = "entity_id")
|
||||
val entityId: String,
|
||||
@ColumnInfo(name = "label")
|
||||
|
|
|
@ -10,8 +10,8 @@ data class ButtonWidgetEntity(
|
|||
override val id: Int,
|
||||
@ColumnInfo(name = "server_id", defaultValue = "0")
|
||||
override val serverId: Int,
|
||||
@ColumnInfo(name = "icon_id")
|
||||
val iconId: Int,
|
||||
@ColumnInfo(name = "icon_name")
|
||||
val iconName: String,
|
||||
@ColumnInfo(name = "domain")
|
||||
val domain: String,
|
||||
@ColumnInfo(name = "service")
|
||||
|
|
|
@ -120,8 +120,9 @@
|
|||
<string name="choose_entity">Choose entity</string>
|
||||
<string name="choose_server">Choose server</string>
|
||||
<string name="clear_favorites">Clear Favorites</string>
|
||||
<string name="clear_search">Clear search</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="color_temp">Color temperature: %1$s</string>
|
||||
<string name="color_temp">Color temperature: %1$d</string>
|
||||
<string name="collapse">Collapse</string>
|
||||
<string name="complication_entity_invalid">Invalid entity</string>
|
||||
<string name="complication_entity_state_content_description">Entity state</string>
|
||||
|
@ -486,6 +487,8 @@
|
|||
<string name="scene">Scene</string>
|
||||
<string name="scenes">Scenes</string>
|
||||
<string name="scripts">Scripts</string>
|
||||
<string name="search_icons">Search icons</string>
|
||||
<string name="search_icons_in_english">Search icons (in English)</string>
|
||||
<string name="search_notifications">Search notifications</string>
|
||||
<string name="search_results">Search Results</string>
|
||||
<string name="search_sensors">Search sensors</string>
|
||||
|
|
Loading…
Reference in a new issue