1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-09-02 21:02:48 +02:00

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 <max width>x1, adapting to the size change and showing a
reasonable amount of information at each size. The default size is
4x1.
This commit is contained in:
Erik Johansson
2018-05-08 20:57:37 +02:00
parent e66e5beee8
commit b8c73afe35
18 changed files with 513 additions and 4 deletions

View File

@@ -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

View File

@@ -36,6 +36,20 @@
</intent-filter>
</receiver>
<receiver android:name=".gui.widget.WidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<activity android:name=".gui.widget.WidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@@ -16,10 +16,13 @@
package com.health.openscale.core;
import android.appwidget.AppWidgetManager;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabaseCorruptException;
@@ -52,6 +55,7 @@ import com.health.openscale.gui.views.FatMeasurementView;
import com.health.openscale.gui.views.LBMMeasurementView;
import com.health.openscale.gui.views.MeasurementViewSettings;
import com.health.openscale.gui.views.WaterMeasurementView;
import com.health.openscale.gui.widget.WidgetProvider;
import java.io.BufferedReader;
import java.io.File;
@@ -166,6 +170,17 @@ public class OpenScale {
scaleDB.close();
}
public void triggerWidgetUpdate() {
int[] ids = AppWidgetManager.getInstance(context).getAppWidgetIds(
new ComponentName(context, WidgetProvider.class));
if (ids.length > 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)

View File

@@ -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<ScaleMeasurement> 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);

View File

@@ -196,6 +196,7 @@ public class MainActivity extends BaseAppCompatActivity
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (settingsActivityRunning) {
recreate();
OpenScale.getInstance().triggerWidgetUpdate();
}
}

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -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;

View File

@@ -0,0 +1,120 @@
/* Copyright (C) 2018 Erik Johansson <erik@ejohansson.se>
*
* 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 <http://www.gnu.org/licenses/>
*/
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<String> users = new ArrayList<>();
final List<Integer> userIds = new ArrayList<>();
for (ScaleUser scaleUser : openScale.getScaleUserList()) {
users.add(scaleUser.getUserName());
userIds.add(scaleUser.getId());
}
ArrayAdapter<String> 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<String> measurements = new ArrayList<>();
final List<String> 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<String> 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();
}
});
}
}

View File

@@ -0,0 +1,193 @@
/* Copyright (C) 2018 Erik Johansson <erik@ejohansson.se>
*
* 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 <http://www.gnu.org/licenses/>
*/
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<MeasurementView> 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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingEnd="0dp"
android:paddingStart="0dp"
android:paddingTop="8dp">
<LinearLayout
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/appwidget_bg"
android:orientation="horizontal">
<ImageView
android:id="@+id/widget_icon"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:maxWidth="40dp"
android:paddingEnd="8dp"
android:paddingStart="8dp" />
<LinearLayout
android:id="@+id/widget_name_date_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="3"
android:orientation="vertical">
<TextView
android:id="@+id/widget_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="@color/widgetTextColor"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/widget_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="@color/widgetTextColor"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:minWidth="40dp"
android:orientation="vertical"
android:paddingEnd="8dp"
android:paddingStart="4dp">
<TextView
android:id="@+id/widget_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:textColor="@color/widgetTextColor"
android:textStyle="bold" />
<TextView
android:id="@+id/widget_delta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:lines="1"
android:textColor="@color/widgetTextColor" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/label_configure_widget" />
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:stretchColumns="*">
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_select_user" />
<Spinner
android:id="@+id/widget_user_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</TableRow>
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_select_measurement" />
<Spinner
android:id="@+id/widget_measurement_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</TableRow>
</TableLayout>
<Button
android:id="@+id/widget_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/save" />
</LinearLayout>

View File

@@ -7,4 +7,6 @@
<color name="primaryColor">#000000</color>
<color name="primaryLightColor">@android:color/holo_blue_light</color>
<color name="primaryDarkColor">#000000</color>
<color name="widgetTextColor">#555</color>
</resources>

View File

@@ -219,4 +219,7 @@
<string name="label_development">Development</string>
<string name="label_debug_log">Save debug log to file</string>
<string name="label_your_bluetooth_scale">Your Bluetooth scale</string>
<string name="label_select_measurement">Select measurement</string>
<string name="label_select_user">Select user</string>
<string name="label_configure_widget">Configure widget</string>
</resources>

View File

@@ -0,0 +1,10 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minHeight="40dp"
android:minResizeWidth="40dp"
android:updatePeriodMillis="0"
android:configure="com.health.openscale.gui.widget.WidgetConfigure"
android:initialLayout="@layout/widget"
android:resizeMode="horizontal"
android:widgetCategory="home_screen">
</appwidget-provider>