mirror of
https://github.com/home-assistant/android
synced 2024-07-23 19:34:23 +00:00
Jetpack Compose icon dialog (#3631)
* Add icon dialog based on Jetpack Compose TODO * Migrate to new icon dialog * Migrate old database * Don't wrap with drawablecompat * Rebase fixes and updates - Fix and update database migration - Fix dependencies - Fix shortcut icons - Fix ComposeView in AlertDialog not working by switching implementation to DialogFragment - Fix icons that no longer exist - ktlint * Visual compatibility - Automotive asset - Handle icon ids in shortcuts to prevent users losing icons when updating shortcuts - Add padding, color filter to shortcut icons to keep icons consistent with older icons - Increase button widget icon padding to keep sizing consistent - Add tip to dialog about searching in non-English languages * Fix line endings --------- Co-authored-by: Tiger Oakes <contact@tigeroakes.com>
This commit is contained in:
parent
938c515549
commit
d1b17aa606
|
@ -143,8 +143,6 @@ dependencies {
|
||||||
|
|
||||||
implementation("com.github.Dimezis:BlurView:version-1.6.6")
|
implementation("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")
|
||||||
|
|
1
app/src/main/assets/mdi_id_map.json
Normal file
1
app/src/main/assets/mdi_id_map.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -13,12 +13,8 @@ import android.util.Log
|
||||||
import android.widget.Toast
|
import 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"
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
|
|
||||||
|
const val MDI_PREFIX = "mdi:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the MDI name of an Iconics icon.
|
||||||
|
* MDI format is used by Home Assistant (ie "mdi:account-alert"),
|
||||||
|
* compared to Iconic's [IIcon.name] format (ie "cmd_account_alert").
|
||||||
|
*/
|
||||||
|
val IIcon.mdiName: String
|
||||||
|
get() = name.replace("${CommunityMaterial.mappingPrefix}_", MDI_PREFIX).replace('_', '-')
|
||||||
|
|
||||||
|
fun CommunityMaterial.getIconByMdiName(mdiName: String): IIcon? {
|
||||||
|
val name = mdiName.replace(MDI_PREFIX, "${mappingPrefix}_").replace('-', '_')
|
||||||
|
return try {
|
||||||
|
getIcon(name)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// Icon doesn't exist (anymore)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||||
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconDialogContent(
|
||||||
|
iconFilter: IconFilter = DefaultIconFilter(),
|
||||||
|
onSelect: (IIcon) -> Unit
|
||||||
|
) {
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
Column {
|
||||||
|
IconDialogSearch(value = searchQuery, onValueChange = { searchQuery = it })
|
||||||
|
IconDialogGrid(
|
||||||
|
typeface = CommunityMaterial,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
iconFilter = iconFilter,
|
||||||
|
onClick = onSelect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconDialog(
|
||||||
|
iconFilter: IconFilter = DefaultIconFilter(),
|
||||||
|
onSelect: (IIcon) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(480.dp)
|
||||||
|
.height(500.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
IconDialogContent(iconFilter = iconFilter, onSelect = onSelect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun IconDialogPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(480.dp)
|
||||||
|
.height(500.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
IconDialogContent(onSelect = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||||
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class IconDialogFragment(callback: (IIcon) -> Unit) : DialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "IconDialogFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onSelect = callback
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return ComposeView(requireContext()).also {
|
||||||
|
it.clipToPadding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
(view as ComposeView).setContent {
|
||||||
|
MdcTheme {
|
||||||
|
IconDialogContent(
|
||||||
|
onSelect = onSelect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val params = dialog?.window?.attributes ?: return
|
||||||
|
params.width = min(
|
||||||
|
(resources.displayMetrics.widthPixels * 0.9).toInt(),
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 480f, resources.displayMetrics).toInt()
|
||||||
|
)
|
||||||
|
params.height = min(
|
||||||
|
(resources.displayMetrics.heightPixels * 0.9).toInt(),
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 500f, resources.displayMetrics).toInt()
|
||||||
|
)
|
||||||
|
dialog?.window?.attributes = params
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||||
|
import com.mikepenz.iconics.compose.Image
|
||||||
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
|
import com.mikepenz.iconics.typeface.ITypeface
|
||||||
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a grid of icons, letting the user select one.
|
||||||
|
* @param icons List of icons to display
|
||||||
|
* @param onClick Invoked when the user clicks on the given icon
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun IconDialogGrid(
|
||||||
|
icons: List<IIcon>,
|
||||||
|
tint: Color = MaterialTheme.colors.onSurface,
|
||||||
|
onClick: (IIcon) -> Unit
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(icons) { icon ->
|
||||||
|
IconButton(onClick = { onClick(icon) }) {
|
||||||
|
Image(
|
||||||
|
asset = icon,
|
||||||
|
colorFilter = ColorFilter.tint(tint),
|
||||||
|
// https://material.io/design/iconography/system-icons.html#color
|
||||||
|
alpha = 0.54f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a grid of icons, letting the user select one.
|
||||||
|
* @param typeface Icon typeface that includes all possible icons.
|
||||||
|
* @param searchQuery Search term used to filter icons from the [typeface].
|
||||||
|
* @param iconFilter Adjust filtering logic for the search process.
|
||||||
|
* @param onClick Invoked when the user clicks on the given icon
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun IconDialogGrid(
|
||||||
|
typeface: ITypeface,
|
||||||
|
searchQuery: String,
|
||||||
|
iconFilter: IconFilter = DefaultIconFilter(),
|
||||||
|
tint: Color = MaterialTheme.colors.onSurface,
|
||||||
|
onClick: (IIcon) -> Unit
|
||||||
|
) {
|
||||||
|
var icons by remember { mutableStateOf<List<IIcon>>(emptyList()) }
|
||||||
|
LaunchedEffect(typeface, searchQuery) {
|
||||||
|
icons = withContext(Dispatchers.IO) { iconFilter.queryIcons(typeface, searchQuery) }
|
||||||
|
}
|
||||||
|
|
||||||
|
IconDialogGrid(icons = icons, tint = tint, onClick = onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun IconDialogGridPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(480.dp)
|
||||||
|
.height(500.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
IconDialogGrid(
|
||||||
|
icons = CommunityMaterial.icons.map { name -> CommunityMaterial.getIcon(name) },
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||||
|
import io.homeassistant.companion.android.common.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconDialogSearch(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val isEnglish by remember { mutableStateOf(Locale.current.language == "en") }
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
singleLine = true,
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(if (isEnglish) R.string.search_icons else R.string.search_icons_in_english))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Filled.Search, contentDescription = null)
|
||||||
|
},
|
||||||
|
trailingIcon = if (value.isNotBlank()) {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onValueChange("") }) {
|
||||||
|
Icon(Icons.Filled.Clear, contentDescription = stringResource(R.string.clear_search))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun IconDialogSearchPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
Surface {
|
||||||
|
IconDialogSearch(value = "account", onValueChange = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package io.homeassistant.companion.android.util.icondialog
|
||||||
|
|
||||||
|
import com.mikepenz.iconics.typeface.IIcon
|
||||||
|
import com.mikepenz.iconics.typeface.ITypeface
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize [this] string, removing all diacritics, all unicode characters, hyphens,
|
||||||
|
* apostrophes and more. Resulting text has only lowercase latin letters and digits.
|
||||||
|
*/
|
||||||
|
private fun String.normalize(locale: Locale): String {
|
||||||
|
val normalized = this.lowercase(locale).trim().let { Normalizer.normalize(it, Normalizer.Form.NFKD) }
|
||||||
|
return normalized.filter { c -> c in 'a'..'z' || c in 'а'..'я' || c in '0'..'9' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to filter the icons for search and sort them afterwards.
|
||||||
|
* Icon filter must be parcelable to be put in bundle.
|
||||||
|
*/
|
||||||
|
interface IconFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all matching icons for a search [query], in no specific order.
|
||||||
|
*/
|
||||||
|
fun queryIcons(pack: ITypeface, query: String? = null): List<IIcon>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultIconFilter(
|
||||||
|
/**
|
||||||
|
* Regex used to split the query into multiple search terms.
|
||||||
|
* Can also be null to not split the query.
|
||||||
|
*/
|
||||||
|
private val termSplitPattern: Regex? = """[;,\s]""".toRegex(),
|
||||||
|
/**
|
||||||
|
* Whether to normalize search query or not, using [String.normalize].
|
||||||
|
*/
|
||||||
|
private val queryNormalized: Boolean = true
|
||||||
|
) : IconFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all matching icons for a search [query].
|
||||||
|
* Base implementation only returns the complete list of icons in the pack,
|
||||||
|
* sorted by ID. Subclasses take care of actual searching and must always ensure
|
||||||
|
* that the returned list is sorted by ID.
|
||||||
|
*/
|
||||||
|
override fun queryIcons(pack: ITypeface, query: String?): List<IIcon> {
|
||||||
|
val icons = pack.icons
|
||||||
|
|
||||||
|
if (query == null || query.isBlank()) {
|
||||||
|
// No search query, return all icons.
|
||||||
|
return icons.map { key -> pack.getIcon(key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split query into terms.
|
||||||
|
val trimmedQuery = query.trim()
|
||||||
|
val terms = (termSplitPattern?.let { trimmedQuery.split(it) } ?: listOf(trimmedQuery))
|
||||||
|
.map {
|
||||||
|
if (queryNormalized) {
|
||||||
|
it.normalize(Locale.ROOT)
|
||||||
|
} else {
|
||||||
|
it.lowercase(Locale.ROOT)
|
||||||
|
}
|
||||||
|
}.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
// Remove all icons that don't match any of the search terms.
|
||||||
|
return icons
|
||||||
|
.filter { icon -> matchesSearch(icon, terms) }
|
||||||
|
.map { key -> pack.getIcon(key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an [icon] name matches any of the search [terms].
|
||||||
|
*/
|
||||||
|
private fun matchesSearch(icon: String, terms: List<String>): Boolean {
|
||||||
|
val name = if (queryNormalized) icon.normalize(Locale.ROOT) else icon
|
||||||
|
return terms.any { it in name }
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,9 +21,11 @@ import androidx.core.graphics.toColorInt
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -0,0 +1,994 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 41,
|
||||||
|
"identityHash": "5ccc437900bf203ab7db2e782ffbd7f9",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "sensor_attributes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "sensorId",
|
||||||
|
"columnName": "sensor_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "valueType",
|
||||||
|
"columnName": "value_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sensor_id",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "authentication_list",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`host`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "host",
|
||||||
|
"columnName": "host",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"host"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sensors",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`, `server_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "registered",
|
||||||
|
"columnName": "registered",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "state",
|
||||||
|
"columnName": "state",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSentState",
|
||||||
|
"columnName": "last_sent_state",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastSentIcon",
|
||||||
|
"columnName": "last_sent_icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "stateType",
|
||||||
|
"columnName": "state_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceClass",
|
||||||
|
"columnName": "device_class",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unitOfMeasurement",
|
||||||
|
"columnName": "unit_of_measurement",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "stateClass",
|
||||||
|
"columnName": "state_class",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityCategory",
|
||||||
|
"columnName": "entity_category",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coreRegistration",
|
||||||
|
"columnName": "core_registration",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "appRegistration",
|
||||||
|
"columnName": "app_registration",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"server_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sensor_settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "sensorId",
|
||||||
|
"columnName": "sensor_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "valueType",
|
||||||
|
"columnName": "value_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entries",
|
||||||
|
"columnName": "entries",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sensor_id",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "button_widgets",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconName",
|
||||||
|
"columnName": "icon_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "service",
|
||||||
|
"columnName": "service",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceData",
|
||||||
|
"columnName": "service_data",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "label",
|
||||||
|
"columnName": "label",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "backgroundType",
|
||||||
|
"columnName": "background_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'DAYNIGHT'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textColor",
|
||||||
|
"columnName": "text_color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "requireAuthentication",
|
||||||
|
"columnName": "require_authentication",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "camera_widgets",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityId",
|
||||||
|
"columnName": "entity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "media_player_controls_widgets",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityId",
|
||||||
|
"columnName": "entity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "label",
|
||||||
|
"columnName": "label",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showSkip",
|
||||||
|
"columnName": "show_skip",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showSeek",
|
||||||
|
"columnName": "show_seek",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showVolume",
|
||||||
|
"columnName": "show_volume",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showSource",
|
||||||
|
"columnName": "show_source",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "backgroundType",
|
||||||
|
"columnName": "background_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'DAYNIGHT'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textColor",
|
||||||
|
"columnName": "text_color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "static_widget",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityId",
|
||||||
|
"columnName": "entity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attributeIds",
|
||||||
|
"columnName": "attribute_ids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "label",
|
||||||
|
"columnName": "label",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textSize",
|
||||||
|
"columnName": "text_size",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "stateSeparator",
|
||||||
|
"columnName": "state_separator",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attributeSeparator",
|
||||||
|
"columnName": "attribute_separator",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdate",
|
||||||
|
"columnName": "last_update",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "backgroundType",
|
||||||
|
"columnName": "background_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'DAYNIGHT'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textColor",
|
||||||
|
"columnName": "text_color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "template_widgets",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "template",
|
||||||
|
"columnName": "template",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textSize",
|
||||||
|
"columnName": "text_size",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "12.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdate",
|
||||||
|
"columnName": "last_update",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "backgroundType",
|
||||||
|
"columnName": "background_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "'DAYNIGHT'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textColor",
|
||||||
|
"columnName": "text_color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "notification_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL, `server_id` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "received",
|
||||||
|
"columnName": "received",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "message",
|
||||||
|
"columnName": "message",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "data",
|
||||||
|
"columnName": "data",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "qs_tiles",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tileId",
|
||||||
|
"columnName": "tile_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "added",
|
||||||
|
"columnName": "added",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "server_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "iconName",
|
||||||
|
"columnName": "icon_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityId",
|
||||||
|
"columnName": "entity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "label",
|
||||||
|
"columnName": "label",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitle",
|
||||||
|
"columnName": "subtitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shouldVibrate",
|
||||||
|
"columnName": "should_vibrate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authRequired",
|
||||||
|
"columnName": "auth_required",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favorites",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favorite_cache",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "friendlyName",
|
||||||
|
"columnName": "friendly_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "entity_state_complications",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entityId",
|
||||||
|
"columnName": "entity_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showTitle",
|
||||||
|
"columnName": "show_title",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `prioritize_internal` INTEGER NOT NULL, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "_name",
|
||||||
|
"columnName": "_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "nameOverride",
|
||||||
|
"columnName": "name_override",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "_version",
|
||||||
|
"columnName": "_version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "listOrder",
|
||||||
|
"columnName": "list_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceName",
|
||||||
|
"columnName": "device_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.externalUrl",
|
||||||
|
"columnName": "external_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.internalUrl",
|
||||||
|
"columnName": "internal_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.cloudUrl",
|
||||||
|
"columnName": "cloud_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.webhookId",
|
||||||
|
"columnName": "webhook_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.secret",
|
||||||
|
"columnName": "secret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.cloudhookUrl",
|
||||||
|
"columnName": "cloudhook_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.useCloud",
|
||||||
|
"columnName": "use_cloud",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.internalSsids",
|
||||||
|
"columnName": "internal_ssids",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "connection.prioritizeInternal",
|
||||||
|
"columnName": "prioritize_internal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "session.accessToken",
|
||||||
|
"columnName": "access_token",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "session.refreshToken",
|
||||||
|
"columnName": "refresh_token",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "session.tokenExpiration",
|
||||||
|
"columnName": "token_expiration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "session.tokenType",
|
||||||
|
"columnName": "token_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "session.installId",
|
||||||
|
"columnName": "install_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "user.id",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "user.name",
|
||||||
|
"columnName": "user_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "user.isOwner",
|
||||||
|
"columnName": "user_is_owner",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "user.isAdmin",
|
||||||
|
"columnName": "user_is_admin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "websocketSetting",
|
||||||
|
"columnName": "websocket_setting",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensorUpdateFrequency",
|
||||||
|
"columnName": "sensor_update_frequency",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ccc437900bf203ab7db2e782ffbd7f9')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
package io.homeassistant.companion.android.database
|
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>()!!
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package io.homeassistant.companion.android.database
|
||||||
|
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.NoSuchPropertyException
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation layer for IDs used by the old icondialog package to material icon names.
|
||||||
|
*/
|
||||||
|
class IconDialogCompat @Inject constructor(
|
||||||
|
private val assets: AssetManager
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Loads map of icon IDs to regular icon names.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun loadAllIcons(): Map<Int, String> {
|
||||||
|
val inputStream = assets.open("mdi_id_map.json")
|
||||||
|
return JsonReader(inputStream.reader()).use { reader ->
|
||||||
|
val result = mutableMapOf<Int, String>()
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
val iconName = reader.nextName()
|
||||||
|
val iconId = reader.nextInt()
|
||||||
|
result[iconId] = iconName
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads map of icon IDs to regular icon names in a background thread.
|
||||||
|
*/
|
||||||
|
suspend fun loadAllIconsAsync() = coroutineScope {
|
||||||
|
async(Dispatchers.IO) { loadAllIcons() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun streamingIconLookup(iconId: Int): String {
|
||||||
|
val iconName = withContext(Dispatchers.IO) {
|
||||||
|
val inputStream = assets.open("mdi_id_map.json")
|
||||||
|
JsonReader(inputStream.reader()).use { reader ->
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
val iconName = reader.nextName()
|
||||||
|
val id = reader.nextInt()
|
||||||
|
if (iconId == id) {
|
||||||
|
return@use iconName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
throw NoSuchPropertyException("ID $iconId is not valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iconName
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,8 +14,9 @@ data class TileEntity(
|
||||||
val added: Boolean,
|
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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue