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:
Joris Pelgröm 2023-07-04 19:25:32 +02:00 committed by GitHub
parent 938c515549
commit d1b17aa606
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1861 additions and 423 deletions

View file

@ -143,8 +143,6 @@ dependencies {
implementation("com.github.Dimezis:BlurView:version-1.6.6") implementation("com.github.Dimezis:BlurView:version-1.6.6")
implementation("org.altbeacon:android-beacon-library:2.19.5") 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-stdlib:1.8.22")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")

File diff suppressed because one or more lines are too long

View file

@ -13,12 +13,8 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService 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.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn 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.database.qs.numberedId
import io.homeassistant.companion.android.settings.SettingsActivity import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.settings.qs.updateActiveTileServices import io.homeassistant.companion.android.settings.qs.updateActiveTileServices
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
@ -113,7 +110,7 @@ abstract class TileExtensions : TileService() {
serverManager.integrationRepository(tileData.serverId).getEntityUpdates(listOf(tileData.entityId))?.collect { serverManager.integrationRepository(tileData.serverId).getEntityUpdates(listOf(tileData.entityId))?.collect {
tile.state = tile.state =
if (it.state in validActiveStates) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE 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.icon = Icon.createWithBitmap(icon)
} }
tile.updateTile() tile.updateTile()
@ -147,7 +144,7 @@ abstract class TileExtensions : TileService() {
val state: Entity<*>? = val state: Entity<*>? =
if ( if (
tileData.entityId.split(".")[0] in toggleDomainsWithLock || tileData.entityId.split(".")[0] in toggleDomainsWithLock ||
tileData.iconId == null tileData.iconName == null
) { ) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@ -170,7 +167,7 @@ abstract class TileExtensions : TileService() {
tile.state = Tile.STATE_INACTIVE tile.state = Tile.STATE_INACTIVE
} }
getTileIcon(tileData.iconId, state, context)?.let { icon -> getTileIcon(tileData.iconName, state, context)?.let { icon ->
tile.icon = Icon.createWithBitmap(icon) tile.icon = Icon.createWithBitmap(icon)
} }
Log.d(TAG, "Tile data set for tile ID: $tileId") Log.d(TAG, "Tile data set for tile ID: $tileId")
@ -308,7 +305,7 @@ abstract class TileExtensions : TileService() {
tileId = tileId, tileId = tileId,
added = true, added = true,
serverId = 0, serverId = 0,
iconId = null, iconName = null,
entityId = "", entityId = "",
label = "", label = "",
subtitle = null, subtitle = null,
@ -324,26 +321,17 @@ abstract class TileExtensions : TileService() {
updateActiveTileServices(highestInUse, applicationContext) 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. // Create an icon pack and load all drawables.
if (tileIconId != null) { if (!tileIconName.isNullOrBlank()) {
if (iconPack == null) { val icon = CommunityMaterial.getIconByMdiName(tileIconName) ?: return null
val loader = IconPackLoader(context) val iconDrawable = IconicsDrawable(context, icon)
iconPack = createMaterialDesignIconPack(loader) return iconDrawable.toBitmap()
iconPack!!.loadDrawables(loader.drawableLoader)
}
val iconDrawable = iconPack?.icons?.get(tileIconId)?.drawable
if (iconDrawable != null) {
return DrawableCompat.wrap(iconDrawable).toBitmap()
}
} else { } else {
entity?.getIcon(context)?.let { entity?.getIcon(context)?.let {
return DrawableCompat.wrap( return IconicsDrawable(context, it).apply {
IconicsDrawable(context, it).apply { sizeDp = 48
sizeDp = 48 }.toBitmap()
}
).toBitmap()
} }
} }
@ -352,7 +340,6 @@ abstract class TileExtensions : TileService() {
companion object { companion object {
private const val TAG = "TileExtensions" private const val TAG = "TileExtensions"
private var iconPack: IconPack? = null
private val toggleDomains = listOf( private val toggleDomains = listOf(
"automation", "cover", "fan", "humidifier", "input_boolean", "light", "automation", "cover", "fan", "humidifier", "input_boolean", "light",
"media_player", "remote", "siren", "switch" "media_player", "remote", "siren", "switch"

View file

@ -8,20 +8,23 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup 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.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.google.accompanist.themeadapter.material.MdcTheme import com.google.accompanist.themeadapter.material.MdcTheme
import com.maltaisn.icondialog.IconDialog import com.mikepenz.iconics.typeface.IIcon
import com.maltaisn.icondialog.IconDialogSettings
import com.maltaisn.icondialog.pack.IconPack
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.settings.qs.views.ManageTilesView 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 import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint @AndroidEntryPoint
class ManageTilesFragment : Fragment(), IconDialog.Callback { class ManageTilesFragment : Fragment() {
companion object { companion object {
private const val TAG = "TileFragment" private const val TAG = "TileFragment"
@ -53,17 +56,24 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
return ComposeView(requireContext()).apply { return ComposeView(requireContext()).apply {
val settings = IconDialogSettings {
searchVisibility = IconDialog.SearchVisibility.ALWAYS
}
val iconDialog = IconDialog.newInstance(settings)
setContent { setContent {
MdcTheme { MdcTheme {
var showingDialog by remember { mutableStateOf(false) }
if (showingDialog) {
IconDialog(
onSelect = {
onIconDialogIconsSelected(it)
showingDialog = false
},
onDismissRequest = { showingDialog = false }
)
}
ManageTilesView( ManageTilesView(
viewModel = viewModel, viewModel = viewModel,
onShowIconDialog = { tag -> onShowIconDialog = {
iconDialog.show(childFragmentManager, tag) showingDialog = true
} }
) )
} }
@ -76,14 +86,8 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback {
activity?.title = getString(commonR.string.tiles) activity?.title = getString(commonR.string.tiles)
} }
override val iconDialogIconPack: IconPack private fun onIconDialogIconsSelected(selectedIcon: IIcon) {
get() = viewModel.iconPack Log.d(TAG, "Selected icon: ${selectedIcon.name}")
viewModel.selectIcon(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) {
viewModel.selectIcon(selectedIcon)
}
} }
} }

View file

@ -4,24 +4,19 @@ import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.app.StatusBarManager import android.app.StatusBarManager
import android.content.ComponentName import android.content.ComponentName
import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.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 dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.domain 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.Tile7Service
import io.homeassistant.companion.android.qs.Tile8Service import io.homeassistant.companion.android.qs.Tile8Service
import io.homeassistant.companion.android.qs.Tile9Service 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.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -139,8 +136,6 @@ class ManageTilesViewModel @Inject constructor(
) )
} }
lateinit var iconPack: IconPack
private val app = application private val app = application
val slots = loadTileSlots(application.resources) val slots = loadTileSlots(application.resources)
@ -154,9 +149,7 @@ class ManageTilesViewModel @Inject constructor(
private set private set
var selectedServerId by mutableStateOf(ServerManager.SERVER_ID_ACTIVE) var selectedServerId by mutableStateOf(ServerManager.SERVER_ID_ACTIVE)
private set private set
var selectedIconId by mutableStateOf<Int?>(null) var selectedIconId by mutableStateOf<String?>(null)
private set
var selectedIconDrawable by mutableStateOf(AppCompatResources.getDrawable(application, commonR.drawable.ic_stat_ic_notification))
private set private set
var selectedEntityId by mutableStateOf("") var selectedEntityId by mutableStateOf("")
var tileLabel by mutableStateOf("") var tileLabel by mutableStateOf("")
@ -165,6 +158,8 @@ class ManageTilesViewModel @Inject constructor(
private set private set
var selectedShouldVibrate by mutableStateOf(false) var selectedShouldVibrate by mutableStateOf(false)
var tileAuthRequired by mutableStateOf(false) var tileAuthRequired by mutableStateOf(false)
var selectedIcon: IIcon? = null
private var selectedTileId = 0 private var selectedTileId = 0
private var selectedTileAdded = false private var selectedTileAdded = false
@ -202,16 +197,6 @@ class ManageTilesViewModel @Inject constructor(
selectTile(slots.indexOf(selectedTile)) 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) { fun selectTile(index: Int) {
@ -259,21 +244,9 @@ class ManageTilesViewModel @Inject constructor(
if (selectedIconId == null) selectIcon(null) // trigger drawable update if (selectedIconId == null) selectIcon(null) // trigger drawable update
} }
fun selectIcon(icon: Icon?) { fun selectIcon(icon: IIcon?) {
selectedIconId = icon?.id selectedIconId = icon?.mdiName
selectedIconDrawable = if (icon != null) { selectedIcon = icon ?: sortedEntities.firstOrNull { it.entityId == selectedEntityId }?.getIcon(app)
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
}
)
}
}
}
} }
private fun updateExistingTileFields(currentTile: TileEntity) { private fun updateExistingTileFields(currentTile: TileEntity) {
@ -283,13 +256,7 @@ class ManageTilesViewModel @Inject constructor(
selectedShouldVibrate = currentTile.shouldVibrate selectedShouldVibrate = currentTile.shouldVibrate
tileAuthRequired = currentTile.authRequired tileAuthRequired = currentTile.authRequired
selectIcon( selectIcon(
currentTile.iconId?.let { currentTile.iconName?.let { CommunityMaterial.getIconByMdiName(it) }
if (::iconPack.isInitialized) {
iconPack.getIcon(it)
} else {
null
}
}
) )
} }
@ -300,7 +267,7 @@ class ManageTilesViewModel @Inject constructor(
tileId = selectedTile.id, tileId = selectedTile.id,
serverId = selectedServerId, serverId = selectedServerId,
added = selectedTileAdded, added = selectedTileAdded,
iconId = selectedIconId, iconName = selectedIconId,
entityId = selectedEntityId, entityId = selectedEntityId,
label = tileLabel, label = tileLabel,
subtitle = tileSubtitle, subtitle = tileSubtitle,
@ -315,11 +282,10 @@ class ManageTilesViewModel @Inject constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !selectedTileAdded) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !selectedTileAdded) {
val statusBarManager = app.getSystemService<StatusBarManager>() val statusBarManager = app.getSystemService<StatusBarManager>()
val service = idToTileService[selectedTile.id] ?: Tile1Service::class.java val service = idToTileService[selectedTile.id] ?: Tile1Service::class.java
val icon = selectedIconDrawable?.let { val icon = selectedIcon?.let {
it.toBitmapOrNull(it.intrinsicWidth, it.intrinsicHeight)?.let { bitmap -> val bitmap = IconicsDrawable(getApplication(), it).toBitmap()
android.graphics.drawable.Icon.createWithBitmap(bitmap) Icon.createWithBitmap(bitmap)
} } ?: Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification)
} ?: android.graphics.drawable.Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification)
statusBarManager?.requestAddTileService( statusBarManager?.requestAddTileService(
ComponentName(app, service), ComponentName(app, service),

View file

@ -1,7 +1,6 @@
package io.homeassistant.companion.android.settings.qs.views package io.homeassistant.companion.android.settings.qs.views
import android.os.Build import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -31,13 +30,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.settings.qs.ManageTilesViewModel import io.homeassistant.companion.android.settings.qs.ManageTilesViewModel
import io.homeassistant.companion.android.util.compose.ServerDropdownButton import io.homeassistant.companion.android.util.compose.ServerDropdownButton
@ -160,12 +157,9 @@ fun ManageTilesView(
OutlinedButton( OutlinedButton(
onClick = { onShowIconDialog(viewModel.selectedTile.id) } onClick = { onShowIconDialog(viewModel.selectedTile.id) }
) { ) {
val iconBitmap = remember(viewModel.selectedIconDrawable) { viewModel.selectedIcon?.let { icon ->
viewModel.selectedIconDrawable?.toBitmap()?.asImageBitmap() com.mikepenz.iconics.compose.Image(
} icon,
iconBitmap?.let {
Image(
iconBitmap,
contentDescription = stringResource(id = R.string.tile_icon), contentDescription = stringResource(id = R.string.tile_icon),
colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)), colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)

View file

@ -1,7 +1,6 @@
package io.homeassistant.companion.android.settings.shortcuts package io.homeassistant.companion.android.settings.shortcuts
import android.content.Intent import android.content.Intent
import android.graphics.PorterDuff
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -11,28 +10,24 @@ import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.RequiresApi 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.compose.ui.platform.ComposeView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.accompanist.themeadapter.material.MdcTheme import com.google.accompanist.themeadapter.material.MdcTheme
import com.maltaisn.icondialog.IconDialog import com.mikepenz.iconics.typeface.IIcon
import com.maltaisn.icondialog.IconDialogSettings
import com.maltaisn.icondialog.pack.IconPack
import com.maltaisn.icondialog.pack.IconPackLoader
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.settings.shortcuts.views.ManageShortcutsView import io.homeassistant.companion.android.settings.shortcuts.views.ManageShortcutsView
import kotlinx.coroutines.Dispatchers import io.homeassistant.companion.android.util.icondialog.IconDialog
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.R as commonR
@RequiresApi(Build.VERSION_CODES.N_MR1) @RequiresApi(Build.VERSION_CODES.N_MR1)
@AndroidEntryPoint @AndroidEntryPoint
class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback { class ManageShortcutsSettingsFragment : Fragment() {
companion object { companion object {
const val MAX_SHORTCUTS = 5 const val MAX_SHORTCUTS = 5
@ -41,19 +36,10 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
} }
val viewModel: ManageShortcutsViewModel by viewModels() val viewModel: ManageShortcutsViewModel by viewModels()
private lateinit var iconPack: IconPack
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val loader = IconPackLoader(requireContext())
iconPack = createMaterialDesignIconPack(loader)
iconPack.loadDrawables(loader.drawableLoader)
}
}
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
@ -71,14 +57,20 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
return ComposeView(requireContext()).apply { return ComposeView(requireContext()).apply {
val settings = IconDialogSettings {
searchVisibility = IconDialog.SearchVisibility.ALWAYS
}
val iconDialog = IconDialog.newInstance(settings)
setContent { setContent {
MdcTheme { 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) activity?.title = getString(commonR.string.shortcuts)
} }
override val iconDialogIconPack: IconPack private fun onIconDialogIconsSelected(tag: String, selectedIcon: IIcon) {
get() = iconPack Log.d(TAG, "Selected icon: $selectedIcon")
override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List<com.maltaisn.icondialog.data.Icon>) { val index = when (tag) {
Log.d(TAG, "Selected icon: ${icons.firstOrNull()}") "shortcut_1" -> 0
val selectedIcon = icons.firstOrNull() "shortcut_2" -> 1
if (selectedIcon != null) { "shortcut_3" -> 2
val iconDrawable = selectedIcon.drawable "shortcut_4" -> 3
if (iconDrawable != null) { "shortcut_5" -> 4
val icon = DrawableCompat.wrap(iconDrawable) else -> 5
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
}
}
}
} }
viewModel.shortcuts[index].selectedIcon.value = selectedIcon
} }
} }

View file

@ -4,35 +4,41 @@ import android.app.Application
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.graphics.Bitmap
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.drawable.Drawable import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.maltaisn.icondialog.pack.IconPack import com.mikepenz.iconics.IconicsDrawable
import com.maltaisn.icondialog.pack.IconPackLoader import com.mikepenz.iconics.IconicsSize
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack 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 dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.servers.ServerManager 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.webview.WebViewActivity
import io.homeassistant.companion.android.widgets.assist.AssistShortcutActivity import io.homeassistant.companion.android.widgets.assist.AssistShortcutActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@RequiresApi(Build.VERSION_CODES.N_MR1) @RequiresApi(Build.VERSION_CODES.N_MR1)
@ -44,14 +50,13 @@ class ManageShortcutsViewModel @Inject constructor(
val app = application val app = application
private val TAG = "ShortcutViewModel" private val TAG = "ShortcutViewModel"
private lateinit var iconPack: IconPack
private var shortcutManager = application.applicationContext.getSystemService<ShortcutManager>()!! private var shortcutManager = application.applicationContext.getSystemService<ShortcutManager>()!!
val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported
var pinnedShortcuts = shortcutManager.pinnedShortcuts var pinnedShortcuts = shortcutManager.pinnedShortcuts
.filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) } .filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) }
.toMutableList() .toMutableList()
private set private set
var dynamicShortcuts: MutableList<ShortcutInfo> = shortcutManager.dynamicShortcuts var dynamicShortcuts = mutableListOf<ShortcutInfo>()
private set private set
var servers by mutableStateOf(serverManager.defaultServers) var servers by mutableStateOf(serverManager.defaultServers)
@ -61,15 +66,16 @@ class ManageShortcutsViewModel @Inject constructor(
private val currentServerId = serverManager.getServer()?.id ?: 0 private val currentServerId = serverManager.getServer()?.id ?: 0
private val iconIdToName: Map<Int, String> by lazy { IconDialogCompat(app.assets).loadAllIcons() }
data class Shortcut( data class Shortcut(
var id: MutableState<String?>, var id: MutableState<String?>,
var serverId: MutableState<Int>, var serverId: MutableState<Int>,
var selectedIcon: MutableState<Int>, var selectedIcon: MutableState<IIcon?>,
var label: MutableState<String>, var label: MutableState<String>,
var desc: MutableState<String>, var desc: MutableState<String>,
var path: MutableState<String>, var path: MutableState<String>,
var type: MutableState<String>, var type: MutableState<String>,
var drawable: MutableState<Drawable?>,
var delete: MutableState<Boolean> var delete: MutableState<Boolean>
) )
@ -90,6 +96,7 @@ class ManageShortcutsViewModel @Inject constructor(
} }
} }
} }
updateDynamicShortcuts()
Log.d(TAG, "We have ${dynamicShortcuts.size} dynamic shortcuts") Log.d(TAG, "We have ${dynamicShortcuts.size} dynamic shortcuts")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -102,12 +109,11 @@ class ManageShortcutsViewModel @Inject constructor(
Shortcut( Shortcut(
mutableStateOf(""), mutableStateOf(""),
mutableStateOf(currentServerId), mutableStateOf(currentServerId),
mutableStateOf(0), mutableStateOf(null),
mutableStateOf(""), mutableStateOf(""),
mutableStateOf(""), mutableStateOf(""),
mutableStateOf(""), mutableStateOf(""),
mutableStateOf("lovelace"), mutableStateOf("lovelace"),
mutableStateOf(AppCompatResources.getDrawable(application, R.drawable.ic_stat_ic_notification_blue)),
mutableStateOf(false) 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") Log.d(TAG, "Attempt to add shortcut $shortcutId")
val intent = Intent( val intent = Intent(
WebViewActivity.newInstance(getApplication(), shortcutPath, serverId).addFlags( WebViewActivity.newInstance(app, shortcutPath, serverId).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK
) )
) )
intent.action = shortcutPath intent.action = shortcutPath
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) 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) .setShortLabel(shortcutLabel)
.setLongLabel(shortcutDesc) .setLongLabel(shortcutDesc)
.setIcon( .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) Icon.createWithBitmap(bitmap)
} else { } else {
Icon.createWithResource(getApplication(), R.drawable.ic_stat_ic_notification_blue) Icon.createWithResource(app, R.drawable.ic_stat_ic_notification_blue)
} }
) )
.setIntent(intent) .setIntent(intent)
@ -146,7 +157,7 @@ class ManageShortcutsViewModel @Inject constructor(
if (shortcutId.startsWith("shortcut")) { if (shortcutId.startsWith("shortcut")) {
shortcutManager.addDynamicShortcuts(listOf(shortcut)) shortcutManager.addDynamicShortcuts(listOf(shortcut))
dynamicShortcuts = shortcutManager.dynamicShortcuts updateDynamicShortcuts()
} else { } else {
var isNewPinned = true var isNewPinned = true
for (item in pinnedShortcuts) { for (item in pinnedShortcuts) {
@ -154,7 +165,7 @@ class ManageShortcutsViewModel @Inject constructor(
isNewPinned = false isNewPinned = false
Log.d(TAG, "Updating pinned shortcut: $shortcutId") Log.d(TAG, "Updating pinned shortcut: $shortcutId")
shortcutManager.updateShortcuts(listOf(shortcut)) 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) { fun deleteShortcut(shortcutId: String) {
shortcutManager.removeDynamicShortcuts(listOf(shortcutId)) shortcutManager.removeDynamicShortcuts(listOf(shortcutId))
dynamicShortcuts = shortcutManager.dynamicShortcuts updateDynamicShortcuts()
} }
fun setPinnedShortcutData(shortcutId: String) { fun setPinnedShortcutData(shortcutId: String) = viewModelScope.launch {
for (item in pinnedShortcuts) { for (item in pinnedShortcuts) {
if (item.id == shortcutId) { if (item.id == shortcutId) {
shortcuts.last().id.value = item.id shortcuts.last().id.value = item.id
shortcuts.last().serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId shortcuts.last().setData(item)
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"
}
} }
} }
} }
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()) { if (dynamicShortcuts.isNotEmpty()) {
for (item in dynamicShortcuts) { for (item in dynamicShortcuts) {
if (item.id == shortcutId) { if (item.id == shortcutId) {
Log.d(TAG, "setting ${item.id} data") Log.d(TAG, "setting ${item.id} data")
shortcuts[index].serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId shortcuts[index].setData(item)
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"
}
} }
} }
} }
} }
private fun getTileIcon(tileIconId: Int): Drawable? { private suspend fun Shortcut.setData(item: ShortcutInfo) {
val loader = IconPackLoader(getApplication()) serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId
iconPack = createMaterialDesignIconPack(loader) label.value = item.shortLabel.toString()
iconPack.loadDrawables(loader.drawableLoader) desc.value = item.longLabel.toString()
val iconDrawable = iconPack.icons[tileIconId]?.drawable path.value = item.intent?.action.toString()
if (iconDrawable != null) { selectedIcon.value = if (item.intent?.extras?.containsKey("iconName") == true) {
val icon = DrawableCompat.wrap(iconDrawable) item.intent?.extras?.getString("iconName")?.let { CommunityMaterial.getIconByMdiName(it) }
icon.setColorFilter(app.resources.getColor(R.color.colorAccent), PorterDuff.Mode.SRC_IN) } else if (item.intent?.extras?.containsKey("iconId") == true) {
return icon 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() { fun updatePinnedShortcuts() {

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Divider import androidx.compose.material.Divider
@ -25,16 +26,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.DrawableCompat import com.mikepenz.iconics.compose.IconicsPainter
import androidx.core.graphics.drawable.toBitmap
import androidx.fragment.app.FragmentManager
import com.maltaisn.icondialog.IconDialog
import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment
import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsViewModel import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsViewModel
@ -44,8 +42,7 @@ import io.homeassistant.companion.android.util.compose.ServerDropdownButton
@Composable @Composable
fun ManageShortcutsView( fun ManageShortcutsView(
viewModel: ManageShortcutsViewModel, viewModel: ManageShortcutsViewModel,
iconDialog: IconDialog, showIconDialog: (tag: String) -> Unit
childFragment: FragmentManager
) { ) {
LazyColumn(contentPadding = PaddingValues(16.dp)) { LazyColumn(contentPadding = PaddingValues(16.dp)) {
item { item {
@ -67,8 +64,7 @@ fun ManageShortcutsView(
CreateShortcutView( CreateShortcutView(
i = i, i = i,
viewModel = viewModel, viewModel = viewModel,
iconDialog = iconDialog, showIconDialog = showIconDialog
childFragment = childFragment
) )
} }
} }
@ -76,7 +72,11 @@ fun ManageShortcutsView(
@RequiresApi(Build.VERSION_CODES.N_MR1) @RequiresApi(Build.VERSION_CODES.N_MR1)
@Composable @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 val context = LocalContext.current
var expandedEntity by remember { mutableStateOf(false) } var expandedEntity by remember { mutableStateOf(false) }
var expandedPinnedShortcuts 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) modifier = Modifier.padding(end = 10.dp)
) )
OutlinedButton(onClick = { OutlinedButton(onClick = {
iconDialog.show(childFragment, shortcutId) showIconDialog(shortcutId)
}) { }) {
val icon = viewModel.shortcuts[i].drawable.value?.let { DrawableCompat.wrap(it) } val icon = viewModel.shortcuts[i].selectedIcon.value
icon?.toBitmap()?.asImageBitmap() val painter = if (icon != null) {
?.let { remember(icon) { IconicsPainter(icon) }
Image( } else {
it, painterResource(R.drawable.ic_stat_ic_notification_blue)
contentDescription = stringResource(id = R.string.shortcut_icon), }
colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent))
) 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.label.value,
shortcut.desc.value, shortcut.desc.value,
shortcut.path.value, shortcut.path.value,
shortcut.drawable.value?.toBitmap(),
shortcut.selectedIcon.value shortcut.selectedIcon.value
) )
}, },

View file

@ -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
}
}

View file

@ -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 = {})
}
}
}

