Bug fix stats widget (#1664)

* added resources to run project

* fixed problem of user stats not being shown in the widget

* refactored some code
This commit is contained in:
pauliancu97 2021-11-10 15:34:35 +02:00 committed by GitHub
parent 4790a8dccb
commit b77158d244
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 277 additions and 116 deletions

View file

@ -286,6 +286,9 @@
<service
android:name=".widget.TodosWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".widget.AvatarStatsWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"/>
<service android:name=".widget.HabitButtonWidgetService"/>
</application>

View file

@ -5,11 +5,8 @@
android:id="@+id/widget_main_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:background="@drawable/widget_background"
android:padding="8dp"
android:elevation="2dp"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:id="@+id/widget_main_avatar_view"
android:elevation="2dp">
<ListView
android:id="@+id/widget_avatar_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/widget_avatar_empty_view"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

View file

@ -109,6 +109,7 @@ import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
import com.habitrpg.android.habitica.ui.views.social.ChatBarView;
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog;
import com.habitrpg.android.habitica.ui.views.tasks.TaskFilterDialog;
import com.habitrpg.android.habitica.widget.AvatarStatsWidgetFactory;
import com.habitrpg.android.habitica.widget.AvatarStatsWidgetProvider;
import com.habitrpg.android.habitica.widget.BaseWidgetProvider;
import com.habitrpg.android.habitica.widget.DailiesWidgetProvider;
@ -349,4 +350,6 @@ public interface UserComponent {
void inject(@NotNull ItemDialogFragment itemDialogFragment);
void inject(@NotNull EquipmentOverviewViewModel equipmentOverviewViewModel);
void inject(@NotNull AvatarStatsWidgetFactory avatarStatsWidgetFactory);
}

View file

@ -19,7 +19,9 @@ import com.habitrpg.android.habitica.extensions.dpToPx
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.models.Avatar
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.subjects.PublishSubject
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
@ -38,6 +40,7 @@ class AvatarView : FrameLayout {
private val avatarMatrix = Matrix()
private val numberLayersInProcess = AtomicInteger(0)
private var avatarImageConsumer: Consumer<Bitmap?>? = null
private var avatarBitmapSubject: PublishSubject<Bitmap> = PublishSubject.create()
private var avatarBitmap: Bitmap? = null
private var avatarCanvas: Canvas? = null
private var currentLayers: Map<LayerType, String>? = null
@ -400,6 +403,7 @@ class AvatarView : FrameLayout {
private fun onLayerComplete() {
if (numberLayersInProcess.decrementAndGet() == 0) {
avatarImageConsumer?.accept(avatarImage)
avatarImage?.let { avatarBitmapSubject.onNext(it) }
}
}
@ -407,12 +411,24 @@ class AvatarView : FrameLayout {
avatarImageConsumer = consumer
if (imageViewHolder.size > 0 && numberLayersInProcess.get() == 0) {
avatarImageConsumer?.accept(avatarImage)
avatarImage?.let { avatarBitmapSubject.onNext(it) }
} else {
initAvatarRectMatrix()
showLayers(layerMap)
}
}
fun createAvatarImage(): Bitmap? {
if (imageViewHolder.size > 0 && numberLayersInProcess.get() == 0) {
avatarImageConsumer?.accept(avatarImage)
avatarImage?.let { avatarBitmapSubject.onNext(it) }
} else {
initAvatarRectMatrix()
showLayers(layerMap)
}
return avatarBitmapSubject.hide().firstElement().blockingGet()
}
fun setAvatar(avatar: Avatar) {
val oldUser = this.avatar
this.avatar = avatar

View file

@ -0,0 +1,182 @@
package com.habitrpg.android.habitica.widget
import android.appwidget.AppWidgetManager
import android.content.Context
import android.os.Handler
import android.view.View
import android.view.ViewGroup
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.extensions.dpToPx
import com.habitrpg.android.habitica.helpers.HealthFormatter
import com.habitrpg.android.habitica.helpers.NumberAbbreviator
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import javax.inject.Inject
class AvatarStatsWidgetFactory(
private val context: Context,
private val widgetId: Int
): RemoteViewsService.RemoteViewsFactory {
private var isInitialized: Boolean = false
@Inject
lateinit var userRepository: UserRepository
private val disposable = CompositeDisposable()
private var user: User? = null
private var shouldLoadData: Boolean = false
private val appWidgetManager = AppWidgetManager.getInstance(context)
private fun setup() {
if (!isInitialized) {
HabiticaBaseApplication.userComponent?.inject(this)
isInitialized = true
}
}
private fun loadUser() {
val mainHandler = Handler(context.mainLooper)
mainHandler.post {
disposable.add(userRepository.getUser()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(
{ user ->
this.user = user
this.shouldLoadData = false
appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.widget_avatar_list)
},
RxErrorHandler.handleEmptyError()
)
)
}
}
private fun getRemoteViewForUser(user: User, stats: Stats): RemoteViews {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_avatar_stats)
val options = appWidgetManager.getAppWidgetOptions(widgetId)
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
val cols = BaseWidgetProvider.getCellsForSize(minWidth)
val rows = BaseWidgetProvider.getCellsForSize(minHeight)
val showAvatar = cols > 3
val showManaBar = rows > 1
val currentHealth = HealthFormatter.format(stats.hp ?: 0.0)
val currentHealthString = HealthFormatter.formatToString(stats.hp ?: 0.0)
val healthValueString = currentHealthString + "/" + stats.maxHealth
val expValueString = "" + stats.exp?.toInt() + "/" + stats.toNextLevel
val mpValueString = "" + stats.mp?.toInt() + "/" + stats.maxMP
remoteViews.setTextViewText(R.id.TV_hp_value, healthValueString)
remoteViews.setTextViewText(R.id.exp_TV_value, expValueString)
remoteViews.setTextViewText(R.id.mp_TV_value, mpValueString)
remoteViews.setImageViewBitmap(R.id.ic_hp_header, HabiticaIconsHelper.imageOfHeartLightBg())
remoteViews.setImageViewBitmap(R.id.ic_exp_header, HabiticaIconsHelper.imageOfExperience())
remoteViews.setImageViewBitmap(R.id.ic_mp_header, HabiticaIconsHelper.imageOfMagic())
remoteViews.setProgressBar(R.id.hp_bar, stats.maxHealth ?: 0, currentHealth.toInt(), false)
remoteViews.setProgressBar(R.id.exp_bar, stats.toNextLevel ?: 0, stats.exp?.toInt() ?: 0, false)
remoteViews.setProgressBar(R.id.mp_bar, stats.maxMP ?: 0, stats.mp?.toInt() ?: 0, false)
remoteViews.setViewVisibility(R.id.mp_wrapper, if (showManaBar && (stats.habitClass == null || (stats.lvl ?: 0) < 10 || user.preferences?.disableClasses == true)) View.GONE else View.VISIBLE)
remoteViews.setTextViewText(R.id.gold_tv, NumberAbbreviator.abbreviate(context, stats.gp ?: 0.0))
remoteViews.setTextViewText(R.id.gems_tv, (user.balance * 4).toInt().toString())
val hourGlassCount = user.hourglassCount
if (hourGlassCount == 0) {
remoteViews.setViewVisibility(R.id.hourglass_icon, View.GONE)
remoteViews.setViewVisibility(R.id.hourglasses_tv, View.GONE)
} else {
remoteViews.setImageViewBitmap(R.id.hourglass_icon, HabiticaIconsHelper.imageOfHourglass())
remoteViews.setViewVisibility(R.id.hourglass_icon, View.VISIBLE)
remoteViews.setTextViewText(R.id.hourglasses_tv, hourGlassCount.toString())
remoteViews.setViewVisibility(R.id.hourglasses_tv, View.VISIBLE)
}
remoteViews.setImageViewBitmap(R.id.gem_icon, HabiticaIconsHelper.imageOfGem())
remoteViews.setImageViewBitmap(R.id.gold_icon, HabiticaIconsHelper.imageOfGold())
remoteViews.setTextViewText(R.id.lvl_tv, context.getString(R.string.user_level, user.stats?.lvl ?: 0))
if (showAvatar) {
val avatarView =
AvatarView(context, showBackground = true, showMount = true, showPet = true)
val layoutParams = ViewGroup.LayoutParams(140.dpToPx(context), 147.dpToPx(context))
avatarView.layoutParams = layoutParams
avatarView.setAvatar(user)
avatarView.createAvatarImage()?.let { bitmap ->
remoteViews.setImageViewBitmap(R.id.avatar_view, bitmap)
}
}
if (showAvatar) {
remoteViews.setViewVisibility(R.id.avatar_view, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.avatar_view, View.GONE)
}
if (showManaBar) {
remoteViews.setViewVisibility(R.id.detail_info_view, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.detail_info_view, View.GONE)
}
return remoteViews
}
override fun onCreate() {
setup()
loadUser()
}
override fun onDestroy() {
disposable.clear()
}
override fun onDataSetChanged() {
if (shouldLoadData) {
loadUser()
}
shouldLoadData = true
}
override fun getCount(): Int {
return 1
}
override fun getViewAt(p0: Int): RemoteViews {
val user = this.user
val stats = user?.stats
return if (user != null && stats != null) {
getRemoteViewForUser(user, stats)
} else {
RemoteViews(context.packageName, R.layout.widget_avatar_stats)
}
}
override fun getLoadingView() = RemoteViews(context.packageName, R.layout.widget_avatar_stats)
override fun getViewTypeCount(): Int {
return 1
}
override fun getItemId(position: Int) = position.toLong()
override fun hasStableIds() = true
}

View file

@ -2,32 +2,21 @@ package com.habitrpg.android.habitica.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import android.net.Uri
import android.os.Bundle
import android.widget.RemoteViews
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.dpToPx
import com.habitrpg.android.habitica.helpers.HealthFormatter
import com.habitrpg.android.habitica.helpers.NumberAbbreviator
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
class AvatarStatsWidgetProvider : BaseWidgetProvider() {
private var appWidgetManager: AppWidgetManager? = null
private var showManaBar: Boolean = true
private var showAvatar: Boolean = true
override fun layoutResourceId(): Int {
return R.layout.widget_avatar_stats
return R.layout.widget_main_avatar_stats
}
private fun setUp() {
@ -38,100 +27,35 @@ class AvatarStatsWidgetProvider : BaseWidgetProvider() {
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
this.setUp()
this.appWidgetManager = appWidgetManager
this.context = context
for (widgetId in appWidgetIds) {
userRepository.getUser().firstElement()?.subscribe({ this.updateData(it) }, RxErrorHandler.handleEmptyError())
}
override fun configureRemoteViews(remoteViews: RemoteViews, widgetId: Int, columns: Int, rows: Int): RemoteViews {
showAvatar = columns > 3
if (showAvatar) {
remoteViews.setViewVisibility(R.id.avatar_view, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.avatar_view, View.GONE)
}
showManaBar = rows > 1
if (rows > 1) {
remoteViews.setViewVisibility(R.id.detail_info_view, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.detail_info_view, View.GONE)
}
return remoteViews
}
private fun updateData(user: User?) {
val context = context
val appWidgetManager = appWidgetManager
val stats = user?.stats
if (user == null || stats == null || context == null || appWidgetManager == null) {
return
}
val thisWidget = ComponentName(context, AvatarStatsWidgetProvider::class.java)
val allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val currentHealth = HealthFormatter.format(stats.hp ?: 0.0)
val currentHealthString = HealthFormatter.formatToString(stats.hp ?: 0.0)
val healthValueString = currentHealthString + "/" + stats.maxHealth
val expValueString = "" + stats.exp?.toInt() + "/" + stats.toNextLevel
val mpValueString = "" + stats.mp?.toInt() + "/" + stats.maxMP
for (widgetId in allWidgetIds) {
var remoteViews = RemoteViews(context.packageName, R.layout.widget_avatar_stats)
remoteViews.setTextViewText(R.id.TV_hp_value, healthValueString)
remoteViews.setTextViewText(R.id.exp_TV_value, expValueString)
remoteViews.setTextViewText(R.id.mp_TV_value, mpValueString)
remoteViews.setImageViewBitmap(R.id.ic_hp_header, HabiticaIconsHelper.imageOfHeartLightBg())
remoteViews.setImageViewBitmap(R.id.ic_exp_header, HabiticaIconsHelper.imageOfExperience())
remoteViews.setImageViewBitmap(R.id.ic_mp_header, HabiticaIconsHelper.imageOfMagic())
remoteViews.setProgressBar(R.id.hp_bar, stats.maxHealth ?: 0, currentHealth.toInt(), false)
remoteViews.setProgressBar(R.id.exp_bar, stats.toNextLevel ?: 0, stats.exp?.toInt() ?: 0, false)
remoteViews.setProgressBar(R.id.mp_bar, stats.maxMP ?: 0, stats.mp?.toInt() ?: 0, false)
remoteViews.setViewVisibility(R.id.mp_wrapper, if (showManaBar && (stats.habitClass == null || (stats.lvl ?: 0) < 10 || user.preferences?.disableClasses == true)) View.GONE else View.VISIBLE)
remoteViews.setTextViewText(R.id.gold_tv, NumberAbbreviator.abbreviate(context, stats.gp ?: 0.0))
remoteViews.setTextViewText(R.id.gems_tv, (user.balance * 4).toInt().toString())
val hourGlassCount = user.hourglassCount
if (hourGlassCount == 0) {
remoteViews.setViewVisibility(R.id.hourglass_icon, View.GONE)
remoteViews.setViewVisibility(R.id.hourglasses_tv, View.GONE)
} else {
remoteViews.setImageViewBitmap(R.id.hourglass_icon, HabiticaIconsHelper.imageOfHourglass())
remoteViews.setViewVisibility(R.id.hourglass_icon, View.VISIBLE)
remoteViews.setTextViewText(R.id.hourglasses_tv, hourGlassCount.toString())
remoteViews.setViewVisibility(R.id.hourglasses_tv, View.VISIBLE)
}
remoteViews.setImageViewBitmap(R.id.gem_icon, HabiticaIconsHelper.imageOfGem())
remoteViews.setImageViewBitmap(R.id.gold_icon, HabiticaIconsHelper.imageOfGold())
remoteViews.setTextViewText(R.id.lvl_tv, context.getString(R.string.user_level, user.stats?.lvl ?: 0))
if (showAvatar) {
val avatarView =
AvatarView(context, showBackground = true, showMount = true, showPet = true)
val layoutParams = ViewGroup.LayoutParams(140.dpToPx(context), 147.dpToPx(context))
avatarView.layoutParams = layoutParams
avatarView.setAvatar(user)
val finalRemoteViews = remoteViews
avatarView.onAvatarImageReady { bitmap ->
finalRemoteViews.setImageViewBitmap(R.id.avatar_view, bitmap)
appWidgetManager.partiallyUpdateAppWidget(allWidgetIds, finalRemoteViews)
}
}
val intent = Intent(context, AvatarStatsWidgetService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
val openAppIntent = Intent(context.applicationContext, MainActivity::class.java)
val openApp = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setOnClickPendingIntent(R.id.widget_main_view, openApp)
val options = appWidgetManager.getAppWidgetOptions(widgetId)
remoteViews = sizeRemoteViews(context, options, widgetId)
val remoteViews = RemoteViews(context.packageName, R.layout.widget_main_avatar_stats)
remoteViews.setRemoteAdapter(R.id.widget_avatar_list, intent)
remoteViews.setEmptyView(R.id.widget_avatar_list, R.id.widget_avatar_empty_view)
remoteViews.setOnClickPendingIntent(R.id.widget_main_avatar_view, openApp)
appWidgetManager.updateAppWidget(widgetId, remoteViews)
appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.widget_avatar_list)
}
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle
) {
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_avatar_list)
}
override fun configureRemoteViews(remoteViews: RemoteViews, widgetId: Int, columns: Int, rows: Int): RemoteViews = remoteViews
}

View file

@ -0,0 +1,12 @@
package com.habitrpg.android.habitica.widget
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.widget.RemoteViewsService
class AvatarStatsWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0)
return AvatarStatsWidgetFactory(this.applicationContext, widgetId)
}
}

View file

@ -15,6 +15,23 @@ import javax.inject.Inject
abstract class BaseWidgetProvider : AppWidgetProvider() {
companion object {
/**
* Returns number of cells needed for given size of the widget.<br></br>
* see http://stackoverflow.com/questions/14270138/dynamically-adjusting-widgets-content-and-layout-to-the-size-the-user-defined-t
*
* @param size Widget size in dp.
* @return Size in number of cells.
*/
fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
}
@Inject
lateinit var userRepository: UserRepository
@ -22,20 +39,7 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
protected var context: Context? = null
/**
* Returns number of cells needed for given size of the widget.<br></br>
* see http://stackoverflow.com/questions/14270138/dynamically-adjusting-widgets-content-and-layout-to-the-size-the-user-defined-t
*
* @param size Widget size in dp.
* @return Size in number of cells.
*/
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
this.context = context