Convert org.isoron.uhabits.core.ui.widgets

pull/709/head
Quentin Hibon 5 years ago
parent 11831a2b24
commit eb8d39fbe3

@ -1,214 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*;
import android.graphics.*;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.intents.*;
import static android.view.View.MeasureSpec.makeMeasureSpec;
public abstract class BaseWidget
{
private final int id;
@NonNull
protected final WidgetPreferences widgetPrefs;
@NonNull
protected final Preferences prefs;
@NonNull
protected final PendingIntentFactory pendingIntentFactory;
@NonNull
private final Context context;
@NonNull
protected final CommandRunner commandRunner;
@NonNull
private WidgetDimensions dimensions;
public BaseWidget(@NonNull Context context, int id)
{
this.id = id;
this.context = context;
HabitsApplication app =
(HabitsApplication) context.getApplicationContext();
widgetPrefs = app.getComponent().getWidgetPreferences();
prefs = app.getComponent().getPreferences();
commandRunner = app.getComponent().getCommandRunner();
pendingIntentFactory = app.getComponent().getPendingIntentFactory();
dimensions = new WidgetDimensions(getDefaultWidth(), getDefaultHeight(),
getDefaultWidth(), getDefaultHeight());
}
public void delete()
{
widgetPrefs.removeWidget(id);
}
@NonNull
public Context getContext()
{
return context;
}
public int getId()
{
return id;
}
@NonNull
public RemoteViews getLandscapeRemoteViews()
{
return getRemoteViews(dimensions.getLandscapeWidth(),
dimensions.getLandscapeHeight());
}
public abstract PendingIntent getOnClickPendingIntent(Context context);
@NonNull
public RemoteViews getPortraitRemoteViews()
{
return getRemoteViews(dimensions.getPortraitWidth(),
dimensions.getPortraitHeight());
}
public abstract void refreshData(View widgetView);
public void setDimensions(@NonNull WidgetDimensions dimensions)
{
this.dimensions = dimensions;
}
protected abstract View buildView();
protected abstract int getDefaultHeight();
protected abstract int getDefaultWidth();
private void adjustRemoteViewsPadding(RemoteViews remoteViews,
View view,
int width,
int height)
{
int imageWidth = view.getMeasuredWidth();
int imageHeight = view.getMeasuredHeight();
int p[] = calculatePadding(width, height, imageWidth, imageHeight);
remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]);
}
private void buildRemoteViews(View view,
RemoteViews remoteViews,
int width,
int height)
{
Bitmap bitmap = getBitmapFromView(view);
remoteViews.setImageViewBitmap(R.id.imageView, bitmap);
adjustRemoteViewsPadding(remoteViews, view, width, height);
PendingIntent onClickIntent = getOnClickPendingIntent(context);
if (onClickIntent != null)
remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent);
}
private int[] calculatePadding(int entireWidth,
int entireHeight,
int imageWidth,
int imageHeight)
{
int w = (int) (((float) entireWidth - imageWidth) / 2);
int h = (int) (((float) entireHeight - imageHeight) / 2);
return new int[]{w, h, w, h};
}
@NonNull
private Bitmap getBitmapFromView(View view)
{
view.invalidate();
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
@NonNull
protected RemoteViews getRemoteViews(int width, int height)
{
View view = buildView();
measureView(view, width, height);
refreshData(view);
if (view.isLayoutRequested()) measureView(view, width, height);
RemoteViews remoteViews =
new RemoteViews(context.getPackageName(), R.layout.widget_wrapper);
buildRemoteViews(view, remoteViews, width, height);
return remoteViews;
}
private void measureView(View view, int width, int height)
{
LayoutInflater inflater = LayoutInflater.from(context);
View entireView = inflater.inflate(R.layout.widget_wrapper, null);
int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
entireView.measure(specWidth, specHeight);
entireView.layout(0, 0, entireView.getMeasuredWidth(),
entireView.getMeasuredHeight());
View imageView = entireView.findViewById(R.id.imageView);
width = imageView.getMeasuredWidth();
height = imageView.getMeasuredHeight();
specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
protected int getPreferedBackgroundAlpha() {
return prefs.getWidgetOpacity();
}
}

@ -0,0 +1,162 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import android.widget.RemoteViews
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.intents.PendingIntentFactory
abstract class BaseWidget(val context: Context, val id: Int) {
protected val widgetPrefs: WidgetPreferences
protected val prefs: Preferences
protected val pendingIntentFactory: PendingIntentFactory
protected val commandRunner: CommandRunner
private var dimensions: WidgetDimensions
fun delete() {
widgetPrefs.removeWidget(id)
}
val landscapeRemoteViews: RemoteViews
get() = getRemoteViews(
dimensions.landscapeWidth,
dimensions.landscapeHeight
)
abstract fun getOnClickPendingIntent(context: Context): PendingIntent?
val portraitRemoteViews: RemoteViews
get() = getRemoteViews(
dimensions.portraitWidth,
dimensions.portraitHeight
)
abstract fun refreshData(widgetView: View)
fun setDimensions(dimensions: WidgetDimensions) {
this.dimensions = dimensions
}
protected abstract fun buildView(): View?
protected abstract val defaultHeight: Int
protected abstract val defaultWidth: Int
private fun adjustRemoteViewsPadding(
remoteViews: RemoteViews,
view: View,
width: Int,
height: Int
) {
val imageWidth = view.measuredWidth
val imageHeight = view.measuredHeight
val p = calculatePadding(width, height, imageWidth, imageHeight)
remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3])
}
private fun buildRemoteViews(
view: View,
remoteViews: RemoteViews,
width: Int,
height: Int
) {
val bitmap = getBitmapFromView(view)
remoteViews.setImageViewBitmap(R.id.imageView, bitmap)
adjustRemoteViewsPadding(remoteViews, view, width, height)
val onClickIntent = getOnClickPendingIntent(context)
if (onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent)
}
private fun calculatePadding(
entireWidth: Int,
entireHeight: Int,
imageWidth: Int,
imageHeight: Int
): IntArray {
val w = ((entireWidth.toFloat() - imageWidth) / 2).toInt()
val h = ((entireHeight.toFloat() - imageHeight) / 2).toInt()
return intArrayOf(w, h, w, h)
}
private fun getBitmapFromView(view: View): Bitmap {
view.invalidate()
val width = view.measuredWidth
val height = view.measuredHeight
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
protected open fun getRemoteViews(width: Int, height: Int): RemoteViews {
val view = buildView()!!
measureView(view, width, height)
refreshData(view)
if (view.isLayoutRequested) measureView(view, width, height)
val remoteViews = RemoteViews(context.packageName, R.layout.widget_wrapper)
buildRemoteViews(view, remoteViews, width, height)
return remoteViews
}
private fun measureView(view: View, width: Int, height: Int) {
var width = width
var height = height
val inflater = LayoutInflater.from(context)
val entireView = inflater.inflate(R.layout.widget_wrapper, null)
var specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
var specHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
entireView.measure(specWidth, specHeight)
entireView.layout(
0,
0,
entireView.measuredWidth,
entireView.measuredHeight
)
val imageView = entireView.findViewById<View>(R.id.imageView)
width = imageView.measuredWidth
height = imageView.measuredHeight
specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
specHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
view.measure(specWidth, specHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
protected val preferedBackgroundAlpha: Int
protected get() = prefs.widgetOpacity
init {
val app = context.applicationContext as HabitsApplication
widgetPrefs = app.component.widgetPreferences
prefs = app.component.preferences
commandRunner = app.component.commandRunner
pendingIntentFactory = app.component.pendingIntentFactory
dimensions = WidgetDimensions(
defaultWidth,
defaultHeight,
defaultWidth,
defaultHeight
)
}
}

@ -1,206 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets;
import android.appwidget.*;
import android.content.*;
import android.os.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import java.util.*;
import static android.appwidget.AppWidgetManager.*;
import static org.isoron.uhabits.utils.InterfaceUtils.dpToPixels;
public abstract class BaseWidgetProvider extends AppWidgetProvider
{
private HabitList habits;
private Preferences preferences;
private WidgetPreferences widgetPrefs;
public static void updateAppWidget(@NonNull AppWidgetManager manager,
@NonNull BaseWidget widget)
{
RemoteViews landscape = widget.getLandscapeRemoteViews();
RemoteViews portrait = widget.getPortraitRemoteViews();
RemoteViews views = new RemoteViews(landscape, portrait);
manager.updateAppWidget(widget.getId(), views);
}
@NonNull
public WidgetDimensions getDimensionsFromOptions(@NonNull Context ctx,
@NonNull Bundle options)
{
int maxWidth =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
int maxHeight =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
int minWidth =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
int minHeight =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight);
}
@Override
public void onAppWidgetOptionsChanged(@Nullable Context context,
@Nullable AppWidgetManager manager,
int widgetId,
@Nullable Bundle options)
{
try
{
if (context == null) throw new RuntimeException("context is null");
if (manager == null) throw new RuntimeException("manager is null");
if (options == null) throw new RuntimeException("options is null");
updateDependencies(context);
context.setTheme(R.style.WidgetTheme);
BaseWidget widget = getWidgetFromId(context, widgetId);
WidgetDimensions dims = getDimensionsFromOptions(context, options);
widget.setDimensions(dims);
updateAppWidget(manager, widget);
}
catch (RuntimeException e)
{
drawErrorWidget(context, manager, widgetId, e);
e.printStackTrace();
}
}
@Override
public void onDeleted(@Nullable Context context, @Nullable int[] ids)
{
if (context == null) throw new RuntimeException("context is null");
if (ids == null) throw new RuntimeException("ids is null");
updateDependencies(context);
for (int id : ids)
{
try
{
BaseWidget widget = getWidgetFromId(context, id);
widget.delete();
}
catch (HabitNotFoundException e)
{
e.printStackTrace();
}
}
}
@Override
public void onUpdate(@Nullable Context context,
@Nullable AppWidgetManager manager,
@Nullable int[] widgetIds)
{
if (context == null) throw new RuntimeException("context is null");
if (manager == null) throw new RuntimeException("manager is null");
if (widgetIds == null) throw new RuntimeException("widgetIds is null");
updateDependencies(context);
context.setTheme(R.style.WidgetTheme);
new Thread(() ->
{
Looper.prepare();
for (int id : widgetIds)
update(context, manager, id);
}).start();
}
protected List<Habit> getHabitsFromWidgetId(int widgetId)
{
long selectedIds[] = widgetPrefs.getHabitIdsFromWidgetId(widgetId);
ArrayList<Habit> selectedHabits = new ArrayList<>(selectedIds.length);
for (long id : selectedIds)
{
Habit h = habits.getById(id);
if (h == null) throw new HabitNotFoundException();
selectedHabits.add(h);
}
return selectedHabits;
}
@NonNull
protected abstract BaseWidget getWidgetFromId(@NonNull Context context,
int id);
private void drawErrorWidget(Context context,
AppWidgetManager manager,
int widgetId,
RuntimeException e)
{
RemoteViews errorView =
new RemoteViews(context.getPackageName(), R.layout.widget_error);
if (e instanceof HabitNotFoundException)
{
errorView.setCharSequence(R.id.label, "setText",
context.getString(R.string.habit_not_found));
}
manager.updateAppWidget(widgetId, errorView);
}
private void update(@NonNull Context context,
@NonNull AppWidgetManager manager,
int widgetId)
{
try
{
BaseWidget widget = getWidgetFromId(context, widgetId);
Bundle options = manager.getAppWidgetOptions(widgetId);
widget.setDimensions(getDimensionsFromOptions(context, options));
updateAppWidget(manager, widget);
}
catch (RuntimeException e)
{
drawErrorWidget(context, manager, widgetId, e);
e.printStackTrace();
}
}
private void updateDependencies(Context context)
{
HabitsApplication app =
(HabitsApplication) context.getApplicationContext();
habits = app.getComponent().getHabitList();
preferences = app.getComponent().getPreferences();
widgetPrefs = app.getComponent().getWidgetPreferences();
}
public Preferences getPreferences()
{
return preferences;
}
}

@ -0,0 +1,177 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.widget.RemoteViews
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
abstract class BaseWidgetProvider : AppWidgetProvider() {
private lateinit var habits: HabitList
lateinit var preferences: Preferences
private set
private lateinit var widgetPrefs: WidgetPreferences
fun getDimensionsFromOptions(
ctx: Context,
options: Bundle
): WidgetDimensions {
val maxWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).toFloat()
).toInt()
val maxHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).toFloat()
).toInt()
val minWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).toFloat()
).toInt()
val minHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).toFloat()
).toInt()
return WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight)
}
override fun onAppWidgetOptionsChanged(
context: Context,
manager: AppWidgetManager,
widgetId: Int,
options: Bundle
) {
try {
updateDependencies(context)
context.setTheme(R.style.WidgetTheme)
val widget = getWidgetFromId(context, widgetId)
val dims = getDimensionsFromOptions(context, options)
widget.setDimensions(dims)
updateAppWidget(manager, widget)
} catch (e: RuntimeException) {
drawErrorWidget(context, manager, widgetId, e)
e.printStackTrace()
}
}
override fun onDeleted(context: Context?, ids: IntArray?) {
if (context == null) throw RuntimeException("context is null")
if (ids == null) throw RuntimeException("ids is null")
updateDependencies(context)
for (id in ids) {
try {
val widget = getWidgetFromId(context, id)
widget.delete()
} catch (e: HabitNotFoundException) {
e.printStackTrace()
}
}
}
override fun onUpdate(
context: Context,
manager: AppWidgetManager,
widgetIds: IntArray
) {
updateDependencies(context)
context.setTheme(R.style.WidgetTheme)
Thread {
Looper.prepare()
for (id in widgetIds) update(context, manager, id)
}.start()
}
protected fun getHabitsFromWidgetId(widgetId: Int): List<Habit> {
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)
val selectedHabits = ArrayList<Habit>(selectedIds.size)
for (id in selectedIds) {
val h = habits.getById(id) ?: throw HabitNotFoundException()
selectedHabits.add(h)
}
return selectedHabits
}
protected abstract fun getWidgetFromId(
context: Context,
id: Int
): BaseWidget
private fun drawErrorWidget(
context: Context,
manager: AppWidgetManager,
widgetId: Int,
e: RuntimeException
) {
val errorView = RemoteViews(context.packageName, R.layout.widget_error)
if (e is HabitNotFoundException) {
errorView.setCharSequence(
R.id.label,
"setText",
context.getString(R.string.habit_not_found)
)
}
manager.updateAppWidget(widgetId, errorView)
}
private fun update(
context: Context,
manager: AppWidgetManager,
widgetId: Int
) {
try {
val widget = getWidgetFromId(context, widgetId)
val options = manager.getAppWidgetOptions(widgetId)
widget.setDimensions(getDimensionsFromOptions(context, options))
updateAppWidget(manager, widget)
} catch (e: RuntimeException) {
drawErrorWidget(context, manager, widgetId, e)
e.printStackTrace()
}
}
private fun updateDependencies(context: Context) {
val app = context.applicationContext as HabitsApplication
habits = app.component.habitList
preferences = app.component.preferences
widgetPrefs = app.component.widgetPreferences
}
companion object {
fun updateAppWidget(
manager: AppWidgetManager,
widget: BaseWidget
) {
val landscape = widget.landscapeRemoteViews
val portrait = widget.portraitRemoteViews
val views = RemoteViews(landscape, portrait)
manager.updateAppWidget(widget.id, views)
}
}
}

@ -21,7 +21,9 @@ package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.utils.DateUtils
@ -31,10 +33,13 @@ import org.isoron.uhabits.widgets.views.CheckmarkWidgetView
open class CheckmarkWidget(
context: Context,
widgetId: Int,
protected val habit: Habit
protected val habit: Habit,
) : BaseWidget(context, widgetId) {
override fun getOnClickPendingIntent(context: Context): PendingIntent {
override val defaultHeight: Int = 125
override val defaultWidth: Int = 125
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) {
pendingIntentFactory.setNumericalValue(context, habit, 10, null)
} else {
@ -42,20 +47,21 @@ open class CheckmarkWidget(
}
}
override fun refreshData(v: View) {
(v as CheckmarkWidgetView).apply {
@RequiresApi(Build.VERSION_CODES.O)
override fun refreshData(widgetView: View) {
(widgetView as CheckmarkWidgetView).apply {
val today = DateUtils.getTodayWithOffset()
setBackgroundAlpha(preferedBackgroundAlpha)
setActiveColor(habit.color.toThemedAndroidColor(context))
setName(habit.name)
setEntryValue(habit.computedEntries.get(today).value)
activeColor = habit.color.toThemedAndroidColor(context)
name = habit.name
entryValue = habit.computedEntries.get(today).value
if (habit.isNumerical) {
setNumerical(true)
setEntryState(getNumericalEntryState())
isNumerical = true
entryState = getNumericalEntryState()
} else {
setEntryState(habit.computedEntries.get(today).value)
entryState = habit.computedEntries.get(today).value
}
setPercentage(habit.scores.get(today).value.toFloat())
percentage = habit.scores[today].value.toFloat()
refresh()
}
}
@ -64,9 +70,6 @@ open class CheckmarkWidget(
return CheckmarkWidgetView(context)
}
override fun getDefaultHeight() = 125
override fun getDefaultWidth() = 125
private fun getNumericalEntryState(): Int {
return if (habit.isCompletedToday()) {
Entry.YES_MANUAL

@ -19,18 +19,19 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.widgets.views.EmptyWidgetView
class EmptyWidget(
context: Context,
widgetId: Int
widgetId: Int,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) = null
override fun getOnClickPendingIntent(context: Context): PendingIntent? = null
override fun refreshData(v: View) {}
override fun buildView() = EmptyWidgetView(context)
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.FrequencyChart
@ -30,10 +31,12 @@ class FrequencyWidget(
context: Context,
widgetId: Int,
private val habit: Habit,
private val firstWeekday: Int
private val firstWeekday: Int,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent? =
pendingIntentFactory.showHabit(habit)
override fun refreshData(v: View) {
@ -50,7 +53,4 @@ class FrequencyWidget(
override fun buildView() =
GraphWidgetView(context, FrequencyChart(context))
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -28,7 +28,7 @@ class FrequencyWidgetProvider : BaseWidgetProvider() {
context,
id,
habits[0],
preferences.firstWeekdayInt
preferences!!.firstWeekdayInt
)
else return StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
}

@ -35,10 +35,13 @@ import java.util.Locale
class HistoryWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override fun getOnClickPendingIntent(context: Context): PendingIntent {
override val defaultHeight: Int = 250
override val defaultWidth: Int = 250
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return pendingIntentFactory.showHabit(habit)
}
@ -72,7 +75,4 @@ class HistoryWidget(
).apply {
setTitle(habit.name)
}
override fun getDefaultHeight() = 250
override fun getDefaultWidth() = 250
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.ScoreChart
@ -30,10 +31,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class ScoreWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 300
override val defaultWidth: Int = 300
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent? =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
@ -57,7 +60,4 @@ class ScoreWidget(
GraphWidgetView(context, ScoreChart(context)).apply {
setTitle(habit.name)
}
override fun getDefaultHeight() = 300
override fun getDefaultWidth() = 300
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
@ -32,15 +33,22 @@ class StackWidget(
context: Context,
widgetId: Int,
private val widgetType: StackWidgetType,
private val habits: List<Habit>
private val habits: List<Habit>,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 0
override val defaultWidth: Int = 0
override fun getOnClickPendingIntent(context: Context) = null
override fun getOnClickPendingIntent(context: Context): PendingIntent? = null
override fun refreshData(v: View) {
// unused
}
override fun buildView(): View? {
// unused
return null
}
override fun getRemoteViews(width: Int, height: Int): RemoteViews {
val manager = AppWidgetManager.getInstance(context)
val remoteViews = RemoteViews(context.packageName, StackWidgetType.getStackWidgetLayoutId(widgetType))
@ -59,8 +67,4 @@ class StackWidget(
)
return remoteViews
}
override fun buildView() = null // unused
override fun getDefaultHeight() = 0 // unused
override fun getDefaultWidth() = 0 // unused
}

@ -1,186 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets;
import android.appwidget.*;
import android.content.*;
import android.os.*;
import android.util.Log;
import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import static android.appwidget.AppWidgetManager.*;
import static org.isoron.uhabits.utils.InterfaceUtils.dpToPixels;
import static org.isoron.uhabits.widgets.StackWidgetService.*;
public class StackWidgetService extends RemoteViewsService
{
public static final String WIDGET_TYPE = "WIDGET_TYPE";
public static final String HABIT_IDS = "HABIT_IDS";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent)
{
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory
{
private Context context;
private int widgetId;
private long[] habitIds;
private StackWidgetType widgetType;
private ArrayList<RemoteViews> remoteViews = new ArrayList<>();
public StackRemoteViewsFactory(Context context, Intent intent)
{
this.context = context;
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int widgetTypeValue = intent.getIntExtra(WIDGET_TYPE, -1);
String habitIdsStr = intent.getStringExtra(HABIT_IDS);
if (widgetTypeValue < 0) throw new RuntimeException("invalid widget type");
if (habitIdsStr == null) throw new RuntimeException("habitIdsStr is null");
widgetType = StackWidgetType.getWidgetTypeFromValue(widgetTypeValue);
habitIds = StringUtils.splitLongs(habitIdsStr);
}
public void onCreate()
{
}
public void onDestroy()
{
}
public int getCount()
{
return habitIds.length;
}
@NonNull
public WidgetDimensions getDimensionsFromOptions(@NonNull Context ctx,
@NonNull Bundle options)
{
int maxWidth = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
int maxHeight = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
int minWidth = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
int minHeight = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight);
}
public RemoteViews getViewAt(int position)
{
Log.i("StackRemoteViewsFactory", "getViewAt " + position);
if (position < 0 || position > remoteViews.size()) return null;
return remoteViews.get(position);
}
@NonNull
private BaseWidget constructWidget(@NonNull Habit habit,
@NonNull Preferences prefs)
{
switch (widgetType)
{
case CHECKMARK:
return new CheckmarkWidget(context, widgetId, habit);
case FREQUENCY:
return new FrequencyWidget(context, widgetId, habit, prefs.getFirstWeekdayInt());
case SCORE:
return new ScoreWidget(context, widgetId, habit);
case HISTORY:
return new HistoryWidget(context, widgetId, habit);
case STREAKS:
return new StreakWidget(context, widgetId, habit);
}
throw new IllegalStateException();
}
public RemoteViews getLoadingView()
{
Bundle options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId);
EmptyWidget widget = new EmptyWidget(context, widgetId);
widget.setDimensions(getDimensionsFromOptions(context, options));
RemoteViews landscapeViews = widget.getLandscapeRemoteViews();
RemoteViews portraitViews = widget.getPortraitRemoteViews();
return new RemoteViews(landscapeViews, portraitViews);
}
public int getViewTypeCount()
{
return 1;
}
public long getItemId(int position)
{
return habitIds[position];
}
public boolean hasStableIds()
{
return true;
}
public void onDataSetChanged()
{
Log.i("StackRemoteViewsFactory", "onDataSetChanged started");
HabitsApplication app = (HabitsApplication) context.getApplicationContext();
Preferences prefs = app.getComponent().getPreferences();
HabitList habitList = app.getComponent().getHabitList();
Bundle options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId);
ArrayList<RemoteViews> newRemoteViews = new ArrayList<>();
if (Looper.myLooper() == null) Looper.prepare();
for (long id : habitIds)
{
Habit h = habitList.getById(id);
if (h == null) throw new HabitNotFoundException();
BaseWidget widget = constructWidget(h, prefs);
widget.setDimensions(getDimensionsFromOptions(context, options));
RemoteViews landscapeViews = widget.getLandscapeRemoteViews();
RemoteViews portraitViews = widget.getPortraitRemoteViews();
newRemoteViews.add(new RemoteViews(landscapeViews, portraitViews));
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget " + id);
}
remoteViews = newRemoteViews;
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended");
}
}

@ -0,0 +1,162 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return StackRemoteViewsFactory(this.applicationContext, intent)
}
companion object {
const val WIDGET_TYPE = "WIDGET_TYPE"
const val HABIT_IDS = "HABIT_IDS"
}
}
internal class StackRemoteViewsFactory(private val context: Context, intent: Intent) :
RemoteViewsFactory {
private val widgetId: Int
private val habitIds: LongArray
private val widgetType: StackWidgetType?
private var remoteViews = ArrayList<RemoteViews>()
override fun onCreate() {}
override fun onDestroy() {}
override fun getCount(): Int {
return habitIds.size
}
fun getDimensionsFromOptions(
ctx: Context,
options: Bundle
): WidgetDimensions {
val maxWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).toFloat()
).toInt()
val maxHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).toFloat()
).toInt()
val minWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).toFloat()
).toInt()
val minHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).toFloat()
).toInt()
return WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight)
}
override fun getViewAt(position: Int): RemoteViews? {
Log.i("StackRemoteViewsFactory", "getViewAt $position")
return if (position < 0 || position > remoteViews.size) null else remoteViews[position]
}
private fun constructWidget(
habit: Habit,
prefs: Preferences
): BaseWidget {
when (widgetType) {
StackWidgetType.CHECKMARK -> return CheckmarkWidget(context, widgetId, habit)
StackWidgetType.FREQUENCY -> return FrequencyWidget(
context,
widgetId,
habit,
prefs.firstWeekdayInt
)
StackWidgetType.SCORE -> return ScoreWidget(context, widgetId, habit)
StackWidgetType.HISTORY -> return HistoryWidget(context, widgetId, habit)
StackWidgetType.STREAKS -> return StreakWidget(context, widgetId, habit)
}
throw IllegalStateException()
}
override fun getLoadingView(): RemoteViews {
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val widget = EmptyWidget(context, widgetId)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
return RemoteViews(landscapeViews, portraitViews)
}
override fun getViewTypeCount(): Int {
return 1
}
override fun getItemId(position: Int): Long {
return habitIds[position]
}
override fun hasStableIds(): Boolean {
return true
}
override fun onDataSetChanged() {
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val newRemoteViews = ArrayList<RemoteViews>()
if (Looper.myLooper() == null) Looper.prepare()
for (id in habitIds) {
val h = habitList.getById(id) ?: throw HabitNotFoundException()
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
}
remoteViews = newRemoteViews
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
}
init {
widgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
val widgetTypeValue = intent.getIntExtra(StackWidgetService.WIDGET_TYPE, -1)
val habitIdsStr = intent.getStringExtra(StackWidgetService.HABIT_IDS)
if (widgetTypeValue < 0) throw RuntimeException("invalid widget type")
if (habitIdsStr == null) throw RuntimeException("habitIdsStr is null")
widgetType = StackWidgetType.Companion.getWidgetTypeFromValue(widgetTypeValue)
habitIds = splitLongs(habitIdsStr)
}
}

@ -1,117 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets;
import org.isoron.uhabits.R;
/**
* Created by victoryu on 11/3/17.
*/
public enum StackWidgetType {
CHECKMARK(0),
FREQUENCY(1),
SCORE(2), // habit strength widget
HISTORY(3),
STREAKS(4),
TARGET(5);
private int value;
StackWidgetType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static StackWidgetType getWidgetTypeFromValue(int value) {
if (CHECKMARK.getValue() == value) {
return CHECKMARK;
} else if (FREQUENCY.getValue() == value) {
return FREQUENCY;
} else if (SCORE.getValue() == value) {
return SCORE;
} else if (HISTORY.getValue() == value) {
return HISTORY;
} else if (STREAKS.getValue() == value) {
return STREAKS;
} else if (TARGET.getValue() == value) {
return TARGET;
}
return null;
}
public static int getStackWidgetLayoutId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.layout.checkmark_stackview_widget;
case FREQUENCY:
return R.layout.frequency_stackview_widget;
case SCORE:
return R.layout.score_stackview_widget;
case HISTORY:
return R.layout.history_stackview_widget;
case STREAKS:
return R.layout.streak_stackview_widget;
case TARGET:
return R.layout.target_stackview_widget;
}
return 0;
}
public static int getStackWidgetAdapterViewId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.id.checkmarkStackWidgetView;
case FREQUENCY:
return R.id.frequencyStackWidgetView;
case SCORE:
return R.id.scoreStackWidgetView;
case HISTORY:
return R.id.historyStackWidgetView;
case STREAKS:
return R.id.streakStackWidgetView;
case TARGET:
return R.id.targetStackWidgetView;
}
return 0;
}
public static int getStackWidgetEmptyViewId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.id.checkmarkStackWidgetEmptyView;
case FREQUENCY:
return R.id.frequencyStackWidgetEmptyView;
case SCORE:
return R.id.scoreStackWidgetEmptyView;
case HISTORY:
return R.id.historyStackWidgetEmptyView;
case STREAKS:
return R.id.streakStackWidgetEmptyView;
case TARGET:
return R.id.targetStackWidgetEmptyView;
}
return 0;
}
}

@ -0,0 +1,79 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets
import org.isoron.uhabits.R
/**
* Created by victoryu on 11/3/17.
*/
enum class StackWidgetType(val value: Int) {
CHECKMARK(0), FREQUENCY(1), SCORE(2), // habit strength widget
HISTORY(3), STREAKS(4), TARGET(5);
companion object {
fun getWidgetTypeFromValue(value: Int): StackWidgetType? {
return when {
CHECKMARK.value == value -> CHECKMARK
FREQUENCY.value == value -> FREQUENCY
SCORE.value == value -> SCORE
HISTORY.value == value -> HISTORY
STREAKS.value == value -> STREAKS
TARGET.value == value -> TARGET
else -> null
}
}
fun getStackWidgetLayoutId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.layout.checkmark_stackview_widget
FREQUENCY -> return R.layout.frequency_stackview_widget
SCORE -> return R.layout.score_stackview_widget
HISTORY -> return R.layout.history_stackview_widget
STREAKS -> return R.layout.streak_stackview_widget
TARGET -> return R.layout.target_stackview_widget
}
return 0
}
fun getStackWidgetAdapterViewId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.id.checkmarkStackWidgetView
FREQUENCY -> return R.id.frequencyStackWidgetView
SCORE -> return R.id.scoreStackWidgetView
HISTORY -> return R.id.historyStackWidgetView
STREAKS -> return R.id.streakStackWidgetView
TARGET -> return R.id.targetStackWidgetView
}
return 0
}
fun getStackWidgetEmptyViewId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.id.checkmarkStackWidgetEmptyView
FREQUENCY -> return R.id.frequencyStackWidgetEmptyView
SCORE -> return R.id.scoreStackWidgetEmptyView
HISTORY -> return R.id.historyStackWidgetEmptyView
STREAKS -> return R.id.streakStackWidgetEmptyView
TARGET -> return R.id.targetStackWidgetEmptyView
}
return 0
}
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
@ -31,10 +32,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class StreakWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent? =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
@ -53,7 +56,4 @@ class StreakWidget(
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
}
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
@ -34,10 +35,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class TargetWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent? =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) = runBlocking {
@ -58,7 +61,4 @@ class TargetWidget(
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
}
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -1,225 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views;
import android.content.*;
import android.util.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.inject.*;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.getDimension;
public class CheckmarkWidgetView extends HabitWidgetView {
protected int activeColor;
protected float percentage;
@Nullable
protected String name;
protected RingView ring;
protected TextView label;
protected int entryValue;
protected int entryState;
protected boolean isNumerical;
private Preferences preferences;
public CheckmarkWidgetView(Context context)
{
super(context);
init();
}
public CheckmarkWidgetView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void refresh()
{
if (backgroundPaint == null || frame == null || ring == null) return;
StyledResources res = new StyledResources(getContext());
int bgColor;
int fgColor;
switch (entryState) {
case Entry.YES_MANUAL:
case Entry.SKIP:
bgColor = activeColor;
fgColor = res.getColor(R.attr.highContrastReverseTextColor);
setShadowAlpha(0x4f);
backgroundPaint.setColor(bgColor);
frame.setBackgroundDrawable(background);
break;
case Entry.YES_AUTO:
case Entry.NO:
case Entry.UNKNOWN:
default:
bgColor = res.getColor(R.attr.cardBgColor);
fgColor = res.getColor(R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
break;
}
ring.setPercentage(percentage);
ring.setColor(fgColor);
ring.setBackgroundColor(bgColor);
ring.setText(getText());
label.setText(name);
label.setTextColor(fgColor);
requestLayout();
postInvalidate();
}
public void setEntryState(int entryState)
{
this.entryState = entryState;
}
protected String getText()
{
if (isNumerical) return NumberButtonViewKt.toShortString(entryValue / 1000.0);
switch (entryState) {
case Entry.YES_MANUAL:
case Entry.YES_AUTO:
return getResources().getString(R.string.fa_check);
case Entry.SKIP:
return getResources().getString(R.string.fa_skipped);
case Entry.UNKNOWN:
{
if (preferences.areQuestionMarksEnabled())
return getResources().getString(R.string.fa_question);
else
getResources().getString(R.string.fa_times);
}
case Entry.NO:
default:
return getResources().getString(R.string.fa_times);
}
}
public void setActiveColor(int activeColor)
{
this.activeColor = activeColor;
}
public void setEntryValue(int entryValue)
{
this.entryValue = entryValue;
}
public void setName(@NonNull String name)
{
this.name = name;
}
public void setPercentage(float percentage)
{
this.percentage = percentage;
}
public void setNumerical(boolean isNumerical)
{
this.isNumerical = isNumerical;
}
@Override
@NonNull
protected Integer getInnerLayoutId()
{
return R.layout.widget_checkmark;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
float w = width;
float h = width * 1.25f;
float scale = Math.min(width / w, height / h);
w *= scale;
h *= scale;
if (h < getDimension(getContext(), R.dimen.checkmarkWidget_heightBreakpoint))
ring.setVisibility(GONE);
else
ring.setVisibility(VISIBLE);
widthMeasureSpec =
MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY);
heightMeasureSpec =
MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY);
float textSize = 0.15f * h;
float maxTextSize = getDimension(getContext(), R.dimen.smallerTextSize);
textSize = Math.min(textSize, maxTextSize);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
ring.setTextSize(textSize);
ring.setThickness(0.15f * textSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void init()
{
HabitsApplicationComponent appComponent;
appComponent = ((HabitsApplication) getContext().getApplicationContext()).getComponent();
preferences = appComponent.getPreferences();
ring = (RingView) findViewById(R.id.scoreRing);
label = (TextView) findViewById(R.id.label);
if (ring != null) ring.setIsTransparencyEnabled(true);
if (isInEditMode())
{
percentage = 0.75f;
name = "Wake up early";
activeColor = PaletteUtils.getAndroidTestColor(6);
entryValue = Entry.YES_MANUAL;
refresh();
}
}
}

@ -0,0 +1,156 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
import org.isoron.uhabits.utils.StyledResources
class CheckmarkWidgetView : HabitWidgetView {
var activeColor: Int = 0
var percentage = 0f
var name: String? = null
protected lateinit var ring: RingView
protected lateinit var label: TextView
var entryValue = 0
var entryState = 0
var isNumerical = false
private var preferences: Preferences? = null
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
fun refresh() {
if (backgroundPaint == null || frame == null) return
val res = StyledResources(context)
val bgColor: Int
val fgColor: Int
when (entryState) {
YES_MANUAL, SKIP -> {
bgColor = activeColor
fgColor = res.getColor(R.attr.highContrastReverseTextColor)
setShadowAlpha(0x4f)
backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background)
}
YES_AUTO, NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.mediumContrastTextColor)
setShadowAlpha(0x00)
}
else -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.mediumContrastTextColor)
setShadowAlpha(0x00)
}
}
ring.percentage = percentage
ring.color = fgColor
ring.setBackgroundColor(bgColor)
ring.setText(text)
label.text = name
label.setTextColor(fgColor)
requestLayout()
postInvalidate()
}
protected val text: String
get() = if (isNumerical) {
(entryValue / 1000.0).toShortString()
} else when (entryState) {
YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check)
SKIP -> resources.getString(R.string.fa_skipped)
UNKNOWN -> {
run {
if (preferences!!.areQuestionMarksEnabled()) {
return resources.getString(R.string.fa_question)
} else {
resources.getString(R.string.fa_times)
}
}
resources.getString(R.string.fa_times)
}
NO -> resources.getString(R.string.fa_times)
else -> resources.getString(R.string.fa_times)
}
override val innerLayoutId: Int
get() = R.layout.widget_checkmark
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec = widthMeasureSpec
var heightMeasureSpec = heightMeasureSpec
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
var w = width.toFloat()
var h = width * 1.25f
val scale = Math.min(width / w, height / h)
w *= scale
h *= scale
if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility =
GONE else ring.visibility = VISIBLE
widthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY)
heightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY)
var textSize = 0.15f * h
val maxTextSize = getDimension(context, R.dimen.smallerTextSize)
textSize = Math.min(textSize, maxTextSize)
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
ring.setTextSize(textSize)
ring.setThickness(0.15f * textSize)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
private fun init() {
val appComponent: HabitsApplicationComponent
appComponent = (context.applicationContext as HabitsApplication).component
preferences = appComponent.preferences
ring = findViewById<View>(R.id.scoreRing) as RingView
label = findViewById<View>(R.id.label) as TextView
ring.setIsTransparencyEnabled(true)
if (isInEditMode) {
percentage = 0.75f
name = "Wake up early"
activeColor = getAndroidTestColor(6)
entryValue = YES_MANUAL
refresh()
}
}
}

