-
Notifications
You must be signed in to change notification settings - Fork 3
feat: system widgets foundation + price widget #895
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
Open
jvsena42
wants to merge
64
commits into
master
Choose a base branch
from
feat/system-widgets-foundation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
9f86a9a
chore: add glance dependency
jvsena42 55b19eb
chore: data models
jvsena42 e909473
chore: independent data store for home widgets and cached data
jvsena42 b3d1235
feat: create repository reusing existing services
jvsena42 a3397b1
feat: refresh data with work manager
jvsena42 2bc681f
feat: data serializer
jvsena42 8112f12
feat: ui
jvsena42 01db527
feat: config
jvsena42 b19db43
feat: resources
jvsena42 32b14d2
feat: overload with GraphPeriod
jvsena42 4dca8ce
feat: manifest settings
jvsena42 fac5d41
refactor: remove duplicated strings
jvsena42 1c29a88
refactor: reuse existing colors
jvsena42 ae32258
refactor: por the existing design system to Glance
jvsena42 62be2c2
chore: lint
jvsena42 d1f4357
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 9ebaffe
refactor: remove boilerplate
jvsena42 abf1cfd
feat: port price widget to Glance
jvsena42 8c23053
feat: update widget instantly instead of enqueue
jvsena42 50d6372
refactor: implement spacer components
jvsena42 f7a37e7
chore: lint
jvsena42 4bc89ea
fix: horizontal spacer
jvsena42 f9368ff
refactor: remove source
jvsena42 705c754
fix: move chart to bottom
jvsena42 8aa87b0
refactor: remove context parameter
jvsena42 0c9a97c
feat: corner radius
jvsena42 d3699fc
feat: make the widget responsive
jvsena42 775cf0e
fix: push value to the left
jvsena42 4656e8a
feat: make chart expandable
jvsena42 6183158
feat: remove click event
jvsena42 e85dcd0
feat: fill height with the chart
jvsena42 940c027
feat: padding
jvsena42 2a6df91
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 21c7378
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 362eda4
feat: display period
jvsena42 4b46176
feat: edit widget on click
jvsena42 c0a328f
feat: remove header from preview
jvsena42 0e0cdee
feat: launch AppWidgetConfigActivity on a new task to don't return no…
jvsena42 2762e5c
fix: match glance text size with Compose
jvsena42 9ae6fa6
refactor: extract text components
jvsena42 a11f6e3
feat: make the chart smooth
jvsena42 757f370
refactor: rename bitmap component
jvsena42 75b210e
fix: reset enabled state
jvsena42 b9dad83
refactor: code cleanup
jvsena42 bec35f7
refactor: stability annotation
jvsena42 dc02330
refactor: move updateAll to activity level
jvsena42 3258330
fear: chart preview mockup
jvsena42 3d7c040
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 1de89d4
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 3352dd5
fix: drop singleInstance on widget config activity
jvsena42 74e367c
fix: cache price data per graph period in widget worker
jvsena42 a84f973
refactor: move companion object to top
jvsena42 76d5bd4
fix: check all widget types before cancelling refresh worker
jvsena42 14817e4
fix: clear widget preferences on instance deletion
jvsena42 2667245
refactor: replace with runCatching
jvsena42 ee6ecc2
doc: changelog entry
jvsena42 b806ac2
chore: lint
jvsena42 83a6955
chore: lint
jvsena42 fcd4ba5
chore: lint
jvsena42 a335c67
refactor: use Hilt EntryPoint for AppWidgetPreferencesStore
jvsena42 1cf5f37
Merge branch 'master' into feat/system-widgets-foundation
jvsena42 4a5061f
fix: warp get data in runCatching
jvsena42 d756a14
chore: lint
jvsena42 c082153
chore: lint
jvsena42 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
app/src/main/java/to/bitkit/appwidget/AppWidgetDataRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package to.bitkit.appwidget | ||
|
|
||
| import kotlinx.coroutines.CoroutineDispatcher | ||
| import kotlinx.coroutines.withContext | ||
| import to.bitkit.data.dto.price.GraphPeriod | ||
| import to.bitkit.data.dto.price.PriceDTO | ||
| import to.bitkit.data.widgets.PriceService | ||
| import to.bitkit.di.IoDispatcher | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class AppWidgetDataRepository @Inject constructor( | ||
| @IoDispatcher private val ioDispatcher: CoroutineDispatcher, | ||
| private val priceService: PriceService, | ||
| ) { | ||
| suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result<PriceDTO> = | ||
| withContext(ioDispatcher) { | ||
| priceService.fetchData(period) | ||
| } | ||
| } |
82 changes: 82 additions & 0 deletions
82
app/src/main/java/to/bitkit/appwidget/AppWidgetPreferencesStore.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package to.bitkit.appwidget | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.dataStore | ||
| import dagger.hilt.EntryPoint | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import dagger.hilt.components.SingletonComponent | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.first | ||
| import kotlinx.coroutines.flow.map | ||
| import to.bitkit.appwidget.model.AppWidgetData | ||
| import to.bitkit.appwidget.model.AppWidgetEntry | ||
| import to.bitkit.appwidget.model.AppWidgetType | ||
| import to.bitkit.data.dto.price.GraphPeriod | ||
| import to.bitkit.data.dto.price.PriceDTO | ||
| import to.bitkit.data.serializers.AppWidgetDataSerializer | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| private val Context.appWidgetDataStore: DataStore<AppWidgetData> by dataStore( | ||
| fileName = "appwidget_data.json", | ||
| serializer = AppWidgetDataSerializer, | ||
| ) | ||
|
|
||
| @EntryPoint | ||
| @InstallIn(SingletonComponent::class) | ||
| interface AppWidgetEntryPoint { | ||
| fun appWidgetPreferencesStore(): AppWidgetPreferencesStore | ||
| } | ||
|
|
||
| @Singleton | ||
| class AppWidgetPreferencesStore @Inject constructor( | ||
| @ApplicationContext private val context: Context, | ||
| ) { | ||
| private val store = context.appWidgetDataStore | ||
|
|
||
| val data: Flow<AppWidgetData> = store.data | ||
|
|
||
| suspend fun registerWidget(appWidgetId: Int, type: AppWidgetType) { | ||
| store.updateData { data -> | ||
| if (data.entries.any { it.appWidgetId == appWidgetId }) return@updateData data | ||
| data.copy(entries = data.entries + AppWidgetEntry(appWidgetId = appWidgetId, type = type)) | ||
| } | ||
| } | ||
|
|
||
| suspend fun unregisterWidget(appWidgetId: Int) { | ||
| store.updateData { data -> | ||
| data.copy(entries = data.entries.filter { it.appWidgetId != appWidgetId }) | ||
| } | ||
| } | ||
|
|
||
| suspend fun getEntry(appWidgetId: Int): AppWidgetEntry? = | ||
| store.data.first().entries.find { it.appWidgetId == appWidgetId } | ||
|
|
||
| suspend fun updateEntry(appWidgetId: Int, transform: (AppWidgetEntry) -> AppWidgetEntry) { | ||
| store.updateData { data -> | ||
| data.copy( | ||
| entries = data.entries.map { | ||
| if (it.appWidgetId == appWidgetId) transform(it) else it | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| suspend fun getActiveWidgetTypes(): Set<AppWidgetType> = | ||
| store.data.first().entries.map { it.type }.toSet() | ||
|
|
||
| suspend fun getActivePricePeriods(): Set<GraphPeriod> = | ||
| store.data.first().entries | ||
| .filter { it.type == AppWidgetType.PRICE } | ||
| .map { it.pricePreferences.period } | ||
| .toSet() | ||
|
|
||
| fun hasWidgetsOfType(type: AppWidgetType): Flow<Boolean> = | ||
| data.map { it.entries.any { entry -> entry.type == type } } | ||
|
|
||
| suspend fun cachePriceData(period: GraphPeriod, price: PriceDTO) { | ||
| store.updateData { it.copy(cachedPrices = it.cachedPrices + (period to price)) } | ||
| } | ||
| } |
92 changes: 92 additions & 0 deletions
92
app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package to.bitkit.appwidget | ||
|
|
||
| import android.appwidget.AppWidgetManager | ||
| import android.content.ComponentName | ||
| import android.content.Context | ||
| import androidx.glance.appwidget.GlanceAppWidgetReceiver | ||
| import androidx.glance.appwidget.updateAll | ||
| import androidx.hilt.work.HiltWorker | ||
| import androidx.work.Constraints | ||
| import androidx.work.CoroutineWorker | ||
| import androidx.work.ExistingPeriodicWorkPolicy | ||
| import androidx.work.NetworkType | ||
| import androidx.work.PeriodicWorkRequestBuilder | ||
| import androidx.work.WorkManager | ||
| import androidx.work.WorkerParameters | ||
| import dagger.assisted.Assisted | ||
| import dagger.assisted.AssistedInject | ||
| import to.bitkit.appwidget.model.AppWidgetType | ||
| import to.bitkit.appwidget.ui.price.PriceGlanceReceiver | ||
| import to.bitkit.appwidget.ui.price.PriceGlanceWidget | ||
| import to.bitkit.utils.Logger | ||
| import kotlin.time.Duration.Companion.minutes | ||
| import kotlin.time.toJavaDuration | ||
|
|
||
| @HiltWorker | ||
| class AppWidgetRefreshWorker @AssistedInject constructor( | ||
| @Assisted private val appContext: Context, | ||
| @Assisted workerParams: WorkerParameters, | ||
| private val dataRepository: AppWidgetDataRepository, | ||
| private val preferencesStore: AppWidgetPreferencesStore, | ||
| ) : CoroutineWorker(appContext, workerParams) { | ||
|
|
||
| companion object { | ||
| private const val TAG = "AppWidgetRefreshWorker" | ||
| private const val WORK_NAME = "appwidget_refresh" | ||
|
|
||
| fun enqueue(context: Context) { | ||
| val constraints = Constraints.Builder() | ||
| .setRequiredNetworkType(NetworkType.CONNECTED) | ||
| .build() | ||
|
|
||
| val request = PeriodicWorkRequestBuilder<AppWidgetRefreshWorker>(15.minutes.toJavaDuration()) | ||
| .setConstraints(constraints) | ||
| .build() | ||
|
|
||
| WorkManager.getInstance(context).enqueueUniquePeriodicWork( | ||
| WORK_NAME, | ||
| ExistingPeriodicWorkPolicy.KEEP, | ||
| request, | ||
| ) | ||
| } | ||
|
|
||
| fun cancelIfNoWidgets(context: Context) { | ||
| val manager = AppWidgetManager.getInstance(context) | ||
| val hasAny = AppWidgetType.entries.any { type -> | ||
| manager.getAppWidgetIds(ComponentName(context, receiverClassFor(type))).isNotEmpty() | ||
| } | ||
| if (!hasAny) { | ||
| WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) | ||
| } | ||
| } | ||
|
jvsena42 marked this conversation as resolved.
|
||
|
|
||
| private fun receiverClassFor(type: AppWidgetType): Class<out GlanceAppWidgetReceiver> = when (type) { | ||
| AppWidgetType.PRICE -> PriceGlanceReceiver::class.java | ||
| } | ||
| } | ||
|
|
||
| override suspend fun doWork(): Result { | ||
| val activeTypes = preferencesStore.getActiveWidgetTypes() | ||
| if (activeTypes.isEmpty()) return Result.success() | ||
|
|
||
| Logger.debug("Refreshing data for widget types: '$activeTypes'", context = TAG) | ||
|
|
||
| for (type in activeTypes) { | ||
| when (type) { | ||
| AppWidgetType.PRICE -> { | ||
| val periods = preferencesStore.getActivePricePeriods() | ||
| periods.forEach { period -> | ||
| dataRepository.fetchPriceData(period) | ||
| .onSuccess { preferencesStore.cachePriceData(period, it) } | ||
| .onFailure { | ||
| Logger.warn("Failed to refresh price for '$period'", it, context = TAG) | ||
| } | ||
| } | ||
| PriceGlanceWidget().updateAll(appContext) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Result.success() | ||
| } | ||
| } | ||
66 changes: 66 additions & 0 deletions
66
app/src/main/java/to/bitkit/appwidget/config/AppWidgetConfigActivity.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package to.bitkit.appwidget.config | ||
|
|
||
| import android.app.Activity | ||
| import android.appwidget.AppWidgetManager | ||
| import android.content.Intent | ||
| import android.os.Bundle | ||
| import androidx.activity.ComponentActivity | ||
| import androidx.activity.compose.setContent | ||
| import androidx.activity.viewModels | ||
| import androidx.glance.appwidget.updateAll | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import to.bitkit.appwidget.AppWidgetRefreshWorker | ||
| import to.bitkit.appwidget.model.AppWidgetType | ||
| import to.bitkit.appwidget.ui.price.PriceGlanceWidget | ||
| import to.bitkit.ui.theme.AppThemeSurface | ||
|
|
||
| @AndroidEntryPoint | ||
| class AppWidgetConfigActivity : ComponentActivity() { | ||
|
|
||
| companion object { | ||
| const val EXTRA_WIDGET_TYPE = "extra_widget_type" | ||
| } | ||
|
|
||
| private val viewModel: AppWidgetConfigViewModel by viewModels() | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| val appWidgetId = intent?.extras?.getInt( | ||
| AppWidgetManager.EXTRA_APPWIDGET_ID, | ||
| AppWidgetManager.INVALID_APPWIDGET_ID, | ||
| ) ?: AppWidgetManager.INVALID_APPWIDGET_ID | ||
|
|
||
| setResult(RESULT_CANCELED) | ||
|
|
||
| if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { | ||
| finish() | ||
| return | ||
| } | ||
|
|
||
| val typeName = intent?.getStringExtra(EXTRA_WIDGET_TYPE) | ||
| val type = typeName?.let { runCatching { AppWidgetType.valueOf(it) }.getOrNull() } | ||
| ?: AppWidgetType.PRICE | ||
|
|
||
| viewModel.init(appWidgetId, type) | ||
|
|
||
| setContent { | ||
| AppThemeSurface { | ||
| AppWidgetConfigScreen( | ||
| viewModel = viewModel, | ||
| onConfirm = { | ||
| PriceGlanceWidget().updateAll(this@AppWidgetConfigActivity) | ||
| AppWidgetRefreshWorker.enqueue(this@AppWidgetConfigActivity) | ||
| val result = Intent().putExtra( | ||
| AppWidgetManager.EXTRA_APPWIDGET_ID, | ||
| appWidgetId, | ||
| ) | ||
| setResult(Activity.RESULT_OK, result) | ||
| finish() | ||
| }, | ||
| onCancel = { finish() }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.