From b8c73afe355757b7e6e1a2f4c96639294e4d48b5 Mon Sep 17 00:00:00 2001 From: Erik Johansson Date: Tue, 8 May 2018 20:57:37 +0200 Subject: [PATCH 1/2] Add measurement widget The widget can be added to the home screen and can be configured to show any of the measurements that are enabled. It can also be resized from 1x1 to x1, adapting to the size change and showing a reasonable amount of information at each size. The default size is 4x1. --- README.md | 1 + android_app/app/src/main/AndroidManifest.xml | 14 ++ .../com/health/openscale/core/OpenScale.java | 20 ++ .../core/database/ScaleMeasurementDAO.java | 3 + .../health/openscale/gui/MainActivity.java | 1 + .../gui/fragments/TableFragment.java | 2 +- .../gui/views/FloatMeasurementView.java | 6 +- .../openscale/gui/views/MeasurementView.java | 5 +- .../openscale/gui/widget/WidgetConfigure.java | 120 +++++++++++ .../openscale/gui/widget/WidgetProvider.java | 193 ++++++++++++++++++ .../main/res/drawable-hdpi/appwidget_bg.9.png | Bin 0 -> 6647 bytes .../main/res/drawable-mdpi/appwidget_bg.9.png | Bin 0 -> 6494 bytes .../res/drawable-xhdpi/appwidget_bg.9.png | Bin 0 -> 6955 bytes .../app/src/main/res/layout/widget.xml | 81 ++++++++ .../main/res/layout/widget_configuration.xml | 56 +++++ .../app/src/main/res/values/colors.xml | 2 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/xml/widget_info.xml | 10 + 18 files changed, 513 insertions(+), 4 deletions(-) create mode 100644 android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetConfigure.java create mode 100644 android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetProvider.java create mode 100644 android_app/app/src/main/res/drawable-hdpi/appwidget_bg.9.png create mode 100644 android_app/app/src/main/res/drawable-mdpi/appwidget_bg.9.png create mode 100644 android_app/app/src/main/res/drawable-xhdpi/appwidget_bg.9.png create mode 100644 android_app/app/src/main/res/layout/widget.xml create mode 100644 android_app/app/src/main/res/layout/widget_configuration.xml create mode 100644 android_app/app/src/main/res/xml/widget_info.xml diff --git a/README.md b/README.md index 847ee0a2..ddf692b0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Install [openScale-dev-build.apk](https://github.com/oliexdev/openScale/releases # Features - Logs your body metrics (weight, body fat, body water, muscle percentage, lean body mass, bone mass, BMI, BMR, waist/hip circumference, waist-hip ratio, waist-to-height ratio) +- Widget that can be added to the home screen showing any of the available metrics - Keep track of your diet process - Display all your data on a chart and table - Import or export your data from/into a CSV file diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml index 58ac47e6..30e2c975 100644 --- a/android_app/app/src/main/AndroidManifest.xml +++ b/android_app/app/src/main/AndroidManifest.xml @@ -36,6 +36,20 @@ + + + + + + + + + + + + + 0) { + Intent intent = new Intent(context, WidgetProvider.class); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); + context.sendBroadcast(intent); + } + } + public int addScaleUser(final ScaleUser user) { return (int)userDAO.insert(user); } @@ -247,6 +262,9 @@ public class OpenScale { return scaleMeasurementList; } + public ScaleMeasurement getLatestScaleMeasurement(int userId) { + return measurementDAO.getLatest(userId); + } public ScaleMeasurement[] getTupleScaleData(int id) { @@ -324,6 +342,7 @@ public class OpenScale { } alarmHandler.entryChanged(context, scaleMeasurement); updateScaleData(); + triggerWidgetUpdate(); } else { if (!silent) { Toast.makeText(context, context.getString(R.string.info_new_data_duplicated), Toast.LENGTH_LONG).show(); @@ -374,6 +393,7 @@ public class OpenScale { alarmHandler.entryChanged(context, scaleMeasurement); updateScaleData(); + triggerWidgetUpdate(); } public void deleteScaleData(int id) diff --git a/android_app/app/src/main/java/com/health/openscale/core/database/ScaleMeasurementDAO.java b/android_app/app/src/main/java/com/health/openscale/core/database/ScaleMeasurementDAO.java index e91a8022..dba85587 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/database/ScaleMeasurementDAO.java +++ b/android_app/app/src/main/java/com/health/openscale/core/database/ScaleMeasurementDAO.java @@ -47,6 +47,9 @@ public interface ScaleMeasurementDAO { @Query("SELECT * FROM scaleMeasurements WHERE datetime >= :startYear AND datetime < :endYear AND userId = :userId AND enabled = 1 ORDER BY datetime DESC") List getAllInRange(Date startYear, Date endYear, int userId); + @Query("SELECT * FROM scaleMeasurements WHERE userId = :userId AND enabled = 1 ORDER BY datetime DESC LIMIT 1") + ScaleMeasurement getLatest(int userId); + @Insert (onConflict = OnConflictStrategy.IGNORE) long insert(ScaleMeasurement measurement); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java index 9522a051..2727cb55 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java @@ -196,6 +196,7 @@ public class MainActivity extends BaseAppCompatActivity public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { if (settingsActivityRunning) { recreate(); + OpenScale.getInstance().triggerWidgetUpdate(); } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java index 6b4fa3bb..294410a8 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/TableFragment.java @@ -284,7 +284,7 @@ public class TableFragment extends Fragment implements FragmentUpdateListener { SpannableStringBuilder string = new SpannableStringBuilder(); string.append(visibleMeasurements.get(i).getValueAsString(false)); - visibleMeasurements.get(i).appendDiffValue(string); + visibleMeasurements.get(i).appendDiffValue(string, true); stringCache[position][i] = string; } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java index 2f17e6b2..48396bd2 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/views/FloatMeasurementView.java @@ -405,7 +405,7 @@ public abstract class FloatMeasurementView extends MeasurementView { } @Override - public void appendDiffValue(SpannableStringBuilder text) { + public void appendDiffValue(SpannableStringBuilder text, boolean newLine) { if (previousValue < 0.0f) { return; } @@ -425,7 +425,9 @@ public abstract class FloatMeasurementView extends MeasurementView { color = Color.GRAY; } - text.append('\n'); + if (newLine) { + text.append('\n'); + } int start = text.length(); text.append(symbol); text.setSpan(new ForegroundColorSpan(color), start, text.length(), diff --git a/android_app/app/src/main/java/com/health/openscale/gui/views/MeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/views/MeasurementView.java index d33a03b1..94d185d6 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/views/MeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/views/MeasurementView.java @@ -65,6 +65,7 @@ public abstract class MeasurementView extends TableLayout { private TableRow measurementRow; private ImageView iconView; + private int iconId; private TextView nameView; private TextView valueView; private LinearLayout incDecLayout; @@ -84,6 +85,7 @@ public abstract class MeasurementView extends TableLayout { initView(context); nameView.setText(textId); + this.iconId = iconId; iconView.setImageResource(iconId); } @@ -256,8 +258,9 @@ public abstract class MeasurementView extends TableLayout { public CharSequence getName() { return nameView.getText(); } public abstract String getValueAsString(boolean withUnit); - public void appendDiffValue(SpannableStringBuilder builder) { } + public void appendDiffValue(SpannableStringBuilder builder, boolean newLine) { } public Drawable getIcon() { return iconView.getDrawable(); } + public int getIconResource() { return iconId; } protected boolean isEditable() { return true; diff --git a/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetConfigure.java b/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetConfigure.java new file mode 100644 index 00000000..6019112b --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetConfigure.java @@ -0,0 +1,120 @@ +/* Copyright (C) 2018 Erik Johansson + * + * This program 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. + * + * This program 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 + */ + +package com.health.openscale.gui.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TableRow; + +import com.health.openscale.R; +import com.health.openscale.core.OpenScale; +import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.gui.activities.BaseAppCompatActivity; +import com.health.openscale.gui.views.MeasurementView; + +import java.util.ArrayList; +import java.util.List; + +public class WidgetConfigure extends BaseAppCompatActivity { + private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setResult(RESULT_CANCELED); + + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish(); + } + + setContentView(R.layout.widget_configuration); + + OpenScale openScale = OpenScale.getInstance(); + + // Set up user spinner + final Spinner userSpinner = findViewById(R.id.widget_user_spinner); + List users = new ArrayList<>(); + final List userIds = new ArrayList<>(); + for (ScaleUser scaleUser : openScale.getScaleUserList()) { + users.add(scaleUser.getUserName()); + userIds.add(scaleUser.getId()); + } + ArrayAdapter userAdapter = new ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, users); + userAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + userSpinner.setAdapter(userAdapter); + + // Hide user selector when there's only one user + if (users.size() == 1) { + TableRow row = (TableRow) userSpinner.getParent(); + row.setVisibility(View.GONE); + } + + // Set up measurement spinner + final Spinner measurementSpinner = findViewById(R.id.widget_measurement_spinner); + List measurements = new ArrayList<>(); + final List measurementKeys = new ArrayList<>(); + for (MeasurementView measurementView : MeasurementView.getMeasurementList( + this, MeasurementView.DateTimeOrder.NONE)) { + if (measurementView.isVisible()) { + measurements.add(measurementView.getName().toString()); + measurementKeys.add(measurementView.getKey()); + } + } + ArrayAdapter measurementAdapter = new ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, measurements); + measurementAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + measurementSpinner.setAdapter(measurementAdapter); + + findViewById(R.id.widget_save).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int userId = userIds.get(userSpinner.getSelectedItemPosition()); + String measurementKey = measurementKeys.get(measurementSpinner.getSelectedItemPosition()); + + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit() + .putInt(WidgetProvider.getUserIdPreferenceName(appWidgetId), userId) + .putString(WidgetProvider.getMeasurementPreferenceName(appWidgetId), measurementKey) + .apply(); + + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {appWidgetId}); + sendBroadcast(intent); + + Intent resultValue = new Intent(); + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + setResult(RESULT_OK, resultValue); + + finish(); + } + }); + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetProvider.java b/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetProvider.java new file mode 100644 index 00000000..41e5c78a --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetProvider.java @@ -0,0 +1,193 @@ +/* Copyright (C) 2018 Erik Johansson + * + * This program 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. + * + * This program 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 + */ + +package com.health.openscale.gui.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.SpannableStringBuilder; +import android.util.TypedValue; +import android.view.View; +import android.widget.RemoteViews; + +import com.health.openscale.R; +import com.health.openscale.core.OpenScale; +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.gui.MainActivity; +import com.health.openscale.gui.activities.BaseAppCompatActivity; +import com.health.openscale.gui.views.MeasurementView; + +import java.text.DateFormat; +import java.util.List; + +public class WidgetProvider extends AppWidgetProvider { + List measurementViews; + + public static final String getUserIdPreferenceName(int appWidgetId) { + return String.format("widget_%d_userid", appWidgetId); + } + + public static final String getMeasurementPreferenceName(int appWidgetId) { + return String.format("widget_%d_measurement", appWidgetId); + } + + private void updateWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId, Bundle newOptions) { + // Make sure we use the correct language + context = BaseAppCompatActivity.createBaseContext(context); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + int userId = prefs.getInt(getUserIdPreferenceName(appWidgetId), -1); + String key = prefs.getString(getMeasurementPreferenceName(appWidgetId), ""); + + if (measurementViews == null) { + measurementViews = MeasurementView.getMeasurementList( + context, MeasurementView.DateTimeOrder.NONE); + } + + MeasurementView measurementView = measurementViews.get(0); + for (MeasurementView view : measurementViews) { + if (view.getKey().equals(key)) { + measurementView = view; + break; + } + } + + OpenScale openScale = OpenScale.getInstance(); + ScaleMeasurement latest = openScale.getLatestScaleMeasurement(userId); + if (latest != null) { + ScaleMeasurement previous = openScale.getTupleScaleData(latest.getId())[0]; + measurementView.loadFrom(latest, previous); + } + + final int minWidth = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); + // From https://developer.android.com/guide/practices/ui_guidelines/widget_design + final int twoCellsMinWidth = 110; + final int thirdCellsMinWidth = 180; + final int fourCellsMinWidth = 250; + + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); + + // Show icon in >= two cell mode + if (minWidth >= twoCellsMinWidth) { + views.setImageViewResource(R.id.widget_icon, measurementView.getIconResource()); + views.setViewVisibility(R.id.widget_icon, View.VISIBLE); + } + else { + views.setViewVisibility(R.id.widget_icon, View.GONE); + } + + // Show measurement name in >= four cell mode + if (minWidth >= fourCellsMinWidth) { + views.setTextViewText(R.id.widget_name, measurementView.getName()); + views.setTextViewText(R.id.widget_date, + latest != null + ? DateFormat.getDateTimeInstance( + DateFormat.LONG, DateFormat.SHORT).format(latest.getDateTime()) + : ""); + views.setViewVisibility(R.id.widget_name_date_layout, View.VISIBLE); + } + else { + views.setViewVisibility(R.id.widget_name_date_layout, View.GONE); + } + + // Always show value, but use smaller font in once cell mode + views.setTextViewText(R.id.widget_value, measurementView.getValueAsString(true)); + SpannableStringBuilder delta = new SpannableStringBuilder(); + measurementView.appendDiffValue(delta, false); + views.setTextViewText(R.id.widget_delta, delta.toString()); + + if (minWidth >= thirdCellsMinWidth) { + views.setTextViewTextSize(R.id.widget_value, TypedValue.COMPLEX_UNIT_DIP, 18); + views.setTextViewTextSize(R.id.widget_delta, TypedValue.COMPLEX_UNIT_DIP, 17); + } + else if (minWidth >= twoCellsMinWidth) { + views.setTextViewTextSize(R.id.widget_value, TypedValue.COMPLEX_UNIT_DIP, 17); + views.setTextViewTextSize(R.id.widget_delta, TypedValue.COMPLEX_UNIT_DIP, 15); + } + else { + views.setTextViewTextSize(R.id.widget_value, TypedValue.COMPLEX_UNIT_DIP, 15); + views.setTextViewTextSize(R.id.widget_delta, TypedValue.COMPLEX_UNIT_DIP, 13); + } + + // Start main activity when widget is clicked + Intent intent = new Intent(context, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent); + + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + for (int appWidgetId : appWidgetIds) { + Bundle newOptions = appWidgetManager.getAppWidgetOptions(appWidgetId); + updateWidget(context, appWidgetManager, appWidgetId, newOptions); + } + } + + @Override + public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, + int appWidgetId, Bundle newOptions) { + updateWidget(context, appWidgetManager, appWidgetId, newOptions); + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(context).edit(); + for (int appWidgetId : appWidgetIds) { + editor.remove(getUserIdPreferenceName(appWidgetId)); + editor.remove(getMeasurementPreferenceName(appWidgetId)); + } + editor.apply(); + } + + @Override + public void onDisabled(Context context) { + measurementViews = null; + } + + @Override + public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + + for (int i = 0; i < oldWidgetIds.length; ++i) { + String oldKey = getUserIdPreferenceName(oldWidgetIds[i]); + if (prefs.contains(oldKey)) { + editor.putInt(getUserIdPreferenceName(newWidgetIds[i]), + prefs.getInt(oldKey, -1)); + editor.remove(oldKey); + } + + oldKey = getMeasurementPreferenceName(oldWidgetIds[i]); + if (prefs.contains(oldKey)) { + editor.putString(getMeasurementPreferenceName(newWidgetIds[i]), + prefs.getString(oldKey, "")); + editor.remove(oldKey); + } + } + + editor.apply(); + } +} diff --git a/android_app/app/src/main/res/drawable-hdpi/appwidget_bg.9.png b/android_app/app/src/main/res/drawable-hdpi/appwidget_bg.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ae6cec22c97001fb64cffe53c536e3e889a80f3e GIT binary patch literal 6647 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 literal 0 HcmV?d00001 diff --git a/android_app/app/src/main/res/drawable-mdpi/appwidget_bg.9.png b/android_app/app/src/main/res/drawable-mdpi/appwidget_bg.9.png new file mode 100644 index 0000000000000000000000000000000000000000..da8f77e3321bb29fae50c3224f2d30428eeec735 GIT binary patch literal 6494 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 + + + + + + + + + + + + + + + + + + + + + diff --git a/android_app/app/src/main/res/layout/widget_configuration.xml b/android_app/app/src/main/res/layout/widget_configuration.xml new file mode 100644 index 00000000..5cfd9b5d --- /dev/null +++ b/android_app/app/src/main/res/layout/widget_configuration.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + +