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..49375144 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 @@ -20,6 +20,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.PreferenceManager; @@ -65,6 +66,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 +86,7 @@ public abstract class MeasurementView extends TableLayout { initView(context); nameView.setText(textId); + this.iconId = iconId; iconView.setImageResource(iconId); } @@ -256,8 +259,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; @@ -319,6 +323,11 @@ public abstract class MeasurementView extends TableLayout { return valueView.getCurrentTextColor(); } + public int getIndicatorColor() { + ColorDrawable background = (ColorDrawable)indicatorView.getBackground(); + return background.getColor(); + } + protected void showEvaluatorRow(boolean show) { if (show) { evaluatorRow.setVisibility(View.VISIBLE); 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..73c8bbb8 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/gui/widget/WidgetProvider.java @@ -0,0 +1,198 @@ +/* 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); + + views.setInt(R.id.indicator_view, "setBackgroundColor", measurementView.getIndicatorColor()); + + // 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); + views.setViewVisibility(R.id.widget_icon_vertical, View.GONE); + } + else { + views.setImageViewResource(R.id.widget_icon_vertical, measurementView.getIconResource()); + views.setViewVisibility(R.id.widget_icon_vertical, View.VISIBLE); + 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 00000000..ae6cec22 Binary files /dev/null and b/android_app/app/src/main/res/drawable-hdpi/appwidget_bg.9.png differ 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 00000000..da8f77e3 Binary files /dev/null and b/android_app/app/src/main/res/drawable-mdpi/appwidget_bg.9.png differ diff --git a/android_app/app/src/main/res/drawable-xhdpi/appwidget_bg.9.png b/android_app/app/src/main/res/drawable-xhdpi/appwidget_bg.9.png new file mode 100644 index 00000000..69fa70c1 Binary files /dev/null and b/android_app/app/src/main/res/drawable-xhdpi/appwidget_bg.9.png differ diff --git a/android_app/app/src/main/res/layout/widget.xml b/android_app/app/src/main/res/layout/widget.xml new file mode 100644 index 00000000..2c10142b --- /dev/null +++ b/android_app/app/src/main/res/layout/widget.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + +