-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Refactor MediaPlayerControlsWidgetConfigureActivity to Compose and Material 3 #7018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 16 commits
c99dc1b
423f518
b0a8847
920c3fc
8bfb566
01c740f
5ba2f00
58d545a
38ab412
ad413d2
94ec420
c4e391b
37cdda3
dbfe171
99b7dcc
6a9fea8
ae5afa1
c886ff6
445aae6
95f8767
e96011a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,38 +5,27 @@ import android.content.Context | |
| import android.content.Intent | ||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import android.view.View | ||
| import android.widget.ArrayAdapter | ||
| import android.widget.AutoCompleteTextView | ||
| import android.widget.MultiAutoCompleteTextView | ||
| import android.widget.Spinner | ||
| import android.widget.Toast | ||
| import androidx.activity.compose.setContent | ||
| import androidx.activity.viewModels | ||
| import androidx.lifecycle.lifecycleScope | ||
| import com.google.android.material.color.DynamicColors | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import dagger.hilt.android.lifecycle.withCreationCallback | ||
| import io.homeassistant.companion.android.BaseActivity | ||
| import io.homeassistant.companion.android.common.R as commonR | ||
| import io.homeassistant.companion.android.common.data.integration.Entity | ||
| import io.homeassistant.companion.android.common.data.integration.IntegrationDomains.MEDIA_PLAYER_DOMAIN | ||
| import io.homeassistant.companion.android.common.compose.theme.HATheme | ||
| import io.homeassistant.companion.android.common.util.SdkVersion | ||
| import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao | ||
| import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity | ||
| import io.homeassistant.companion.android.database.widget.WidgetBackgroundType | ||
| import io.homeassistant.companion.android.databinding.WidgetMediaControlsConfigureBinding | ||
| import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel | ||
| import io.homeassistant.companion.android.util.applySafeDrawingInsets | ||
| import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity | ||
| import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter | ||
| import io.homeassistant.companion.android.widgets.common.WidgetUtils | ||
| import java.util.LinkedList | ||
| import io.homeassistant.companion.android.widgets.mediaplayer.MediaPlayerControlsWidgetConfigureViewModel.Factory | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.runBlocking | ||
| import timber.log.Timber | ||
|
|
||
| // TODO Migrate to compose https://github.com/home-assistant/android/issues/6308 | ||
| @AndroidEntryPoint | ||
| class MediaPlayerControlsWidgetConfigureActivity : | ||
| BaseWidgetConfigureActivity<MediaPlayerControlsWidgetEntity, MediaPlayerControlsWidgetDao>() { | ||
| class MediaPlayerControlsWidgetConfigureActivity : BaseActivity() { | ||
|
|
||
| companion object { | ||
| private const val FOR_ENTITY = "for_entity" | ||
|
|
||
| fun newInstance(context: Context, entityId: String): Intent { | ||
| return Intent(context, MediaPlayerControlsWidgetConfigureActivity::class.java).apply { | ||
| putExtra(FOR_ENTITY, entityId) | ||
|
|
@@ -46,198 +35,80 @@ class MediaPlayerControlsWidgetConfigureActivity : | |
| } | ||
| } | ||
|
|
||
| private var requestLauncherSetup = false | ||
|
|
||
| private lateinit var binding: WidgetMediaControlsConfigureBinding | ||
|
|
||
| override val serverSelect: View | ||
| get() = binding.serverSelect | ||
|
|
||
| override val serverSelectList: Spinner | ||
| get() = binding.serverSelectList | ||
|
|
||
| private var entities = mutableMapOf<Int, List<Entity>>() | ||
| private var selectedEntities: LinkedList<Entity?> = LinkedList() | ||
| private val viewModel: MediaPlayerControlsWidgetConfigureViewModel by viewModels( | ||
| extrasProducer = { | ||
| defaultViewModelCreationExtras.withCreationCallback<Factory> { factory -> | ||
| factory.create(intent.getStringExtra(FOR_ENTITY)) | ||
| } | ||
| }, | ||
| ) | ||
|
|
||
| private var entityAdapter: SingleItemArrayAdapter<Entity>? = null | ||
| private val requestLauncherSetup: Boolean | ||
| get() = intent.extras?.getBoolean(ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER, false) == true | ||
|
|
||
| public override fun onCreate(savedInstanceState: Bundle?) { | ||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| // Set the result to CANCELED. This will cause the widget host to cancel | ||
| // out of the widget placement if the user presses the back button. | ||
| // Set the result to CANCELED. This will cause the widget host to cancel out of the widget | ||
| // placement if the user closes the screen or presses the back button. | ||
| setResult(RESULT_CANCELED) | ||
|
|
||
| binding = WidgetMediaControlsConfigureBinding.inflate(layoutInflater) | ||
| setContentView(binding.root) | ||
| binding.root.applySafeDrawingInsets() | ||
|
|
||
| binding.addButton.setOnClickListener { | ||
| lifecycleScope.launch { | ||
| if (requestLauncherSetup) { | ||
| if ( | ||
| SdkVersion.isAtLeast(Build.VERSION_CODES.O) && | ||
| isValidServerId() && | ||
| binding.widgetTextConfigEntityId.text.split(",").any { | ||
| entities[selectedServerId!!].orEmpty().any { e -> e.entityId == it.trim() } | ||
| } | ||
| ) { | ||
| requestWidgetCreation() | ||
| } else { | ||
| showAddWidgetError() | ||
| } | ||
| } else { | ||
| updateWidget() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Find the widget id from the intent. | ||
| val intent = intent | ||
| val extras = intent.extras | ||
| if (extras != null) { | ||
| if (extras.containsKey(FOR_ENTITY)) { | ||
| binding.widgetTextConfigEntityId.setText(extras.getString(FOR_ENTITY)) | ||
| } | ||
| appWidgetId = extras.getInt( | ||
| AppWidgetManager.EXTRA_APPWIDGET_ID, | ||
| AppWidgetManager.INVALID_APPWIDGET_ID, | ||
| ) | ||
| requestLauncherSetup = extras.getBoolean( | ||
| ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER, | ||
| false, | ||
| ) | ||
| } | ||
| val widgetId = intent.extras?.getInt( | ||
| AppWidgetManager.EXTRA_APPWIDGET_ID, | ||
| AppWidgetManager.INVALID_APPWIDGET_ID, | ||
| ) ?: AppWidgetManager.INVALID_APPWIDGET_ID | ||
|
|
||
|
TimoPtr marked this conversation as resolved.
Outdated
|
||
| // If this activity was started with an intent without an app widget ID, finish with an error. | ||
| if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) { | ||
| if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) { | ||
| finish() | ||
| return | ||
| } | ||
|
|
||
| val backgroundTypeValues = WidgetUtils.getBackgroundOptionList(this) | ||
| binding.backgroundType.adapter = | ||
| ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, backgroundTypeValues) | ||
| viewModel.onSetup(widgetId) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could potentially directly pass the widgetId in the factory of the ViewModel like we already do for the Entity
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #7007's |
||
|
|
||
| lifecycleScope.launch { | ||
| val mediaPlayerWidget = dao.get(appWidgetId) | ||
|
|
||
| if (mediaPlayerWidget != null) { | ||
| binding.label.setText(mediaPlayerWidget.label) | ||
| binding.widgetTextConfigEntityId.setText(mediaPlayerWidget.entityId) | ||
| binding.widgetShowVolumeButtonCheckbox.isChecked = mediaPlayerWidget.showVolume | ||
| binding.widgetShowSeekButtonsCheckbox.isChecked = mediaPlayerWidget.showSeek | ||
| binding.widgetShowSkipButtonsCheckbox.isChecked = mediaPlayerWidget.showSkip | ||
| binding.widgetShowMediaPlayerSource.isChecked = mediaPlayerWidget.showSource | ||
| binding.backgroundType.setSelection( | ||
| WidgetUtils.getSelectedBackgroundOption( | ||
| this@MediaPlayerControlsWidgetConfigureActivity, | ||
| mediaPlayerWidget.backgroundType, | ||
| backgroundTypeValues, | ||
| ), | ||
| setContent { | ||
| HATheme { | ||
| MediaPlayerControlsWidgetConfigureScreen( | ||
| viewModel = viewModel, | ||
| dynamicColorAvailable = DynamicColors.isDynamicColorAvailable(), | ||
| onActionClick = ::onActionClick, | ||
| onClose = ::finish, | ||
| ) | ||
| val entities = runBlocking { | ||
| try { | ||
| mediaPlayerWidget.entityId.split(",").map { s -> | ||
| serverManager.integrationRepository(mediaPlayerWidget.serverId).getEntity(s.trim()) | ||
| } | ||
| } catch (e: Exception) { | ||
| Timber.e(e, "Unable to get entity information") | ||
| Toast.makeText( | ||
| applicationContext, | ||
| commonR.string.widget_entity_fetch_error, | ||
| Toast.LENGTH_LONG, | ||
| ) | ||
| .show() | ||
| null | ||
| } | ||
| } | ||
| if (entities != null) { | ||
| selectedEntities.addAll(entities) | ||
| } | ||
| binding.addButton.setText(commonR.string.update_widget) | ||
| } | ||
| setupServerSelect(mediaPlayerWidget?.serverId) | ||
| } | ||
| } | ||
|
|
||
| entityAdapter = SingleItemArrayAdapter(this) { it?.entityId ?: "" } | ||
|
|
||
| binding.widgetTextConfigEntityId.setAdapter(entityAdapter) | ||
| binding.widgetTextConfigEntityId.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer()) | ||
| binding.widgetTextConfigEntityId.onFocusChangeListener = dropDownOnFocus | ||
|
|
||
| private fun onActionClick() { | ||
| lifecycleScope.launch { | ||
| serverManager.servers().forEach { server -> | ||
| launch { | ||
| try { | ||
| val fetchedEntities = serverManager.integrationRepository(server.id).getEntities().orEmpty() | ||
| .filter { it.domain == MEDIA_PLAYER_DOMAIN } | ||
| entities[server.id] = fetchedEntities | ||
| if (server.id == selectedServerId) setAdapterEntities(server.id) | ||
| } catch (e: Exception) { | ||
| // If entities fail to load, it's okay to pass | ||
| // an empty map to the dynamicFieldAdapter | ||
| Timber.e(e, "Failed to query entities") | ||
| } | ||
| if (requestLauncherSetup) { | ||
| if (SdkVersion.isAtLeast(Build.VERSION_CODES.O) && viewModel.isValidSelection()) { | ||
| requestPinWidget() | ||
| } else { | ||
| viewModel.onUserMessage(commonR.string.widget_creation_error) | ||
| } | ||
| } else { | ||
| updateWidget() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private val dropDownOnFocus = View.OnFocusChangeListener { view, hasFocus -> | ||
| if (hasFocus && view is AutoCompleteTextView) { | ||
| view.showDropDown() | ||
| } | ||
| } | ||
|
|
||
| override fun onServerSelected(serverId: Int) { | ||
| selectedEntities.clear() | ||
| binding.widgetTextConfigEntityId.setText("") | ||
| setAdapterEntities(serverId) | ||
| } | ||
|
|
||
| private fun setAdapterEntities(serverId: Int) { | ||
| entityAdapter?.let { adapter -> | ||
| adapter.clearAll() | ||
| if (entities[serverId] != null) { | ||
| adapter.addAll(entities[serverId].orEmpty().toMutableList()) | ||
| adapter.sort() | ||
| } | ||
| runOnUiThread { adapter.notifyDataSetChanged() } | ||
| private suspend fun requestPinWidget() { | ||
| try { | ||
| viewModel.requestWidgetCreation(this) | ||
| finish() | ||
| } catch (e: IllegalStateException) { | ||
| Timber.e(e, "Unable to request widget pin") | ||
| viewModel.onUserMessage(commonR.string.widget_creation_error) | ||
| } | ||
| } | ||
|
|
||
| override suspend fun getPendingDaoEntity(): MediaPlayerControlsWidgetEntity { | ||
| val serverId = checkNotNull(selectedServerId) { "Selected server ID is null" } | ||
| selectedEntities = LinkedList() | ||
| val se = binding.widgetTextConfigEntityId.text.split(",") | ||
| se.forEach { | ||
| val entity = entities[serverId]?.firstOrNull { e -> e.entityId == it.trim() } | ||
| if (entity != null) selectedEntities.add(entity) | ||
| } | ||
|
|
||
| val entitySelection = selectedEntities.map { e -> e?.entityId }.reduceOrNull { a, b -> "$a,$b" } | ||
|
|
||
| if (entitySelection == null) { | ||
| throw IllegalStateException("No valid entities selected") | ||
| } | ||
|
|
||
| return MediaPlayerControlsWidgetEntity( | ||
| id = appWidgetId, | ||
| serverId = serverId, | ||
| entityId = entitySelection, | ||
| label = binding.label.text.toString(), | ||
| showVolume = binding.widgetShowVolumeButtonCheckbox.isChecked, | ||
| showSkip = binding.widgetShowSkipButtonsCheckbox.isChecked, | ||
| showSeek = binding.widgetShowSeekButtonsCheckbox.isChecked, | ||
| showSource = binding.widgetShowMediaPlayerSource.isChecked, | ||
| backgroundType = when (binding.backgroundType.selectedItem as String?) { | ||
| getString(commonR.string.widget_background_type_dynamiccolor) -> WidgetBackgroundType.DYNAMICCOLOR | ||
| getString(commonR.string.widget_background_type_transparent) -> WidgetBackgroundType.TRANSPARENT | ||
| else -> WidgetBackgroundType.DAYNIGHT | ||
| }, | ||
| private suspend fun updateWidget() { | ||
| // The view model surfaces a user message itself when the selection is invalid. | ||
| if (!viewModel.updateWidgetConfiguration()) return | ||
| viewModel.updateWidget(this) | ||
| setResult( | ||
| RESULT_OK, | ||
| Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, viewModel.widgetId), | ||
| ) | ||
| finish() | ||
| } | ||
|
|
||
| override val widgetClass: Class<*> = MediaPlayerControlsWidget::class.java | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.