View file

@ -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
}
}

View file

@ -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 = {}
)
}
}
}

View file

@ -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 = {})
}
}
}

View file

@ -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 }
}
}

View file

@ -21,9 +21,11 @@ import androidx.core.graphics.toColorInt
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.maltaisn.icondialog.pack.IconPack import com.mikepenz.iconics.IconicsDrawable
import com.maltaisn.icondialog.pack.IconPackLoader import com.mikepenz.iconics.IconicsSize
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack 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 dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.servers.ServerManager 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.ButtonWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.util.getAttribute import io.homeassistant.companion.android.util.getAttribute
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -56,7 +59,7 @@ class ButtonWidget : AppWidgetProvider() {
internal const val EXTRA_SERVICE = "EXTRA_SERVICE" internal const val EXTRA_SERVICE = "EXTRA_SERVICE"
internal const val EXTRA_SERVICE_DATA = "EXTRA_SERVICE_DATA" internal const val EXTRA_SERVICE_DATA = "EXTRA_SERVICE_DATA"
internal const val EXTRA_LABEL = "EXTRA_LABEL" 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_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR" internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR"
internal const val EXTRA_REQUIRE_AUTHENTICATION = "EXTRA_REQUIRE_AUTHENTICATION" internal const val EXTRA_REQUIRE_AUTHENTICATION = "EXTRA_REQUIRE_AUTHENTICATION"
@ -71,8 +74,6 @@ class ButtonWidget : AppWidgetProvider() {
@Inject @Inject
lateinit var buttonWidgetDao: ButtonWidgetDao lateinit var buttonWidgetDao: ButtonWidgetDao
private var iconPack: IconPack? = null
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
override fun onUpdate( override fun onUpdate(
@ -176,12 +177,6 @@ class ButtonWidget : AppWidgetProvider() {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 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() 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 { return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_button_wrapper_dynamiccolor else R.layout.widget_button_wrapper_default).apply {
// Theming // Theming
@ -196,39 +191,41 @@ class ButtonWidget : AppWidgetProvider() {
setLabelVisibility(this, widget) setLabelVisibility(this, widget)
// Content // 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 val iconDrawable = IconicsDrawable(context, iconData).apply {
if (iconDrawable != null) { padding = IconicsSize.dp(2)
val icon = DrawableCompat.wrap(iconDrawable) size = IconicsSize.dp(24)
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 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( setOnClickPendingIntent(
R.id.widgetImageButtonLayout, R.id.widgetImageButtonLayout,
@ -364,7 +361,7 @@ class ButtonWidget : AppWidgetProvider() {
val serviceData: String? = extras.getString(EXTRA_SERVICE_DATA) val serviceData: String? = extras.getString(EXTRA_SERVICE_DATA)
val label: String? = extras.getString(EXTRA_LABEL) val label: String? = extras.getString(EXTRA_LABEL)
val requireAuthentication: Boolean = extras.getBoolean(EXTRA_REQUIRE_AUTHENTICATION) 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 backgroundType: WidgetBackgroundType = extras.getSerializable(EXTRA_BACKGROUND_TYPE) as WidgetBackgroundType
val textColor: String? = extras.getString(EXTRA_TEXT_COLOR) val textColor: String? = extras.getString(EXTRA_TEXT_COLOR)

View file

@ -5,6 +5,8 @@ import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@ -21,21 +23,18 @@ import android.widget.Spinner
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.maltaisn.icondialog.IconDialog import com.mikepenz.iconics.IconicsDrawable
import com.maltaisn.icondialog.IconDialogSettings import com.mikepenz.iconics.typeface.IIcon
import com.maltaisn.icondialog.data.Icon import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.maltaisn.icondialog.pack.IconPack
import com.maltaisn.icondialog.pack.IconPackLoader
import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack
import dagger.hilt.android.AndroidEntryPoint 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.Entity
import io.homeassistant.companion.android.common.data.integration.Service import io.homeassistant.companion.android.common.data.integration.Service
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao 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.databinding.WidgetButtonConfigureBinding
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
import io.homeassistant.companion.android.util.getHexForColor 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.BaseWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.common.ServiceFieldBinder import io.homeassistant.companion.android.widgets.common.ServiceFieldBinder
import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter 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 import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint @AndroidEntryPoint
class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.Callback { class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() {
companion object { companion object {
private const val TAG: String = "ButtonWidgetConfigAct" 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" 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 lateinit var buttonWidgetDao: ButtonWidgetDao
override val dao get() = buttonWidgetDao override val dao get() = buttonWidgetDao
private lateinit var iconPack: IconPack
private var services = mutableMapOf<Int, HashMap<String, Service>>() private var services = mutableMapOf<Int, HashMap<String, Service>>()
private var entities = mutableMapOf<Int, HashMap<String, Entity<Any>>>() private var entities = mutableMapOf<Int, HashMap<String, Entity<Any>>>()
private var dynamicFields = ArrayList<ServiceFieldBinder>() private var dynamicFields = ArrayList<ServiceFieldBinder>()
@ -258,9 +257,6 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
setupServerSelect(buttonWidget?.serverId) setupServerSelect(buttonWidget?.serverId)
// Create an icon pack loader with application context.
val loader = IconPackLoader(this)
serviceAdapter = SingleItemArrayAdapter<Service>(this) { serviceAdapter = SingleItemArrayAdapter<Service>(this) {
if (it != null) getServiceString(it) else "" 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... // Do this off the main thread, takes a second or two...
runOnUiThread { runOnUiThread {
// Create an icon pack and load all drawables. // Create an icon pack and load all drawables.
iconPack = createMaterialDesignIconPack(loader) val iconName = buttonWidget?.iconName ?: "mdi:flash"
iconPack.loadDrawables(loader.drawableLoader) val icon = CommunityMaterial.getIconByMdiName(iconName) ?: CommunityMaterial.Icon2.cmd_flash
val settings = IconDialogSettings { onIconDialogIconsSelected(icon)
searchVisibility = IconDialog.SearchVisibility.ALWAYS
}
val iconDialog = IconDialog.newInstance(settings)
val iconId = buttonWidget?.iconId ?: 62017
onIconDialogIconsSelected(iconDialog, listOf(iconPack.icons[iconId]!!))
binding.widgetConfigIconSelector.setOnClickListener { 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? private fun onIconDialogIconsSelected(selectedIcon: IIcon) {
get() = iconPack 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>) { binding.widgetConfigIconSelector.setImageBitmap(iconDrawable.toBitmap())
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())
}
}
} }
private fun onAddWidget() { private fun onAddWidget() {
@ -459,8 +448,8 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.
binding.label.text.toString() binding.label.text.toString()
) )
intent.putExtra( intent.putExtra(
ButtonWidget.EXTRA_ICON, ButtonWidget.EXTRA_ICON_NAME,
binding.widgetConfigIconSelector.tag as Int binding.widgetConfigIconSelector.tag as String
) )
// Analyze and send service data // Analyze and send service data

View file

@ -45,6 +45,9 @@ android {
java { java {
srcDirs("../app/src/main/java") srcDirs("../app/src/main/java")
} }
assets {
srcDirs("../app/src/main/assets")
}
res { res {
srcDirs("../app/src/main/res") srcDirs("../app/src/main/res")
} }
@ -169,8 +172,6 @@ dependencies {
implementation("com.github.Dimezis:BlurView:version-1.6.6") implementation("com.github.Dimezis:BlurView:version-1.6.6")
implementation("org.altbeacon:android-beacon-library:2.19.5") 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-stdlib:1.8.22")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")

View file

@ -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')"
]
}
}

View file

@ -1,9 +1,12 @@
package io.homeassistant.companion.android.database package io.homeassistant.companion.android.database
import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.res.AssetManager
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
@ -14,6 +17,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.database.getStringOrNull
import androidx.room.AutoMigration import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.OnConflictStrategy 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 io.homeassistant.companion.android.database.widget.WidgetBackgroundTypeConverter
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.UUID import java.util.UUID
import kotlin.collections.ArrayList
import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.R as commonR
@Database( @Database(
@ -87,7 +90,7 @@ import io.homeassistant.companion.android.common.R as commonR
Server::class, Server::class,
Setting::class Setting::class
], ],
version = 40, version = 41,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 24, to = 25), AutoMigration(from = 24, to = 25),
AutoMigration(from = 25, to = 26), AutoMigration(from = 25, to = 26),
@ -175,12 +178,25 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_20_21, MIGRATION_20_21,
MIGRATION_21_22, MIGRATION_21_22,
MIGRATION_22_23, MIGRATION_22_23,
MIGRATION_23_24 MIGRATION_23_24,
Migration40to41(context.assets)
) )
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .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) { private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL( database.execSQL(
@ -210,22 +226,22 @@ abstract class AppDatabase : RoomDatabase() {
} }
private val MIGRATION_5_6 = object : Migration(5, 6) { private val MIGRATION_5_6 = object : Migration(5, 6) {
@SuppressLint("Range")
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
try { try {
val contentValues: ArrayList<ContentValues> = ArrayList()
val widgets = database.query("SELECT * FROM `static_widget`") val widgets = database.query("SELECT * FROM `static_widget`")
widgets.use { widgets.use {
if (widgets.count > 0) { if (widgets.count > 0) {
while (widgets.moveToNext()) { val contentValues = widgets.map { widgets ->
val cv = ContentValues() ContentValues().apply {
cv.put("id", widgets.getInt(widgets.getColumnIndex("id"))) put("id", widgets.getInt(widgets.getColumnIndex("id")))
cv.put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id"))) put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id")))
cv.put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id"))) put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id")))
cv.put("label", widgets.getString(widgets.getColumnIndex("label"))) put("label", widgets.getString(widgets.getColumnIndex("label")))
cv.put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size"))) put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size")))
cv.put("state_separator", widgets.getString(widgets.getColumnIndex("separator"))) put("state_separator", widgets.getString(widgets.getColumnIndex("separator")))
cv.put("attribute_separator", " ") put("attribute_separator", " ")
contentValues.add(cv) }
} }
database.execSQL("DROP TABLE IF EXISTS `static_widget`") 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`))") 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) { private val MIGRATION_6_7 = object : Migration(6, 7) {
@SuppressLint("Range")
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM sensors")
val sensors = mutableListOf<ContentValues>()
var migrationSuccessful = false
var migrationFailed = false var migrationFailed = false
try { val sensors = try {
if (cursor.moveToFirst()) { database.query("SELECT * FROM sensors").use { cursor ->
while (cursor.moveToNext()) { cursor.map {
sensors.add( ContentValues().also {
ContentValues().also { it.put("id", cursor.getString(cursor.getColumnIndex("unique_id")))
it.put("id", cursor.getString(cursor.getColumnIndex("unique_id"))) it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) it.put(
it.put( "registered",
"registered", cursor.getInt(cursor.getColumnIndex("registered"))
cursor.getInt(cursor.getColumnIndex("registered")) )
) it.put("state", "")
it.put("state", "") it.put("state_type", "")
it.put("state_type", "") it.put("type", "")
it.put("type", "") it.put("icon", "")
it.put("icon", "") it.put("name", "")
it.put("name", "") it.put("device_class", "")
it.put("device_class", "") }
}
)
} }
migrationSuccessful = true
} }
cursor.close()
} catch (e: Exception) { } catch (e: Exception) {
migrationFailed = true migrationFailed = true
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
null
} }
database.execSQL("DROP TABLE IF EXISTS `sensors`") 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`))") 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 { sensors?.forEach {
database.insert("sensors", OnConflictStrategy.REPLACE, it) database.insert("sensors", OnConflictStrategy.REPLACE, it)
}
} }
if (migrationFailed) { if (migrationFailed) {
notifyMigrationFailed() notifyMigrationFailed()
@ -305,44 +315,38 @@ abstract class AppDatabase : RoomDatabase() {
} }
private val MIGRATION_9_10 = object : Migration(9, 10) { private val MIGRATION_9_10 = object : Migration(9, 10) {
@SuppressLint("Range")
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM sensors")
val sensors = mutableListOf<ContentValues>()
var migrationSuccessful = false
var migrationFailed = false var migrationFailed = false
try { val sensors = try {
if (cursor.moveToFirst()) { database.query("SELECT * FROM sensors").use { cursor ->
while (cursor.moveToNext()) { cursor.map {
sensors.add( ContentValues().also {
ContentValues().also { it.put("id", cursor.getString(cursor.getColumnIndex("id")))
it.put("id", cursor.getString(cursor.getColumnIndex("id"))) it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) it.put(
it.put( "registered",
"registered", cursor.getInt(cursor.getColumnIndex("registered"))
cursor.getInt(cursor.getColumnIndex("registered")) )
) it.put("state", "")
it.put("state", "") it.put("last_sent_state", "")
it.put("last_sent_state", "") it.put("state_type", "")
it.put("state_type", "") it.put("type", "")
it.put("type", "") it.put("icon", "")
it.put("icon", "") it.put("name", "")
it.put("name", "") }
}
)
} }
migrationSuccessful = true
} }
cursor.close()
} catch (e: Exception) { } catch (e: Exception) {
migrationFailed = true migrationFailed = true
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
null
} }
database.execSQL("DROP TABLE IF EXISTS `sensors`") 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`))") 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 { sensors?.forEach {
database.insert("sensors", OnConflictStrategy.REPLACE, it) database.insert("sensors", OnConflictStrategy.REPLACE, it)
}
} }
if (migrationFailed) { if (migrationFailed) {
notifyMigrationFailed() notifyMigrationFailed()
@ -388,6 +392,7 @@ abstract class AppDatabase : RoomDatabase() {
} }
private val MIGRATION_16_17 = object : Migration(16, 17) { private val MIGRATION_16_17 = object : Migration(16, 17) {
@SuppressLint("Range")
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM sensor_settings") val cursor = database.query("SELECT * FROM sensor_settings")
val sensorSettings = mutableListOf<ContentValues>() val sensorSettings = mutableListOf<ContentValues>()
@ -472,19 +477,17 @@ abstract class AppDatabase : RoomDatabase() {
} }
) )
} }
migrationSuccessful = true
} }
cursor.close()
} catch (e: Exception) { } catch (e: Exception) {
migrationFailed = true migrationFailed = true
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
null
} }
database.execSQL("DROP TABLE IF EXISTS `sensor_settings`") 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`))") 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 { sensorSettings?.forEach {
database.insert("sensor_settings", OnConflictStrategy.REPLACE, it) database.insert("sensor_settings", OnConflictStrategy.REPLACE, it)
}
} }
if (migrationFailed) { if (migrationFailed) {
notifyMigrationFailed() 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() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = appContext.getSystemService<NotificationManager>()!! val notificationManager = appContext.getSystemService<NotificationManager>()!!

View file

@ -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
}
}

View file

@ -14,8 +14,9 @@ data class TileEntity(
val added: Boolean, val added: Boolean,
@ColumnInfo(name = "server_id", defaultValue = "0") @ColumnInfo(name = "server_id", defaultValue = "0")
val serverId: Int, val serverId: Int,
@ColumnInfo(name = "icon_id") /** Icon name, such as "mdi:account-alert" */
val iconId: Int?, @ColumnInfo(name = "icon_name")
val iconName: String?,
@ColumnInfo(name = "entity_id") @ColumnInfo(name = "entity_id")
val entityId: String, val entityId: String,
@ColumnInfo(name = "label") @ColumnInfo(name = "label")

View file

@ -10,8 +10,8 @@ data class ButtonWidgetEntity(
override val id: Int, override val id: Int,
@ColumnInfo(name = "server_id", defaultValue = "0") @ColumnInfo(name = "server_id", defaultValue = "0")
override val serverId: Int, override val serverId: Int,
@ColumnInfo(name = "icon_id") @ColumnInfo(name = "icon_name")
val iconId: Int, val iconName: String,
@ColumnInfo(name = "domain") @ColumnInfo(name = "domain")
val domain: String, val domain: String,
@ColumnInfo(name = "service") @ColumnInfo(name = "service")

View file

@ -120,8 +120,9 @@
<string name="choose_entity">Choose entity</string> <string name="choose_entity">Choose entity</string>
<string name="choose_server">Choose server</string> <string name="choose_server">Choose server</string>
<string name="clear_favorites">Clear Favorites</string> <string name="clear_favorites">Clear Favorites</string>
<string name="clear_search">Clear search</string>
<string name="close">Close</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="collapse">Collapse</string>
<string name="complication_entity_invalid">Invalid entity</string> <string name="complication_entity_invalid">Invalid entity</string>
<string name="complication_entity_state_content_description">Entity state</string> <string name="complication_entity_state_content_description">Entity state</string>
@ -486,6 +487,8 @@
<string name="scene">Scene</string> <string name="scene">Scene</string>
<string name="scenes">Scenes</string> <string name="scenes">Scenes</string>
<string name="scripts">Scripts</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_notifications">Search notifications</string>
<string name="search_results">Search Results</string> <string name="search_results">Search Results</string>
<string name="search_sensors">Search sensors</string> <string name="search_sensors">Search sensors</string>