@ -16,41 +16,28 @@
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views
package org.isoron.uhabits.widgets.views;
import android.content.Context
import android.view.View
import android.widget.TextView
import org.isoron.uhabits.R
import android.content.Context;
import androidx.annotation.NonNull;
import android.widget.TextView;
import org.isoron.uhabits.R;
public class EmptyWidgetView extends HabitWidgetView
{
private TextView title;
public EmptyWidgetView(Context context)
{
super(context);
init();
class EmptyWidgetView(context: Context?) : HabitWidgetView(context) {
private lateinit var title: TextView
fun setTitle(text: String?) {
title.text = text
}
public void setTitle(String text)
{
title.setText(text);
}
override val innerLayoutId: Int
get() = R.layout.widget_graph
@Override
@NonNull
protected Integer getInnerLayoutId()
{
return R.layout.widget_graph;
private fun init() {
title = findViewById<View>(R.id.title) as TextView
title.visibility = VISIBLE
}
private void init()
{
title = (TextView) findViewById(R.id.title);
title.setVisibility(VISIBLE);
init {
init()
}
}

@ -1,74 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views;
import android.content.*;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
public class GraphWidgetView extends HabitWidgetView
{
private final View dataView;
private TextView title;
public GraphWidgetView(Context context, View dataView)
{
super(context);
this.dataView = dataView;
init();
}
public View getDataView()
{
return dataView;
}
public void setTitle(String text)
{
title.setText(text);
}
@Override
@NonNull
protected Integer getInnerLayoutId()
{
return R.layout.widget_graph;
}
private void init()
{
ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
dataView.setLayoutParams(params);
ViewGroup innerFrame = (ViewGroup) findViewById(R.id.innerFrame);
innerFrame.addView(dataView);
title = (TextView) findViewById(R.id.title);
title.setVisibility(VISIBLE);
}
}

@ -0,0 +1,51 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import org.isoron.uhabits.R
class GraphWidgetView(context: Context?, val dataView: View) : HabitWidgetView(context) {
private lateinit var title: TextView
fun setTitle(text: String?) {
title.text = text
}
override val innerLayoutId: Int
get() = R.layout.widget_graph
private fun init() {
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
dataView.layoutParams = params
val innerFrame = findViewById<View>(R.id.innerFrame) as ViewGroup
innerFrame.addView(dataView)
title = findViewById<View>(R.id.title) as TextView
title.visibility = VISIBLE
}
init {
init()
}
}

@ -1,121 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views;
import android.content.*;
import android.graphics.*;
import android.graphics.drawable.*;
import android.graphics.drawable.shapes.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import androidx.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*;
public abstract class HabitWidgetView extends FrameLayout
{
@Nullable
protected InsetDrawable background;
@Nullable
protected Paint backgroundPaint;
protected ViewGroup frame;
private int shadowAlpha;
private StyledResources res;
private int backgroundAlpha;
public HabitWidgetView(Context context)
{
super(context);
init();
}
public HabitWidgetView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void setShadowAlpha(int shadowAlpha)
{
this.shadowAlpha = shadowAlpha;
rebuildBackground();
}
public void setBackgroundAlpha(int backgroundAlpha)
{
this.backgroundAlpha = backgroundAlpha;
rebuildBackground();
}
protected abstract
@NonNull
Integer getInnerLayoutId();
public void rebuildBackground()
{
Context context = getContext();
int shadowRadius = (int) dpToPixels(context, 2);
int shadowOffset = (int) dpToPixels(context, 1);
int shadowColor = Color.argb(shadowAlpha, 0, 0, 0);
float cornerRadius = dpToPixels(context, 5);
float[] radii = new float[8];
Arrays.fill(radii, cornerRadius);
RoundRectShape shape = new RoundRectShape(radii, null, null);
ShapeDrawable innerDrawable = new ShapeDrawable(shape);
int insetLeftTop = Math.max(shadowRadius - shadowOffset, 0);
int insetRightBottom = shadowRadius + shadowOffset;
background =
new InsetDrawable(innerDrawable, insetLeftTop, insetLeftTop,
insetRightBottom, insetRightBottom);
backgroundPaint = innerDrawable.getPaint();
backgroundPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset,
shadowColor);
backgroundPaint.setColor(res.getColor(R.attr.cardBgColor));
backgroundPaint.setAlpha(backgroundAlpha);
frame = (ViewGroup) findViewById(R.id.frame);
if (frame != null) frame.setBackground(background);
}
private void init()
{
inflate(getContext(), getInnerLayoutId(), this);
res = new StyledResources(getContext());
shadowAlpha = (int) (255 * res.getFloat(R.attr.widgetShadowAlpha));
rebuildBackground();
}
}

@ -0,0 +1,104 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.views
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import org.isoron.uhabits.utils.StyledResources
import java.util.Arrays
abstract class HabitWidgetView : FrameLayout {
protected var background: InsetDrawable? = null
protected var backgroundPaint: Paint? = null
protected var frame: ViewGroup? = null
private var shadowAlpha = 0
private var res: StyledResources? = null
private var backgroundAlpha = 0
constructor(context: Context?) : super(context!!) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(
context!!,
attrs
) {
init()
}
fun setShadowAlpha(shadowAlpha: Int) {
this.shadowAlpha = shadowAlpha
rebuildBackground()
}
fun setBackgroundAlpha(backgroundAlpha: Int) {
this.backgroundAlpha = backgroundAlpha
rebuildBackground()
}
protected abstract val innerLayoutId: Int
fun rebuildBackground() {
val context = context
val shadowRadius = dpToPixels(context, 2f).toInt()
val shadowOffset = dpToPixels(context, 1f).toInt()
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
val cornerRadius = dpToPixels(context, 5f)
val radii = FloatArray(8)
Arrays.fill(radii, cornerRadius)
val shape = RoundRectShape(radii, null, null)
val innerDrawable = ShapeDrawable(shape)
val insetLeftTop = Math.max(shadowRadius - shadowOffset, 0)
val insetRightBottom = shadowRadius + shadowOffset
background = InsetDrawable(
innerDrawable,
insetLeftTop,
insetLeftTop,
insetRightBottom,
insetRightBottom
)
backgroundPaint = innerDrawable.paint
backgroundPaint?.setShadowLayer(
shadowRadius.toFloat(),
shadowOffset.toFloat(),
shadowOffset.toFloat(),
shadowColor
)
backgroundPaint?.color = res!!.getColor(R.attr.cardBgColor)
backgroundPaint?.alpha = backgroundAlpha
frame = findViewById<View>(R.id.frame) as ViewGroup
if (frame != null) frame!!.background = background
}
private fun init() {
inflate(context, innerLayoutId, this)
res = StyledResources(context)
shadowAlpha = (255 * res!!.getFloat(R.attr.widgetShadowAlpha)).toInt()
rebuildBackground()
}
}
Loading…
Cancel
Save