mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-24 17:45:50 +01:00
Dashboard view (#3478)
This adds a new dashboard-type view to Gadgetbridge. The new dashboard activity displays several widgets with aggregated statistics from multiple devices. New preferences are added to allow configuration of the dashboard and its widgets. A new bottom navigation bar is added to switch between the Dashboard and Devices views. Some issues that prompted this feature and provided inspiration for the implementation: - https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/301 (More Intuitive User Interface) - https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/3074 (Ability to merge historical data from several devices) Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3478 Reviewed-by: José Rebelo <joserebelo@noreply.codeberg.org> Co-authored-by: Arjan Schrijver <a_gadgetbridge@anymore.nl> Co-committed-by: Arjan Schrijver <a_gadgetbridge@anymore.nl>
This commit is contained in:
parent
e4cac887cc
commit
43fddd0110
@ -222,12 +222,14 @@ dependencies {
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
implementation "androidx.legacy:legacy-support-v4:1.0.0"
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation "androidx.palette:palette:1.0.0"
|
||||
implementation "androidx.activity:activity:1.7.2"
|
||||
implementation "androidx.fragment:fragment:1.6.1"
|
||||
implementation "androidx.fragment:fragment:1.6.2"
|
||||
implementation "androidx.navigation:navigation-fragment:2.6.0"
|
||||
implementation "androidx.navigation:navigation-ui:2.6.0"
|
||||
|
||||
implementation "com.google.android.material:material:1.9.0"
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
|
@ -128,6 +128,10 @@
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.DashboardPreferencesActivity"
|
||||
android:label="@string/dashboard_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AboutUserPreferencesActivity"
|
||||
android:label="@string/activity_prefs_about_you"
|
||||
@ -848,6 +852,11 @@
|
||||
android:name=".externalevents.opentracks.OpenTracksController"
|
||||
android:label="OpenTracks controller and intent receiver"
|
||||
android:exported="true"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.dashboard.DashboardCalendarActivity"
|
||||
android:label="@string/menuitem_calendar"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -25,7 +25,6 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
|
@ -20,7 +20,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
@ -53,7 +52,6 @@ import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.view.menu.MenuItemImpl;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@ -61,12 +59,14 @@ import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -74,9 +74,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -87,31 +84,26 @@ import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBChangeLog;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBChangeLog;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
//TODO: extend AbstractGBActivity, but it requires actionbar that is not available
|
||||
public class ControlCenterv2 extends AppCompatActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ControlCenterv2.class);
|
||||
public static final int MENU_REFRESH_CODE = 1;
|
||||
public static final String ACTION_REQUEST_PERMISSIONS
|
||||
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestpermissions";
|
||||
public static final String ACTION_REQUEST_LOCATION_PERMISSIONS
|
||||
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestlocationpermissions";
|
||||
private boolean isLanguageInvalid = false;
|
||||
private boolean isThemeInvalid = false;
|
||||
private static PhoneStateListener fakeStateListener;
|
||||
|
||||
//needed for KK compatibility
|
||||
@ -119,15 +111,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
private DeviceManager deviceManager;
|
||||
private GBDeviceAdapterv2 mGBDeviceAdapter;
|
||||
private RecyclerView deviceListView;
|
||||
private FloatingActionButton fab;
|
||||
private boolean isLanguageInvalid = false;
|
||||
private boolean isThemeInvalid = false;
|
||||
List<GBDevice> deviceList;
|
||||
private HashMap<String,long[]> deviceActivityHashMap = new HashMap();
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@ -142,12 +125,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
case GBApplication.ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
case DeviceManager.ACTION_DEVICES_CHANGED:
|
||||
case GBApplication.ACTION_NEW_DATA:
|
||||
createRefreshTask("get activity data", getApplication()).execute();
|
||||
mGBDeviceAdapter.rebuildFolders();
|
||||
refreshPairedDevices();
|
||||
break;
|
||||
case DeviceService.ACTION_REALTIME_SAMPLES:
|
||||
handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
|
||||
break;
|
||||
@ -157,7 +134,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
case ACTION_REQUEST_LOCATION_PERMISSIONS:
|
||||
checkAndRequestLocationPermissions();
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -171,7 +147,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
private void setCurrentHRSample(ActivitySample sample) {
|
||||
if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) {
|
||||
currentHRSample = sample;
|
||||
refreshPairedDevices();
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,11 +160,42 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_controlcenterv2);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
boolean activityTrackerAvailable = false;
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
for (GBDevice dev : devices) {
|
||||
if (dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
activityTrackerAvailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
NavHostFragment navHostFragment = (NavHostFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
NavController navController = navHostFragment.getNavController();
|
||||
if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) {
|
||||
NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.main);
|
||||
navGraph.setStartDestination(R.id.bottom_nav_devices);
|
||||
navController.setGraph(navGraph);
|
||||
}
|
||||
BottomNavigationView navigationView = findViewById(R.id.bottom_nav_bar);
|
||||
NavigationUI.setupWithNavController(navigationView, navController);
|
||||
navigationView.setVisibility(activityTrackerAvailable ? View.VISIBLE : View.GONE);
|
||||
|
||||
NavigationView drawerNavigationView = findViewById(R.id.nav_view);
|
||||
drawerNavigationView.setNavigationItemSelectedListener(this);
|
||||
|
||||
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
||||
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
|
||||
drawer.setDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
if (GBApplication.areDynamicColorsEnabled()) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
@ -202,103 +208,18 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
toolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
|
||||
}
|
||||
|
||||
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
||||
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
|
||||
drawer.setDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
/* This sucks but for the play store we're not allowed a donation link. Instead for
|
||||
the Bangle.js Play Store app we put a message in the About dialog via @string/about_description */
|
||||
if (BuildConfig.FLAVOR == "banglejs") {
|
||||
MenuItemImpl v = (MenuItemImpl) ((NavigationView) drawer.getChildAt(1)).getMenu().findItem(R.id.donation_link);
|
||||
if (v != null) v.setVisible(false);
|
||||
}
|
||||
|
||||
NavigationView navigationView = findViewById(R.id.nav_view);
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
|
||||
//end of material design boilerplate
|
||||
deviceManager = ((GBApplication) getApplication()).getDeviceManager();
|
||||
|
||||
deviceListView = findViewById(R.id.deviceListView);
|
||||
deviceListView.setHasFixedSize(true);
|
||||
deviceListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
deviceList = deviceManager.getDevices();
|
||||
mGBDeviceAdapter = new GBDeviceAdapterv2(this, deviceList, deviceActivityHashMap);
|
||||
mGBDeviceAdapter.setHasStableIds(true);
|
||||
|
||||
// get activity data asynchronously, this fills the deviceActivityHashMap
|
||||
// and calls refreshPairedDevices() → notifyDataSetChanged
|
||||
createRefreshTask("get activity data", getApplication()).execute();
|
||||
|
||||
deviceListView.setAdapter(this.mGBDeviceAdapter);
|
||||
|
||||
fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
launchDiscoveryActivity();
|
||||
}
|
||||
});
|
||||
|
||||
showFabIfNeccessary();
|
||||
|
||||
/* uncomment to enable fixed-swipe to reveal more actions
|
||||
|
||||
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) {
|
||||
@Override
|
||||
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if(dX>50)
|
||||
dX = 50;
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||
GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
|
||||
RecyclerView.ViewHolder viewHolder, float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
}
|
||||
});
|
||||
|
||||
swipeToDismissTouchHelper.attachToRecyclerView(deviceListView);
|
||||
*/
|
||||
|
||||
registerForContextMenu(deviceListView);
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
|
||||
filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE);
|
||||
filterLocal.addAction(GBApplication.ACTION_QUIT);
|
||||
filterLocal.addAction(GBApplication.ACTION_NEW_DATA);
|
||||
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
|
||||
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
filterLocal.addAction(ACTION_REQUEST_PERMISSIONS);
|
||||
filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
refreshPairedDevices();
|
||||
|
||||
/*
|
||||
* Ask for permission to intercept notifications on first run.
|
||||
*/
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
|
||||
|
||||
boolean displayPermissionDialog = !prefs.getBoolean("permission_dialog_displayed", false);
|
||||
@ -315,7 +236,7 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
/* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy).
|
||||
/* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy).
|
||||
|
||||
Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first
|
||||
on the page, and as they are accepted the permissions are requested in turn.
|
||||
@ -364,21 +285,15 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
|
||||
GBChangeLog cl = createChangeLog();
|
||||
final boolean showChangelog = prefs.getBoolean("show_changelog", true);
|
||||
GBChangeLog cl = GBChangeLog.createChangeLog(this);
|
||||
boolean showChangelog = prefs.getBoolean("show_changelog", true);
|
||||
if (showChangelog && cl.isFirstRun() && cl.hasChanges(cl.isFirstRunEver())) {
|
||||
try {
|
||||
cl.getMaterialLogDialog().show();
|
||||
} catch (Exception ignored) {
|
||||
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.toast(this, getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
launchDiscoveryActivity();
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -394,30 +309,10 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForContextMenu(deviceListView);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
||||
drawer.closeDrawer(GravityCompat.START);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == MENU_REFRESH_CODE) {
|
||||
showFabIfNeccessary();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
|
||||
@ -461,7 +356,7 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
cl.getMaterialFullLogDialog().show();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
GB.toast(getBaseContext(), "Error showing Changelog", Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.toast(getBaseContext(), getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
return false;
|
||||
case R.id.about:
|
||||
@ -485,18 +380,14 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class));
|
||||
}
|
||||
|
||||
private void refreshPairedDevices() {
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void showFabIfNeccessary() {
|
||||
if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) {
|
||||
fab.show();
|
||||
} else {
|
||||
if (deviceManager.getDevices().size() < 1) {
|
||||
fab.show();
|
||||
} else {
|
||||
fab.hide();
|
||||
private void handleShortcut(Intent intent) {
|
||||
if(ACTION_CONNECT.equals(intent.getAction())) {
|
||||
String btDeviceAddress = intent.getStringExtra("device");
|
||||
if(btDeviceAddress!=null){
|
||||
GBDevice candidate = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
|
||||
if (candidate != null && !candidate.isConnected()) {
|
||||
GBApplication.deviceService(candidate).connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -504,7 +395,7 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
private void checkAndRequestLocationPermissions() {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access background location!");
|
||||
toast(ControlCenterv2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
GB.toast(getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
|
||||
}
|
||||
}
|
||||
@ -647,51 +538,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
|
||||
private long[] getSteps(GBDevice device, DBHandler db) {
|
||||
Calendar day = GregorianCalendar.getInstance();
|
||||
|
||||
DailyTotals ds = new DailyTotals();
|
||||
return ds.getDailyTotalsForDevice(device, day, db);
|
||||
}
|
||||
|
||||
protected RefreshTask createRefreshTask(String task, Context context) {
|
||||
return new RefreshTask(task, context);
|
||||
}
|
||||
|
||||
private void handleShortcut(Intent intent) {
|
||||
if(ACTION_CONNECT.equals(intent.getAction())) {
|
||||
String btDeviceAddress = intent.getStringExtra("device");
|
||||
if(btDeviceAddress!=null){
|
||||
GBDevice candidate = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this);
|
||||
if (candidate != null && !candidate.isConnected()) {
|
||||
GBApplication.deviceService(candidate).connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public class RefreshTask extends DBAccess {
|
||||
public RefreshTask(String task, Context context) {
|
||||
super(task, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInBackground(DBHandler db) {
|
||||
for (GBDevice gbDevice : deviceList) {
|
||||
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
if (coordinator.supportsActivityTracking()) {
|
||||
long[] stepsAndSleepData = getSteps(gbDevice, db);
|
||||
deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
refreshPairedDevices();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
|
||||
public static class NotifyPolicyPermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
@ -700,8 +546,8 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
final Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_notification_policy_access,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
@ -724,8 +570,8 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
final Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_notification_listener,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
try {
|
||||
|
@ -0,0 +1,479 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentContainerView;
|
||||
import androidx.gridlayout.widget.GridLayout;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class DashboardFragment extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class);
|
||||
|
||||
private Calendar day = GregorianCalendar.getInstance();
|
||||
private TextView textViewDate;
|
||||
private TextView arrowLeft;
|
||||
private TextView arrowRight;
|
||||
private GridLayout gridLayout;
|
||||
private SwipeRefreshLayout swipeLayout;
|
||||
private DashboardTodayWidget todayWidget;
|
||||
private DashboardGoalsWidget goalsWidget;
|
||||
private DashboardStepsWidget stepsWidget;
|
||||
private DashboardDistanceWidget distanceWidget;
|
||||
private DashboardActiveTimeWidget activeTimeWidget;
|
||||
private DashboardSleepWidget sleepWidget;
|
||||
private DashboardData dashboardData = new DashboardData();
|
||||
private boolean isConfigChanged = false;
|
||||
|
||||
public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change";
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
switch (action) {
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (dev != null && !dev.isBusy()) {
|
||||
refresh();
|
||||
}
|
||||
break;
|
||||
case ACTION_CONFIG_CHANGE:
|
||||
isConfigChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false);
|
||||
setHasOptionsMenu(true);
|
||||
textViewDate = dashboardView.findViewById(R.id.dashboard_date);
|
||||
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
|
||||
swipeLayout = dashboardView.findViewById(R.id.dashboard_swipe_layout);
|
||||
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
// Signal DeviceCommunicationService to fetch activity for all connected devices
|
||||
Intent intent = new Intent(requireContext(), DeviceCommunicationService.class);
|
||||
intent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA)
|
||||
.putExtra(DeviceService.EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
|
||||
requireContext().startService(intent);
|
||||
// Hide 'refreshing' animation immediately if no health devices are connected
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
for (GBDevice dev : devices) {
|
||||
if (dev.getDeviceCoordinator().supportsActivityTracking() && dev.isInitialized()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
swipeLayout.setRefreshing(false);
|
||||
GB.toast(getString(R.string.info_no_devices_connected), Toast.LENGTH_LONG, GB.WARN);
|
||||
}
|
||||
});
|
||||
|
||||
// Increase column count on landscape, tablets and open foldables
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
if (displayMetrics.widthPixels / displayMetrics.density >= 600) {
|
||||
gridLayout.setColumnCount(4);
|
||||
}
|
||||
|
||||
arrowLeft = dashboardView.findViewById(R.id.arrow_left);
|
||||
arrowLeft.setOnClickListener(v -> {
|
||||
day.add(Calendar.DAY_OF_MONTH, -1);
|
||||
refresh();
|
||||
});
|
||||
arrowRight = dashboardView.findViewById(R.id.arrow_right);
|
||||
arrowRight.setOnClickListener(v -> {
|
||||
Calendar today = GregorianCalendar.getInstance();
|
||||
if (!DateTimeUtils.isSameDay(today, day)) {
|
||||
day.add(Calendar.DAY_OF_MONTH, 1);
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey("dashboard_data") && dashboardData.isEmpty()) {
|
||||
dashboardData = (DashboardData) savedInstanceState.getSerializable("dashboard_data");
|
||||
}
|
||||
|
||||
// Make sure the widget fragments are (re)instantiated when drawing the dashboard
|
||||
todayWidget = null;
|
||||
goalsWidget = null;
|
||||
stepsWidget = null;
|
||||
distanceWidget = null;
|
||||
activeTimeWidget = null;
|
||||
sleepWidget = null;
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
filterLocal.addAction(ACTION_CONFIG_CHANGE);
|
||||
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
return dashboardView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
draw();
|
||||
if (isConfigChanged) {
|
||||
isConfigChanged = false;
|
||||
fullRefresh();
|
||||
} else if (dashboardData.isEmpty() || todayWidget == null) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable("dashboard_data", dashboardData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.dashboard_menu, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.dashboard_show_calendar:
|
||||
Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class);
|
||||
intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis());
|
||||
startActivityForResult(intent, 0);
|
||||
return false;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == 0 && resultCode == DashboardCalendarActivity.RESULT_OK && data != null) {
|
||||
long timeMillis = data.getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0);
|
||||
if (timeMillis != 0) {
|
||||
day.setTimeInMillis(timeMillis);
|
||||
fullRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fullRefresh() {
|
||||
gridLayout.removeAllViews();
|
||||
todayWidget = null;
|
||||
goalsWidget = null;
|
||||
stepsWidget = null;
|
||||
distanceWidget = null;
|
||||
activeTimeWidget = null;
|
||||
sleepWidget = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
swipeLayout.setRefreshing(false);
|
||||
day.set(Calendar.HOUR_OF_DAY, 23);
|
||||
day.set(Calendar.MINUTE, 59);
|
||||
day.set(Calendar.SECOND, 59);
|
||||
dashboardData.clear();
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
dashboardData.showAllDevices = prefs.getBoolean("dashboard_devices_all", true);
|
||||
dashboardData.showDeviceList = prefs.getStringSet("dashboard_devices_multiselect", new HashSet<>());
|
||||
dashboardData.hrIntervalSecs = prefs.getInt("dashboard_widget_today_hr_interval", 1) * 60;
|
||||
dashboardData.timeTo = (int) (day.getTimeInMillis() / 1000);
|
||||
dashboardData.timeFrom = DateTimeUtils.shiftDays(dashboardData.timeTo, -1);
|
||||
draw();
|
||||
}
|
||||
|
||||
private void draw() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_values));
|
||||
String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder);
|
||||
List<String> widgetsOrder = Arrays.asList(widgetsOrderPref.split(","));
|
||||
|
||||
Calendar today = GregorianCalendar.getInstance();
|
||||
if (DateTimeUtils.isSameDay(today, day)) {
|
||||
textViewDate.setText(getContext().getString(R.string.activity_summary_today));
|
||||
arrowRight.setAlpha(0.5f);
|
||||
} else {
|
||||
textViewDate.setText(DateTimeUtils.formatDate(day.getTime()));
|
||||
arrowRight.setAlpha(1);
|
||||
}
|
||||
|
||||
boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true);
|
||||
|
||||
for (String widgetName : widgetsOrder) {
|
||||
switch (widgetName) {
|
||||
case "today":
|
||||
if (todayWidget == null) {
|
||||
todayWidget = DashboardTodayWidget.newInstance(dashboardData);
|
||||
createWidget(todayWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1);
|
||||
} else {
|
||||
todayWidget.update();
|
||||
}
|
||||
break;
|
||||
case "goals":
|
||||
if (goalsWidget == null) {
|
||||
goalsWidget = DashboardGoalsWidget.newInstance(dashboardData);
|
||||
createWidget(goalsWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1);
|
||||
} else {
|
||||
goalsWidget.update();
|
||||
}
|
||||
break;
|
||||
case "steps":
|
||||
if (stepsWidget == null) {
|
||||
stepsWidget = DashboardStepsWidget.newInstance(dashboardData);
|
||||
createWidget(stepsWidget, cardsEnabled, 1);
|
||||
} else {
|
||||
stepsWidget.update();
|
||||
}
|
||||
break;
|
||||
case "distance":
|
||||
if (distanceWidget == null) {
|
||||
distanceWidget = DashboardDistanceWidget.newInstance(dashboardData);
|
||||
createWidget(distanceWidget, cardsEnabled, 1);
|
||||
} else {
|
||||
distanceWidget.update();
|
||||
}
|
||||
break;
|
||||
case "activetime":
|
||||
if (activeTimeWidget == null) {
|
||||
activeTimeWidget = DashboardActiveTimeWidget.newInstance(dashboardData);
|
||||
createWidget(activeTimeWidget, cardsEnabled, 1);
|
||||
} else {
|
||||
activeTimeWidget.update();
|
||||
}
|
||||
break;
|
||||
case "sleep":
|
||||
if (sleepWidget == null) {
|
||||
sleepWidget = DashboardSleepWidget.newInstance(dashboardData);
|
||||
createWidget(sleepWidget, cardsEnabled, 1);
|
||||
} else {
|
||||
sleepWidget.update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createWidget(AbstractDashboardWidget widgetObj, boolean cardsEnabled, int columnSpan) {
|
||||
final float scale = requireContext().getResources().getDisplayMetrics().density;
|
||||
FragmentContainerView fragment = new FragmentContainerView(requireActivity());
|
||||
int fragmentId = View.generateViewId();
|
||||
fragment.setId(fragmentId);
|
||||
getParentFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(fragmentId, widgetObj)
|
||||
.commit();
|
||||
|
||||
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL,1f)
|
||||
);
|
||||
layoutParams.width = 0;
|
||||
int pixels_8dp = (int) (8 * scale + 0.5f);
|
||||
layoutParams.setMargins(pixels_8dp, pixels_8dp, pixels_8dp, pixels_8dp);
|
||||
|
||||
if (cardsEnabled) {
|
||||
MaterialCardView card = new MaterialCardView(requireActivity());
|
||||
int pixels_4dp = (int) (4 * scale + 0.5f);
|
||||
card.setRadius(pixels_4dp);
|
||||
card.setCardElevation(pixels_4dp);
|
||||
card.setContentPadding(pixels_4dp, pixels_4dp, pixels_4dp, pixels_4dp);
|
||||
card.setLayoutParams(layoutParams);
|
||||
card.addView(fragment);
|
||||
gridLayout.addView(card);
|
||||
} else {
|
||||
fragment.setLayoutParams(layoutParams);
|
||||
gridLayout.addView(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class serves as a data collection object for all data points used by the various
|
||||
* dashboard widgets. Since retrieving this data can be costly, this class makes sure it will
|
||||
* only be done once. It will be passed to every widget, making sure they have the necessary
|
||||
* data available.
|
||||
*/
|
||||
public static class DashboardData implements Serializable {
|
||||
public boolean showAllDevices;
|
||||
public Set<String> showDeviceList;
|
||||
public int hrIntervalSecs;
|
||||
public int timeFrom;
|
||||
public int timeTo;
|
||||
public final List<GeneralizedActivity> generalizedActivities = Collections.synchronizedList(new ArrayList<>());
|
||||
private int stepsTotal;
|
||||
private float stepsGoalFactor;
|
||||
private long sleepTotalMinutes;
|
||||
private float sleepGoalFactor;
|
||||
private float distanceTotalMeters;
|
||||
private float distanceGoalFactor;
|
||||
private long activeMinutesTotal;
|
||||
private float activeMinutesGoalFactor;
|
||||
|
||||
public void clear() {
|
||||
stepsTotal = 0;
|
||||
stepsGoalFactor = 0;
|
||||
sleepTotalMinutes = 0;
|
||||
sleepGoalFactor = 0;
|
||||
distanceTotalMeters = 0;
|
||||
distanceGoalFactor = 0;
|
||||
activeMinutesTotal = 0;
|
||||
activeMinutesGoalFactor = 0;
|
||||
generalizedActivities.clear();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return (stepsTotal == 0 &&
|
||||
stepsGoalFactor == 0 &&
|
||||
sleepTotalMinutes == 0 &&
|
||||
sleepGoalFactor == 0 &&
|
||||
distanceTotalMeters == 0 &&
|
||||
distanceGoalFactor == 0 &&
|
||||
activeMinutesTotal == 0 &&
|
||||
activeMinutesGoalFactor == 0 &&
|
||||
generalizedActivities.isEmpty());
|
||||
}
|
||||
|
||||
public synchronized int getStepsTotal() {
|
||||
if (stepsTotal == 0)
|
||||
stepsTotal = DashboardUtils.getStepsTotal(this);
|
||||
return stepsTotal;
|
||||
}
|
||||
|
||||
public synchronized float getStepsGoalFactor() {
|
||||
if (stepsGoalFactor == 0)
|
||||
stepsGoalFactor = DashboardUtils.getStepsGoalFactor(this);
|
||||
return stepsGoalFactor;
|
||||
}
|
||||
|
||||
public synchronized float getDistanceTotal() {
|
||||
if (distanceTotalMeters == 0)
|
||||
distanceTotalMeters = DashboardUtils.getDistanceTotal(this);
|
||||
return distanceTotalMeters;
|
||||
}
|
||||
|
||||
public synchronized float getDistanceGoalFactor() {
|
||||
if (distanceGoalFactor == 0)
|
||||
distanceGoalFactor = DashboardUtils.getDistanceGoalFactor(this);
|
||||
return distanceGoalFactor;
|
||||
}
|
||||
|
||||
public synchronized long getActiveMinutesTotal() {
|
||||
if (activeMinutesTotal == 0)
|
||||
activeMinutesTotal = DashboardUtils.getActiveMinutesTotal(this);
|
||||
return activeMinutesTotal;
|
||||
}
|
||||
|
||||
public synchronized float getActiveMinutesGoalFactor() {
|
||||
if (activeMinutesGoalFactor == 0)
|
||||
activeMinutesGoalFactor = DashboardUtils.getActiveMinutesGoalFactor(this);
|
||||
return activeMinutesGoalFactor;
|
||||
}
|
||||
|
||||
public synchronized long getSleepMinutesTotal() {
|
||||
if (sleepTotalMinutes == 0)
|
||||
sleepTotalMinutes = DashboardUtils.getSleepMinutesTotal(this);
|
||||
return sleepTotalMinutes;
|
||||
}
|
||||
|
||||
public synchronized float getSleepMinutesGoalFactor() {
|
||||
if (sleepGoalFactor == 0)
|
||||
sleepGoalFactor = DashboardUtils.getSleepMinutesGoalFactor(this);
|
||||
return sleepGoalFactor;
|
||||
}
|
||||
|
||||
public static class GeneralizedActivity implements Serializable {
|
||||
public int activityKind;
|
||||
public long timeFrom;
|
||||
public long timeTo;
|
||||
|
||||
public GeneralizedActivity(int activityKind, long timeFrom, long timeTo) {
|
||||
this.activityKind = activityKind;
|
||||
this.timeFrom = timeFrom;
|
||||
this.timeTo = timeTo;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Generalized activity: timeFrom=" + timeFrom + ", timeTo=" + timeTo + ", activityKind=" + activityKind + ", calculated duration: " + (timeTo - timeFrom) + " seconds";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class DashboardPreferencesActivity extends AbstractSettingsActivityV2 {
|
||||
@Override
|
||||
protected String fragmentTag() {
|
||||
return DashboardPreferencesFragment.FRAGMENT_TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PreferenceFragmentCompat newFragment() {
|
||||
return new DashboardPreferencesFragment();
|
||||
}
|
||||
|
||||
public static class DashboardPreferencesFragment extends AbstractPreferenceFragment {
|
||||
static final String FRAGMENT_TAG = "DASHBOARD_PREFERENCES_FRAGMENT";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
setPreferencesFromResource(R.xml.dashboard_preferences, rootKey);
|
||||
|
||||
setInputTypeFor("dashboard_widget_today_hr_interval", InputType.TYPE_CLASS_NUMBER);
|
||||
|
||||
final MultiSelectListPreference dashboardDevices = findPreference("dashboard_devices_multiselect");
|
||||
if (dashboardDevices != null) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
List<String> deviceMACs = new ArrayList<>();
|
||||
List<String> deviceNames = new ArrayList<>();
|
||||
for (GBDevice dev : devices) {
|
||||
deviceMACs.add(dev.getAddress());
|
||||
deviceNames.add(dev.getAliasOrName());
|
||||
}
|
||||
dashboardDevices.setEntryValues(deviceMACs.toArray(new String[0]));
|
||||
dashboardDevices.setEntries(deviceNames.toArray(new String[0]));
|
||||
}
|
||||
List<String> dashboardPrefs = Arrays.asList(
|
||||
"dashboard_cards_enabled",
|
||||
"pref_dashboard_widgets_order",
|
||||
"dashboard_widget_today_24h",
|
||||
"dashboard_widget_today_2columns",
|
||||
"dashboard_widget_today_legend",
|
||||
"dashboard_widget_today_hr_interval",
|
||||
"dashboard_widget_goals_2columns",
|
||||
"dashboard_widget_goals_legend",
|
||||
"dashboard_devices_all",
|
||||
"dashboard_devices_multiselect"
|
||||
);
|
||||
Preference pref;
|
||||
for (String dashboardPref : dashboardPrefs) {
|
||||
pref = findPreference(dashboardPref);
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, autoExportEnabled) -> {
|
||||
sendDashboardConfigChangedIntent();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal dashboard that its config has changed
|
||||
*/
|
||||
private void sendDashboardConfigChangedIntent() {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(DashboardFragment.ACTION_CONFIG_CHANGE);
|
||||
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/* Copyright (C) 2016-2024 Andreas Shimokawa, Andrzej Surowiec, Arjan
|
||||
Schrijver, Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, Ganblejs,
|
||||
gfwilliams, Gordon Williams, Johannes Tysiak, José Rebelo, marco.altomonte,
|
||||
Petr Vaněk, Taavi Eomäe
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class DevicesFragment extends Fragment {
|
||||
|
||||
private DeviceManager deviceManager;
|
||||
private GBDeviceAdapterv2 mGBDeviceAdapter;
|
||||
private RecyclerView deviceListView;
|
||||
private FloatingActionButton fab;
|
||||
List<GBDevice> deviceList;
|
||||
private HashMap<String,long[]> deviceActivityHashMap = new HashMap();
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (Objects.requireNonNull(action)) {
|
||||
case DeviceManager.ACTION_DEVICES_CHANGED:
|
||||
case GBApplication.ACTION_NEW_DATA:
|
||||
createRefreshTask("get activity data", requireContext()).execute();
|
||||
mGBDeviceAdapter.rebuildFolders();
|
||||
refreshPairedDevices();
|
||||
break;
|
||||
case DeviceService.ACTION_REALTIME_SAMPLES:
|
||||
handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void handleRealtimeSample(Serializable extra) {
|
||||
if (extra instanceof ActivitySample) {
|
||||
ActivitySample sample = (ActivitySample) extra;
|
||||
if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) {
|
||||
refreshPairedDevices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
View currentView = inflater.inflate(R.layout.fragment_devices, container, false);
|
||||
|
||||
deviceManager = ((GBApplication) getActivity().getApplication()).getDeviceManager();
|
||||
|
||||
deviceListView = currentView.findViewById(R.id.deviceListView);
|
||||
deviceListView.setHasFixedSize(true);
|
||||
deviceListView.setLayoutManager(new LinearLayoutManager(currentView.getContext()));
|
||||
|
||||
deviceList = deviceManager.getDevices();
|
||||
mGBDeviceAdapter = new GBDeviceAdapterv2(currentView.getContext(), deviceList, deviceActivityHashMap);
|
||||
mGBDeviceAdapter.setHasStableIds(true);
|
||||
|
||||
deviceListView.setAdapter(this.mGBDeviceAdapter);
|
||||
|
||||
// get activity data asynchronously, this fills the deviceActivityHashMap
|
||||
// and calls refreshPairedDevices() → notifyDataSetChanged
|
||||
deviceListView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getContext() != null) {
|
||||
createRefreshTask("get activity data", getContext()).execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fab = currentView.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
launchDiscoveryActivity();
|
||||
}
|
||||
});
|
||||
|
||||
showFabIfNeccessary();
|
||||
|
||||
/* uncomment to enable fixed-swipe to reveal more actions
|
||||
|
||||
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) {
|
||||
@Override
|
||||
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if(dX>50)
|
||||
dX = 50;
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||
GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
|
||||
RecyclerView.ViewHolder viewHolder, float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
}
|
||||
});
|
||||
|
||||
swipeToDismissTouchHelper.attachToRecyclerView(deviceListView);
|
||||
*/
|
||||
|
||||
registerForContextMenu(deviceListView);
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBApplication.ACTION_NEW_DATA);
|
||||
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
|
||||
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
refreshPairedDevices();
|
||||
|
||||
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
startActivity(new Intent(getActivity(), DiscoveryActivityV2.class));
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
|
||||
return currentView;
|
||||
}
|
||||
|
||||
private void launchDiscoveryActivity() {
|
||||
startActivity(new Intent(getActivity(), DiscoveryActivityV2.class));
|
||||
}
|
||||
|
||||
private void showFabIfNeccessary() {
|
||||
if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) {
|
||||
fab.show();
|
||||
} else {
|
||||
if (deviceManager.getDevices().size() < 1) {
|
||||
fab.show();
|
||||
} else {
|
||||
fab.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (deviceListView != null) unregisterForContextMenu(deviceListView);
|
||||
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private long[] getSteps(GBDevice device, DBHandler db) {
|
||||
Calendar day = GregorianCalendar.getInstance();
|
||||
|
||||
DailyTotals ds = new DailyTotals();
|
||||
return ds.getDailyTotalsForDevice(device, day, db);
|
||||
}
|
||||
|
||||
public void refreshPairedDevices() {
|
||||
if (mGBDeviceAdapter != null) {
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
mGBDeviceAdapter.rebuildFolders();
|
||||
}
|
||||
}
|
||||
|
||||
public RefreshTask createRefreshTask(String task, Context context) {
|
||||
return new RefreshTask(task, context);
|
||||
}
|
||||
|
||||
public class RefreshTask extends DBAccess {
|
||||
public RefreshTask(String task, Context context) {
|
||||
super(task, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInBackground(DBHandler db) {
|
||||
for (GBDevice gbDevice : deviceList) {
|
||||
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
if (coordinator.supportsActivityTracking()) {
|
||||
long[] stepsAndSleepData = getSteps(gbDevice, db);
|
||||
deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
refreshPairedDevices();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -45,6 +45,7 @@ import android.widget.Toast;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
@ -55,6 +56,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -72,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleSettingsActivit
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
@ -397,6 +401,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
audioPlayer.setDefaultValue(newValues[0]);
|
||||
}
|
||||
|
||||
pref = findPreference("pref_category_dashboard");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
Intent enableIntent = new Intent(requireContext(), DashboardPreferencesActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final Preference theme = findPreference("pref_key_theme");
|
||||
final Preference amoled_black = findPreference("pref_key_theme_amoled_black");
|
||||
|
||||
|
@ -0,0 +1,92 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
|
||||
public abstract class AbstractDashboardWidget extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class);
|
||||
|
||||
protected static String ARG_DASHBOARD_DATA = "dashboard_widget_argument_data";
|
||||
|
||||
protected DashboardFragment.DashboardData dashboardData;
|
||||
|
||||
protected @ColorInt int color_unknown = Color.argb(25, 128, 128, 128);
|
||||
protected @ColorInt int color_not_worn = Color.BLACK;
|
||||
protected @ColorInt int color_worn = Color.rgb(128, 128, 128);
|
||||
protected @ColorInt int color_activity = Color.GREEN;
|
||||
protected @ColorInt int color_exercise = Color.rgb(255, 128, 0);
|
||||
protected @ColorInt int color_deep_sleep = Color.BLUE;
|
||||
protected @ColorInt int color_light_sleep = Color.rgb(150, 150, 255);
|
||||
protected @ColorInt int color_rem_sleep = Color.rgb(182, 191, 255);
|
||||
protected @ColorInt int color_distance = Color.BLUE;
|
||||
protected @ColorInt int color_active_time = Color.rgb(170, 0, 255);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
dashboardData = (DashboardFragment.DashboardData) getArguments().getSerializable(ARG_DASHBOARD_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void update() {
|
||||
fillData();
|
||||
}
|
||||
|
||||
protected abstract void fillData();
|
||||
|
||||
/**
|
||||
* @param width Bitmap width in pixels
|
||||
* @param barWidth Gauge bar width in pixels
|
||||
* @param filledColor Color of the filled part of the gauge
|
||||
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
||||
* @return Bitmap containing the gauge
|
||||
*/
|
||||
Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, float filledFactor) {
|
||||
int height = width / 2;
|
||||
int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(filledColor);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardActiveTimeWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardActiveTimeWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardActiveTimeWidget.class);
|
||||
private TextView activeTime;
|
||||
private ImageView activeTimeGauge;
|
||||
|
||||
public DashboardActiveTimeWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardActiveTimeWidget.
|
||||
*/
|
||||
public static DashboardActiveTimeWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_active_time, container, false);
|
||||
activeTime = fragmentView.findViewById(R.id.activetime_text);
|
||||
activeTimeGauge = fragmentView.findViewById(R.id.activetime_gauge);
|
||||
|
||||
fillData();
|
||||
|
||||
return fragmentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activeTime != null && activeTimeGauge != null) fillData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillData() {
|
||||
if (activeTimeGauge == null) return;
|
||||
activeTimeGauge.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
dashboardData.getActiveMinutesTotal();
|
||||
dashboardData.getActiveMinutesGoalFactor();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
|
||||
// Update text representation
|
||||
long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
|
||||
String activeHours = String.format("%d", (int) Math.floor(totalActiveMinutes / 60f));
|
||||
String activeMinutes = String.format("%02d", (int) (totalActiveMinutes % 60f));
|
||||
activeTime.setText(activeHours + ":" + activeMinutes);
|
||||
|
||||
// Draw gauge
|
||||
activeTimeGauge.setImageBitmap(drawGauge(200, 15, color_active_time, dashboardData.getActiveMinutesGoalFactor()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Gravity;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.gridlayout.widget.GridLayout;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class DashboardCalendarActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardCalendarActivity.class);
|
||||
public static String EXTRA_TIMESTAMP = "dashboard_calendar_chosen_day";
|
||||
private final ConcurrentHashMap<Calendar, TextView> dayCells = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Integer> dayColors = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean showAllDevices;
|
||||
private Set<String> showDeviceList;
|
||||
|
||||
TextView monthTextView;
|
||||
TextView arrowLeft;
|
||||
TextView arrowRight;
|
||||
GridLayout calendarGrid;
|
||||
Calendar currentDay;
|
||||
Calendar cal;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_dashboard_calendar);
|
||||
monthTextView = findViewById(R.id.calendar_month);
|
||||
calendarGrid = findViewById(R.id.dashboard_calendar_grid);
|
||||
currentDay = Calendar.getInstance();
|
||||
cal = Calendar.getInstance();
|
||||
long receivedTimestamp = getIntent().getLongExtra(EXTRA_TIMESTAMP, 0);
|
||||
if (receivedTimestamp != 0) {
|
||||
currentDay.setTimeInMillis(receivedTimestamp);
|
||||
cal.setTimeInMillis(receivedTimestamp);
|
||||
}
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
showAllDevices = prefs.getBoolean("dashboard_devices_all", true);
|
||||
showDeviceList = prefs.getStringSet("dashboard_devices_multiselect", new HashSet<>());
|
||||
|
||||
arrowLeft = findViewById(R.id.arrow_left);
|
||||
arrowLeft.setOnClickListener(v -> {
|
||||
cal.add(Calendar.MONTH, -1);
|
||||
draw();
|
||||
});
|
||||
arrowRight = findViewById(R.id.arrow_right);
|
||||
arrowRight.setOnClickListener(v -> {
|
||||
Calendar today = GregorianCalendar.getInstance();
|
||||
if (!DateTimeUtils.isSameMonth(today, cal)) {
|
||||
cal.add(Calendar.MONTH, 1);
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
private void displayColorsAsync() {
|
||||
calendarGrid.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void draw() {
|
||||
// Remove previous calendar days
|
||||
dayCells.clear();
|
||||
dayColors.clear();
|
||||
calendarGrid.removeAllViews();
|
||||
// Update month display
|
||||
SimpleDateFormat monthFormat = new SimpleDateFormat("LLLL yyyy", Locale.getDefault());
|
||||
monthTextView.setText(monthFormat.format(cal.getTime()));
|
||||
Calendar today = GregorianCalendar.getInstance();
|
||||
today.set(Calendar.HOUR, 23);
|
||||
today.set(Calendar.MINUTE, 59);
|
||||
today.set(Calendar.SECOND, 59);
|
||||
if (DateTimeUtils.isSameMonth(today, cal)) {
|
||||
arrowRight.setAlpha(0.5f);
|
||||
} else {
|
||||
arrowRight.setAlpha(1);
|
||||
}
|
||||
// Calculate grid cell size for dates
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int cellSize = screenWidth / 7;
|
||||
// Determine first day that should be displayed
|
||||
Calendar drawCal = (Calendar) cal.clone();
|
||||
drawCal.set(Calendar.DAY_OF_MONTH, 1);
|
||||
int displayMonth = drawCal.get(Calendar.MONTH);
|
||||
int firstDayOfWeek = cal.getFirstDayOfWeek();
|
||||
int daysToFirstDay = (drawCal.get(Calendar.DAY_OF_WEEK) - firstDayOfWeek + 7) % 7;
|
||||
drawCal.add(Calendar.DAY_OF_MONTH, -daysToFirstDay);
|
||||
// Determine last day that should be displayed
|
||||
Calendar lastDay = (Calendar) cal.clone();
|
||||
lastDay.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
lastDay.add(Calendar.DAY_OF_MONTH, (firstDayOfWeek + 7 - lastDay.get(Calendar.DAY_OF_WEEK)) % 7);
|
||||
// Add day names header
|
||||
SimpleDateFormat dayFormat = new SimpleDateFormat("E", Locale.getDefault());
|
||||
Calendar weekdays = Calendar.getInstance();
|
||||
for (int i=0; i<7; i++) {
|
||||
int currentDayOfWeek = (firstDayOfWeek + i - 1) % 7 + 1;
|
||||
weekdays.set(Calendar.DAY_OF_WEEK, currentDayOfWeek);
|
||||
createWeekdayCell(dayFormat.format(weekdays.getTime()), cellSize);
|
||||
}
|
||||
// Loop through month days and create grid cells for them
|
||||
while (!DateTimeUtils.isSameDay(drawCal, lastDay)) {
|
||||
boolean clickable = drawCal.get(Calendar.MONTH) == displayMonth;
|
||||
if (drawCal.after(today)) clickable = false;
|
||||
createDateCell(drawCal, cellSize, clickable);
|
||||
drawCal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
}
|
||||
// Asynchronously determine and display goal colors
|
||||
displayColorsAsync();
|
||||
}
|
||||
|
||||
private TextView prepareGridElement(int cellSize) {
|
||||
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, 1, GridLayout.FILL,1f)
|
||||
);
|
||||
int margin = cellSize / 10;
|
||||
layoutParams.width = 0;
|
||||
layoutParams.height = cellSize - 2 * margin;
|
||||
layoutParams.setMargins(margin, margin, margin, margin);
|
||||
TextView text = new TextView(this);
|
||||
text.setLayoutParams(layoutParams);
|
||||
text.setGravity(Gravity.CENTER);
|
||||
return text;
|
||||
}
|
||||
|
||||
private void createWeekdayCell(String day, int cellSize) {
|
||||
TextView text = prepareGridElement(cellSize);
|
||||
text.setText(day);
|
||||
calendarGrid.addView(text);
|
||||
}
|
||||
|
||||
private void createDateCell(Calendar day, int cellSize, boolean clickable) {
|
||||
TextView text = prepareGridElement(cellSize);
|
||||
text.setText(String.valueOf(day.get(Calendar.DAY_OF_MONTH)));
|
||||
if (clickable) {
|
||||
// Save textview for later coloring
|
||||
dayCells.put((Calendar) day.clone(), text);
|
||||
}
|
||||
calendarGrid.addView(text);
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
for (Calendar day : dayCells.keySet()) {
|
||||
// Determine day color by the amount of the steps goal reached
|
||||
DashboardFragment.DashboardData dashboardData = new DashboardFragment.DashboardData();
|
||||
dashboardData.showAllDevices = showAllDevices;
|
||||
dashboardData.showDeviceList = showDeviceList;
|
||||
dashboardData.timeTo = (int) (day.getTimeInMillis() / 1000);
|
||||
dashboardData.timeFrom = DateTimeUtils.shiftDays(dashboardData.timeTo, -1);
|
||||
float goalFactor = DashboardUtils.getStepsGoalFactor(dashboardData);
|
||||
@ColorInt int dayColor;
|
||||
if (goalFactor >= 1) {
|
||||
dayColor = Color.argb(128, 0, 255, 0); // Green
|
||||
} else if (goalFactor >= 0.75) {
|
||||
dayColor = Color.argb(128, 0, 128, 0); // Dark green
|
||||
} else if (goalFactor >= 0.5) {
|
||||
dayColor = Color.argb(128, 255, 255, 0); // Yellow
|
||||
} else if (goalFactor > 0.25) {
|
||||
dayColor = Color.argb(128, 255, 128, 0); // Orange
|
||||
} else if (goalFactor > 0) {
|
||||
dayColor = Color.argb(128, 255, 0, 0); // Red
|
||||
} else {
|
||||
dayColor = Color.argb(50, 128, 128, 128);
|
||||
}
|
||||
dayColors.put(day.get(Calendar.DAY_OF_MONTH), dayColor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
for (Map.Entry<Calendar, TextView> entry : dayCells.entrySet()) {
|
||||
Calendar day = entry.getKey();
|
||||
TextView text = entry.getValue();
|
||||
@ColorInt int dayColor;
|
||||
try {
|
||||
dayColor = dayColors.get(day.get(Calendar.DAY_OF_MONTH));
|
||||
} catch (NullPointerException e) {
|
||||
continue;
|
||||
}
|
||||
final long timestamp = day.getTimeInMillis();
|
||||
// Draw colored circle
|
||||
GradientDrawable backgroundDrawable = new GradientDrawable();
|
||||
backgroundDrawable.setShape(GradientDrawable.OVAL);
|
||||
backgroundDrawable.setColor(dayColor);
|
||||
if (DateTimeUtils.isSameDay(day, currentDay)) {
|
||||
GradientDrawable borderDrawable = new GradientDrawable();
|
||||
borderDrawable.setShape(GradientDrawable.OVAL);
|
||||
borderDrawable.setColor(Color.TRANSPARENT);
|
||||
borderDrawable.setStroke(5, GBApplication.getTextColor(getApplicationContext()));
|
||||
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{backgroundDrawable, borderDrawable});
|
||||
text.setBackground(layerDrawable);
|
||||
} else {
|
||||
text.setBackground(backgroundDrawable);
|
||||
}
|
||||
text.setOnClickListener(v -> {
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(EXTRA_TIMESTAMP, timestamp);
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardDistanceWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardDistanceWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardDistanceWidget.class);
|
||||
private TextView distanceText;
|
||||
private ImageView distanceGauge;
|
||||
|
||||
public DashboardDistanceWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardDistanceWidget.
|
||||
*/
|
||||
public static DashboardDistanceWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardDistanceWidget fragment = new DashboardDistanceWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_distance, container, false);
|
||||
distanceText = fragmentView.findViewById(R.id.distance_text);
|
||||
distanceGauge = fragmentView.findViewById(R.id.distance_gauge);
|
||||
|
||||
fillData();
|
||||
|
||||
return fragmentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (distanceText != null && distanceGauge != null) fillData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillData() {
|
||||
if (distanceGauge == null) return;
|
||||
distanceGauge.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
dashboardData.getDistanceTotal();
|
||||
dashboardData.getDistanceGoalFactor();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
|
||||
// Update text representation
|
||||
String distanceFormatted = FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal());
|
||||
distanceText.setText(distanceFormatted);
|
||||
|
||||
// Draw gauge
|
||||
distanceGauge.setImageBitmap(drawGauge(200, 15, color_distance, dashboardData.getDistanceGoalFactor()));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardGoalsWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardGoalsWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardGoalsWidget.class);
|
||||
private View goalsView;
|
||||
private ImageView goalsChart;
|
||||
|
||||
public DashboardGoalsWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardGoalsWidget.
|
||||
*/
|
||||
public static DashboardGoalsWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardGoalsWidget fragment = new DashboardGoalsWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
goalsView = inflater.inflate(R.layout.dashboard_widget_goals, container, false);
|
||||
goalsChart = goalsView.findViewById(R.id.dashboard_goals_chart);
|
||||
|
||||
// Initialize legend
|
||||
TextView legend = goalsView.findViewById(R.id.dashboard_goals_legend);
|
||||
SpannableString l_steps = new SpannableString("■ " + getString(R.string.steps));
|
||||
l_steps.setSpan(new ForegroundColorSpan(color_activity), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_distance = new SpannableString("■ " + getString(R.string.distance));
|
||||
l_distance.setSpan(new ForegroundColorSpan(color_distance), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_active_time = new SpannableString("■ " + getString(R.string.activity_list_summary_active_time));
|
||||
l_active_time.setSpan(new ForegroundColorSpan(color_active_time), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_sleep = new SpannableString("■ " + getString(R.string.menuitem_sleep));
|
||||
l_sleep.setSpan(new ForegroundColorSpan(color_light_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableStringBuilder legendBuilder = new SpannableStringBuilder();
|
||||
legend.setText(legendBuilder.append(l_steps).append(" ").append(l_distance).append("\n").append(l_active_time).append(" ").append(l_sleep));
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE);
|
||||
|
||||
fillData();
|
||||
|
||||
return goalsView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (goalsChart != null) fillData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillData() {
|
||||
if (goalsView == null) return;
|
||||
goalsView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
private Bitmap goalsBitmap;
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
int width = 500;
|
||||
int height = 500;
|
||||
int barWidth = 20;
|
||||
int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
goalsBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(goalsBitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360, false, paint);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_activity);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getStepsGoalFactor(), false, paint);
|
||||
|
||||
barMargin += barWidth * 1.5;
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360, false, paint);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_distance);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getDistanceGoalFactor(), false, paint);
|
||||
|
||||
barMargin += barWidth * 1.5;
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360, false, paint);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_active_time);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getActiveMinutesGoalFactor(), false, paint);
|
||||
|
||||
barMargin += barWidth * 1.5;
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360, false, paint);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_light_sleep);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
goalsChart.setImageBitmap(goalsBitmap);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardSleepWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardSleepWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardSleepWidget.class);
|
||||
private TextView sleepAmount;
|
||||
private ImageView sleepGauge;
|
||||
|
||||
public DashboardSleepWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardSleepWidget.
|
||||
*/
|
||||
public static DashboardSleepWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardSleepWidget fragment = new DashboardSleepWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_sleep, container, false);
|
||||
sleepAmount = fragmentView.findViewById(R.id.sleep_text);
|
||||
sleepGauge = fragmentView.findViewById(R.id.sleep_gauge);
|
||||
|
||||
fillData();
|
||||
|
||||
return fragmentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (sleepAmount != null && sleepGauge != null) fillData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillData() {
|
||||
if (sleepGauge == null) return;
|
||||
sleepGauge.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
dashboardData.getSleepMinutesTotal();
|
||||
dashboardData.getSleepMinutesGoalFactor();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
|
||||
// Update text representation
|
||||
long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
|
||||
String sleepHours = String.format("%d", (int) Math.floor(totalSleepMinutes / 60f));
|
||||
String sleepMinutes = String.format("%02d", (int) (totalSleepMinutes % 60f));
|
||||
sleepAmount.setText(sleepHours + ":" + sleepMinutes);
|
||||
|
||||
// Draw gauge
|
||||
sleepGauge.setImageBitmap(drawGauge(200, 15, color_light_sleep, dashboardData.getSleepMinutesGoalFactor()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardStepsWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardStepsWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardStepsWidget.class);
|
||||
private TextView stepsCount;
|
||||
private ImageView stepsGauge;
|
||||
|
||||
public DashboardStepsWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardStepsWidget.
|
||||
*/
|
||||
public static DashboardStepsWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardStepsWidget fragment = new DashboardStepsWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_steps, container, false);
|
||||
stepsCount = fragmentView.findViewById(R.id.steps_count);
|
||||
stepsGauge = fragmentView.findViewById(R.id.steps_gauge);
|
||||
fillData();
|
||||
return fragmentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (stepsCount != null && stepsGauge != null) fillData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillData() {
|
||||
if (stepsGauge == null) return;
|
||||
stepsGauge.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
dashboardData.getStepsTotal();
|
||||
dashboardData.getStepsGoalFactor();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
|
||||
// Update text representation
|
||||
stepsCount.setText(String.valueOf(dashboardData.getStepsTotal()));
|
||||
|
||||
// Draw gauge
|
||||
stepsGauge.setImageBitmap(drawGauge(200, 15, color_activity, dashboardData.getStepsGoalFactor()));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,475 @@
|
||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StepAnalysis;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
/**
|
||||
* A simple {@link AbstractDashboardWidget} subclass.
|
||||
* Use the {@link DashboardTodayWidget#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class DashboardTodayWidget extends AbstractDashboardWidget {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardTodayWidget.class);
|
||||
|
||||
private View todayView;
|
||||
private ImageView todayChart;
|
||||
|
||||
private boolean mode_24h;
|
||||
|
||||
public DashboardTodayWidget() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||
* @return A new instance of fragment DashboardTodayWidget.
|
||||
*/
|
||||
public static DashboardTodayWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
||||
DashboardTodayWidget fragment = new DashboardTodayWidget();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
todayView = inflater.inflate(R.layout.dashboard_widget_today, container, false);
|
||||
todayChart = todayView.findViewById(R.id.dashboard_today_chart);
|
||||
|
||||
// Determine whether to draw a single or a double chart. In case 24h mode is selected,
|
||||
// use just the outer chart (chart_12_24) for all data.
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
mode_24h = prefs.getBoolean("dashboard_widget_today_24h", false);
|
||||
|
||||
// Initialize legend
|
||||
TextView legend = todayView.findViewById(R.id.dashboard_piechart_legend);
|
||||
SpannableString l_not_worn = new SpannableString("■ " + getString(R.string.abstract_chart_fragment_kind_not_worn));
|
||||
l_not_worn.setSpan(new ForegroundColorSpan(color_not_worn), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_worn = new SpannableString("■ " + getString(R.string.activity_type_worn));
|
||||
l_worn.setSpan(new ForegroundColorSpan(color_worn), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_activity = new SpannableString("■ " + getString(R.string.activity_type_activity));
|
||||
l_activity.setSpan(new ForegroundColorSpan(color_activity), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_exercise = new SpannableString("■ " + getString(R.string.activity_type_exercise));
|
||||
l_exercise.setSpan(new ForegroundColorSpan(color_exercise), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_deep_sleep = new SpannableString("■ " + getString(R.string.activity_type_deep_sleep));
|
||||
l_deep_sleep.setSpan(new ForegroundColorSpan(color_deep_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_light_sleep = new SpannableString("■ " + getString(R.string.activity_type_light_sleep));
|
||||
l_light_sleep.setSpan(new ForegroundColorSpan(color_light_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableString l_rem_sleep = new SpannableString("■ " + getString(R.string.abstract_chart_fragment_kind_rem_sleep));
|
||||
l_rem_sleep.setSpan(new ForegroundColorSpan(color_rem_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpannableStringBuilder legendBuilder = new SpannableStringBuilder();
|
||||
legend.setText(legendBuilder.append(l_not_worn).append(" ").append(l_worn).append("\n").append(l_activity).append(" ").append(l_exercise).append("\n").append(l_light_sleep).append(" ").append(l_deep_sleep).append(" ").append(l_rem_sleep));
|
||||
|
||||
legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (dashboardData.generalizedActivities.isEmpty()) {
|
||||
fillData();
|
||||
} else {
|
||||
draw();
|
||||
}
|
||||
|
||||
return todayView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (todayChart != null) fillData();
|
||||
}
|
||||
|
||||
private void draw() {
|
||||
// Prepare circular chart
|
||||
long midDaySecond = dashboardData.timeFrom + (12 * 60 * 60);
|
||||
int width = 500;
|
||||
int height = 500;
|
||||
int barWidth = 40;
|
||||
int hourTextSp = 12;
|
||||
float hourTextPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, hourTextSp, requireContext().getResources().getDisplayMetrics());
|
||||
float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f;
|
||||
float innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
|
||||
float degreeFactor = mode_24h ? 240 : 120;
|
||||
Bitmap todayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(todayBitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
|
||||
// Draw clock stripes
|
||||
float clockMargin = outerCircleMargin + (mode_24h ? barWidth : barWidth*2.3f);
|
||||
int clockStripesInterval = mode_24h ? 15 : 30;
|
||||
float clockStripesWidth = barWidth / 3f;
|
||||
paint.setStrokeWidth(clockStripesWidth);
|
||||
paint.setColor(color_worn);
|
||||
for (int i=0; i<360; i+=clockStripesInterval) {
|
||||
canvas.drawArc(clockMargin, clockMargin, width - clockMargin, height - clockMargin, i, 1, false, paint);
|
||||
}
|
||||
|
||||
// Draw hours
|
||||
boolean normalClock = DateFormat.is24HourFormat(getContext());
|
||||
Map<Integer, String> hours = new HashMap<Integer, String>() {
|
||||
{
|
||||
put(3, "3");
|
||||
put(6, normalClock ? "6" : "6am");
|
||||
put(9, "9");
|
||||
put(12, normalClock ? "12" : "12pm");
|
||||
put(15, normalClock ? "15" : "3");
|
||||
put(18, normalClock ? "18" : "6pm");
|
||||
put(21, normalClock ? "21" : "9");
|
||||
put(24, normalClock ? "24" : "12am");
|
||||
}
|
||||
};
|
||||
Paint textPaint = new Paint();
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setColor(color_worn);
|
||||
textPaint.setTextSize(hourTextPixels);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
Rect textBounds = new Rect();
|
||||
if (mode_24h) {
|
||||
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
|
||||
canvas.drawText(hours.get(6), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
|
||||
canvas.drawText(hours.get(12), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
|
||||
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
|
||||
canvas.drawText(hours.get(18), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.getTextBounds(hours.get(24), 0, hours.get(24).length(), textBounds);
|
||||
canvas.drawText(hours.get(24), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
|
||||
} else {
|
||||
textPaint.getTextBounds(hours.get(3), 0, hours.get(3).length(), textBounds);
|
||||
canvas.drawText(hours.get(3), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
|
||||
canvas.drawText(hours.get(6), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
|
||||
textPaint.getTextBounds(hours.get(9), 0, hours.get(9).length(), textBounds);
|
||||
canvas.drawText(hours.get(9), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
|
||||
canvas.drawText(hours.get(12), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
|
||||
textPaint.getTextBounds(hours.get(15), 0, hours.get(15).length(), textBounds);
|
||||
canvas.drawText(hours.get(15), (float) (width - Math.ceil(textBounds.width() / 2f)), height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
|
||||
canvas.drawText(hours.get(18), width / 2f, height - textBounds.height() / 2f, textPaint);
|
||||
textPaint.setTextAlign(Paint.Align.LEFT);
|
||||
textPaint.getTextBounds(hours.get(21), 0, hours.get(21).length(), textBounds);
|
||||
canvas.drawText(hours.get(21), 1, height / 2f + textBounds.height() / 2f, textPaint);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.getTextBounds(hours.get(24), 0, hours.get(24).length(), textBounds);
|
||||
canvas.drawText(hours.get(24), width / 2f, textBounds.height(), textPaint);
|
||||
}
|
||||
|
||||
// Draw generalized activities on circular chart
|
||||
long secondIndex = dashboardData.timeFrom;
|
||||
long currentTime = Calendar.getInstance().getTimeInMillis() / 1000;
|
||||
synchronized (dashboardData.generalizedActivities) {
|
||||
for (DashboardFragment.DashboardData.GeneralizedActivity activity : dashboardData.generalizedActivities) {
|
||||
// Determine margin depending on 24h/12h mode
|
||||
float margin = (mode_24h || activity.timeFrom >= midDaySecond) ? outerCircleMargin : innerCircleMargin;
|
||||
// Draw inactive slices
|
||||
if (!mode_24h && secondIndex < midDaySecond && activity.timeFrom >= midDaySecond) {
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
|
||||
secondIndex = midDaySecond;
|
||||
}
|
||||
if (activity.timeFrom > secondIndex) {
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (activity.timeFrom - secondIndex) / degreeFactor, false, paint);
|
||||
}
|
||||
float start_angle = 270 + (activity.timeFrom - dashboardData.timeFrom) / degreeFactor;
|
||||
float sweep_angle = (activity.timeTo - activity.timeFrom) / degreeFactor;
|
||||
if (activity.activityKind == ActivityKind.TYPE_NOT_MEASURED) {
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_worn);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else if (activity.activityKind == ActivityKind.TYPE_NOT_WORN) {
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_not_worn);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else if (activity.activityKind == ActivityKind.TYPE_LIGHT_SLEEP || activity.activityKind == ActivityKind.TYPE_SLEEP) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_light_sleep);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else if (activity.activityKind == ActivityKind.TYPE_REM_SLEEP) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_rem_sleep);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else if (activity.activityKind == ActivityKind.TYPE_DEEP_SLEEP) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_deep_sleep);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else if (activity.activityKind == ActivityKind.TYPE_EXERCISE) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_exercise);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
} else {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(color_activity);
|
||||
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
||||
}
|
||||
secondIndex = activity.timeTo;
|
||||
}
|
||||
}
|
||||
// Fill remaining time until current time in 12h mode before midday
|
||||
if (!mode_24h && currentTime < midDaySecond) {
|
||||
// Fill inner bar up until current time
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
|
||||
// Fill inner bar up until midday
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (currentTime - dashboardData.timeFrom) / degreeFactor, (midDaySecond - currentTime) / degreeFactor, false, paint);
|
||||
// Fill outer bar up until midnight
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 0, 360, false, paint);
|
||||
}
|
||||
// Fill remaining time until current time in 24h mode or in 12h mode after midday
|
||||
if ((mode_24h || currentTime >= midDaySecond) && currentTime < dashboardData.timeTo) {
|
||||
// Fill inner bar up until midday
|
||||
if (!mode_24h && secondIndex < midDaySecond) {
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
|
||||
secondIndex = midDaySecond;
|
||||
}
|
||||
// Fill outer bar up until current time
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
|
||||
// Fill outer bar up until midnight
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (currentTime - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - currentTime) / degreeFactor, false, paint);
|
||||
}
|
||||
// Only when displaying a past day
|
||||
if (secondIndex < dashboardData.timeTo && currentTime > dashboardData.timeTo) {
|
||||
// Fill outer bar up until midnight
|
||||
paint.setStrokeWidth(barWidth / 3f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - secondIndex) / degreeFactor, false, paint);
|
||||
}
|
||||
|
||||
todayChart.setImageBitmap(todayBitmap);
|
||||
}
|
||||
|
||||
protected void fillData() {
|
||||
if (todayView == null) return;
|
||||
todayView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||
myAsyncTask.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
private final TreeMap<Long, Integer> activityTimestamps = new TreeMap<>();
|
||||
|
||||
private void addActivity(long timeFrom, long timeTo, int activityKind) {
|
||||
for (long i = timeFrom; i<=timeTo; i++) {
|
||||
// If the current timestamp isn't saved yet, do so immediately
|
||||
if (activityTimestamps.get(i) == null) {
|
||||
activityTimestamps.put(i, activityKind);
|
||||
continue;
|
||||
}
|
||||
// If the current timestamp is already saved, compare the activity kinds and
|
||||
// keep the most 'important' one
|
||||
switch (activityTimestamps.get(i)) {
|
||||
case ActivityKind.TYPE_EXERCISE:
|
||||
break;
|
||||
case ActivityKind.TYPE_ACTIVITY:
|
||||
if (activityKind == ActivityKind.TYPE_EXERCISE)
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
||||
activityKind == ActivityKind.TYPE_ACTIVITY)
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
||||
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
||||
activityKind == ActivityKind.TYPE_DEEP_SLEEP)
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
case ActivityKind.TYPE_REM_SLEEP:
|
||||
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
||||
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
||||
activityKind == ActivityKind.TYPE_DEEP_SLEEP ||
|
||||
activityKind == ActivityKind.TYPE_LIGHT_SLEEP)
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
case ActivityKind.TYPE_SLEEP:
|
||||
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
||||
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
||||
activityKind == ActivityKind.TYPE_DEEP_SLEEP ||
|
||||
activityKind == ActivityKind.TYPE_LIGHT_SLEEP ||
|
||||
activityKind == ActivityKind.TYPE_REM_SLEEP)
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
default:
|
||||
activityTimestamps.put(i, activityKind);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateWornSessions(List<ActivitySample> samples) {
|
||||
int firstTimestamp = 0;
|
||||
int lastTimestamp = 0;
|
||||
|
||||
for (ActivitySample sample : samples) {
|
||||
if (sample.getHeartRate() < 10 && firstTimestamp == 0) continue;
|
||||
if (firstTimestamp == 0) firstTimestamp = sample.getTimestamp();
|
||||
if (lastTimestamp == 0) lastTimestamp = sample.getTimestamp();
|
||||
if ((sample.getHeartRate() < 10 || sample.getTimestamp() > lastTimestamp + dashboardData.hrIntervalSecs) && firstTimestamp != lastTimestamp) {
|
||||
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
|
||||
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
|
||||
if (sample.getHeartRate() < 10) {
|
||||
firstTimestamp = 0;
|
||||
lastTimestamp = 0;
|
||||
} else {
|
||||
firstTimestamp = sample.getTimestamp();
|
||||
lastTimestamp = sample.getTimestamp();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
lastTimestamp = sample.getTimestamp();
|
||||
}
|
||||
if (firstTimestamp != lastTimestamp) {
|
||||
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
|
||||
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
|
||||
}
|
||||
}
|
||||
|
||||
private void createGeneralizedActivities() {
|
||||
DashboardFragment.DashboardData.GeneralizedActivity previous = null;
|
||||
long midDaySecond = dashboardData.timeFrom + (12 * 60 * 60);
|
||||
for (Map.Entry<Long, Integer> activity : activityTimestamps.entrySet()) {
|
||||
long timestamp = activity.getKey();
|
||||
int activityKind = activity.getValue();
|
||||
if (previous == null || previous.activityKind != activityKind || (!mode_24h && timestamp == midDaySecond) || previous.timeTo < timestamp - 60) {
|
||||
previous = new DashboardFragment.DashboardData.GeneralizedActivity(activityKind, timestamp, timestamp);
|
||||
dashboardData.generalizedActivities.add(previous);
|
||||
} else {
|
||||
previous.timeTo = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// Retrieve activity data
|
||||
dashboardData.generalizedActivities.clear();
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
List<ActivitySample> allActivitySamples = new ArrayList<>();
|
||||
List<ActivitySession> stepSessions = new ArrayList<>();
|
||||
List<BaseActivitySummary> activitySummaries = null;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
for (GBDevice dev : devices) {
|
||||
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
List<? extends ActivitySample> activitySamples = DashboardUtils.getAllSamples(dbHandler, dev, dashboardData);
|
||||
allActivitySamples.addAll(activitySamples);
|
||||
StepAnalysis stepAnalysis = new StepAnalysis();
|
||||
stepSessions.addAll(stepAnalysis.calculateStepSessions(activitySamples));
|
||||
}
|
||||
}
|
||||
activitySummaries = DashboardUtils.getWorkoutSamples(dbHandler, dashboardData);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not retrieve activity amounts: ", e);
|
||||
}
|
||||
Collections.sort(allActivitySamples, (lhs, rhs) -> Integer.valueOf(lhs.getTimestamp()).compareTo(rhs.getTimestamp()));
|
||||
|
||||
// Determine worn sessions from heart rate samples
|
||||
calculateWornSessions(allActivitySamples);
|
||||
|
||||
// Integrate various data from multiple devices
|
||||
for (ActivitySample sample : allActivitySamples) {
|
||||
// Handle only TYPE_NOT_WORN and TYPE_SLEEP (including variants) here
|
||||
if (sample.getKind() != ActivityKind.TYPE_NOT_WORN && (sample.getKind() == ActivityKind.TYPE_NOT_MEASURED || (sample.getKind() & ActivityKind.TYPE_SLEEP) == 0))
|
||||
continue;
|
||||
// Add to day results
|
||||
addActivity(sample.getTimestamp(), sample.getTimestamp() + 60, sample.getKind());
|
||||
}
|
||||
if (activitySummaries != null) {
|
||||
for (BaseActivitySummary baseActivitySummary : activitySummaries) {
|
||||
addActivity(baseActivitySummary.getStartTime().getTime() / 1000, baseActivitySummary.getEndTime().getTime() / 1000, ActivityKind.TYPE_EXERCISE);
|
||||
}
|
||||
}
|
||||
for (ActivitySession session : stepSessions) {
|
||||
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.TYPE_ACTIVITY);
|
||||
}
|
||||
createGeneralizedActivities();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void unused) {
|
||||
super.onPostExecute(unused);
|
||||
try {
|
||||
draw();
|
||||
} catch (IllegalStateException e) {
|
||||
LOG.warn("calling draw() failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -845,7 +845,7 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
|
||||
boolean deviceConnected = device.getState() != GBDevice.State.NOT_CONNECTED;
|
||||
|
||||
PopupMenu menu = new PopupMenu(v.getContext(), v);
|
||||
menu.inflate(R.menu.activity_controlcenterv2_device_submenu);
|
||||
menu.inflate(R.menu.fragment_devices_device_submenu);
|
||||
|
||||
final boolean detailsShown = expandedDeviceAddress.equals(device.getAddress());
|
||||
boolean showInfoIcon = device.hasDeviceInfos() && !device.isBusy();
|
||||
|
@ -37,7 +37,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.unknown.UnknownDeviceSupport;
|
||||
|
||||
|
@ -18,7 +18,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -33,8 +32,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class DailyTotals {
|
||||
@ -78,17 +75,17 @@ public class DailyTotals {
|
||||
}
|
||||
|
||||
public long[] getDailyTotalsForDevice(GBDevice device, Calendar day, DBHandler handler) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
ActivityAmounts amountsSteps;
|
||||
ActivityAmounts amountsSleep;
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
ActivityAmounts amountsSteps;
|
||||
ActivityAmounts amountsSleep;
|
||||
|
||||
amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device));
|
||||
amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device));
|
||||
amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device));
|
||||
amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device));
|
||||
|
||||
long[] sleep = getTotalsSleepForActivityAmounts(amountsSleep);
|
||||
long steps = getTotalsStepsForActivityAmounts(amountsSteps);
|
||||
long[] sleep = getTotalsSleepForActivityAmounts(amountsSleep);
|
||||
long steps = getTotalsStepsForActivityAmounts(amountsSteps);
|
||||
|
||||
return new long[]{steps, sleep[0] + sleep[1] + sleep[2]};
|
||||
return new long[]{steps, sleep[0] + sleep[1] + sleep[2]};
|
||||
}
|
||||
|
||||
private long[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
|
@ -0,0 +1,191 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StepAnalysis;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
|
||||
|
||||
public class DashboardUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardUtils.class);
|
||||
|
||||
public static long getSteps(GBDevice device, DBHandler db, int timeTo) {
|
||||
Calendar day = GregorianCalendar.getInstance();
|
||||
day.setTimeInMillis(timeTo * 1000L);
|
||||
DailyTotals ds = new DailyTotals();
|
||||
return ds.getDailyTotalsForDevice(device, day, db)[0];
|
||||
}
|
||||
|
||||
public static int getStepsTotal(DashboardFragment.DashboardData dashboardData) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
int totalSteps = 0;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
for (GBDevice dev : devices) {
|
||||
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
totalSteps += getSteps(dev, dbHandler, dashboardData.timeTo);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not calculate total amount of steps: ", e);
|
||||
}
|
||||
return totalSteps;
|
||||
}
|
||||
|
||||
public static float getStepsGoalFactor(DashboardFragment.DashboardData dashboardData) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
float stepsGoal = activityUser.getStepsGoal();
|
||||
float goalFactor = getStepsTotal(dashboardData) / stepsGoal;
|
||||
if (goalFactor > 1) goalFactor = 1;
|
||||
|
||||
return goalFactor;
|
||||
}
|
||||
|
||||
public static long getSleep(GBDevice device, DBHandler db, int timeTo) {
|
||||
Calendar day = GregorianCalendar.getInstance();
|
||||
day.setTimeInMillis(timeTo * 1000L);
|
||||
DailyTotals ds = new DailyTotals();
|
||||
return ds.getDailyTotalsForDevice(device, day, db)[1];
|
||||
}
|
||||
|
||||
public static long getSleepMinutesTotal(DashboardFragment.DashboardData dashboardData) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
long totalSleepMinutes = 0;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
for (GBDevice dev : devices) {
|
||||
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
totalSleepMinutes += getSleep(dev, dbHandler, dashboardData.timeTo);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not calculate total amount of sleep: ", e);
|
||||
}
|
||||
return totalSleepMinutes;
|
||||
}
|
||||
|
||||
public static float getSleepMinutesGoalFactor(DashboardFragment.DashboardData dashboardData) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
int sleepMinutesGoal = activityUser.getSleepDurationGoal() * 60;
|
||||
float goalFactor = (float) getSleepMinutesTotal(dashboardData) / sleepMinutesGoal;
|
||||
if (goalFactor > 1) goalFactor = 1;
|
||||
|
||||
return goalFactor;
|
||||
}
|
||||
|
||||
public static float getDistanceTotal(DashboardFragment.DashboardData dashboardData) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
long totalSteps = 0;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
for (GBDevice dev : devices) {
|
||||
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
totalSteps += getSteps(dev, dbHandler, dashboardData.timeTo);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not calculate total distance: ", e);
|
||||
}
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
int stepLength = activityUser.getStepLengthCm();
|
||||
return totalSteps * stepLength * 0.01f;
|
||||
}
|
||||
|
||||
public static float getDistanceGoalFactor(DashboardFragment.DashboardData dashboardData) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
int distanceGoal = activityUser.getDistanceGoalMeters();
|
||||
float goalFactor = getDistanceTotal(dashboardData) / distanceGoal;
|
||||
if (goalFactor > 1) goalFactor = 1;
|
||||
|
||||
return goalFactor;
|
||||
}
|
||||
|
||||
public static long getActiveMinutesTotal(DashboardFragment.DashboardData dashboardData) {
|
||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||
long totalActiveMinutes = 0;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
for (GBDevice dev : devices) {
|
||||
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
|
||||
totalActiveMinutes += getActiveMinutes(dev, dbHandler, dashboardData);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not calculate total amount of activity: ", e);
|
||||
}
|
||||
return totalActiveMinutes;
|
||||
}
|
||||
|
||||
public static float getActiveMinutesGoalFactor(DashboardFragment.DashboardData dashboardData) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
int activeTimeGoal = activityUser.getActiveTimeGoalMinutes();
|
||||
float goalFactor = (float) getActiveMinutesTotal(dashboardData) / activeTimeGoal;
|
||||
if (goalFactor > 1) goalFactor = 1;
|
||||
|
||||
return goalFactor;
|
||||
}
|
||||
|
||||
public static long getActiveMinutes(GBDevice gbDevice, DBHandler db, DashboardFragment.DashboardData dashboardData) {
|
||||
ActivitySession stepSessionsSummary = new ActivitySession();
|
||||
List<ActivitySession> stepSessions;
|
||||
List<? extends ActivitySample> activitySamples = getAllSamples(db, gbDevice, dashboardData);
|
||||
StepAnalysis stepAnalysis = new StepAnalysis();
|
||||
|
||||
boolean isEmptySummary = false;
|
||||
if (activitySamples != null) {
|
||||
stepSessions = stepAnalysis.calculateStepSessions(activitySamples);
|
||||
if (stepSessions.toArray().length == 0) {
|
||||
isEmptySummary = true;
|
||||
}
|
||||
stepSessionsSummary = stepAnalysis.calculateSummary(stepSessions, isEmptySummary);
|
||||
}
|
||||
long duration = stepSessionsSummary.getEndTime().getTime() - stepSessionsSummary.getStartTime().getTime();
|
||||
return duration / 1000 / 60;
|
||||
}
|
||||
|
||||
public static List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, DashboardFragment.DashboardData dashboardData) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
return provider.getAllActivitySamples(dashboardData.timeFrom, dashboardData.timeTo);
|
||||
}
|
||||
|
||||
protected static SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator.getSampleProvider(device, db.getDaoSession());
|
||||
}
|
||||
|
||||
public static List<BaseActivitySummary> getWorkoutSamples(DBHandler db, DashboardFragment.DashboardData dashboardData) {
|
||||
return db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where(
|
||||
BaseActivitySummaryDao.Properties.StartTime.gt(new Date(dashboardData.timeFrom * 1000L)),
|
||||
BaseActivitySummaryDao.Properties.EndTime.lt(new Date(dashboardData.timeTo * 1000L))
|
||||
).build().list();
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ import android.text.format.DateUtils;
|
||||
|
||||
import com.github.pfichtner.durationformatter.DurationFormatter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.FieldPosition;
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
@ -206,12 +205,9 @@ public class DateTimeUtils {
|
||||
* @param days
|
||||
*/
|
||||
public static int shiftDays(int time, int days) {
|
||||
int newTime = time + ((24 * 3600) - 1) * days;
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTimeInMillis(newTime * 1000L);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.setTimeInMillis(time * 1000L);
|
||||
day.add(Calendar.DAY_OF_YEAR, days);
|
||||
return (int) (day.getTimeInMillis() / 1000);
|
||||
}
|
||||
|
||||
@ -225,4 +221,28 @@ public class DateTimeUtils {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays((time2 - time1) * 1000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether two Calendar instances are on the same day
|
||||
*
|
||||
* @param calendar1 The first calendar to compare
|
||||
* @param calendar2 The second calendar to compare
|
||||
* @return true if the Calendar instances are on the same day
|
||||
*/
|
||||
public static boolean isSameDay(Calendar calendar1, Calendar calendar2) {
|
||||
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR)
|
||||
&& calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH)
|
||||
&& calendar1.get(Calendar.DAY_OF_MONTH) == calendar2.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether two Calendar instances are in the same month
|
||||
*
|
||||
* @param calendar1 The first calendar to compare
|
||||
* @param calendar2 The second calendar to compare
|
||||
* @return true if the Calendar instances are in the same month
|
||||
*/
|
||||
public static boolean isSameMonth(Calendar calendar1, Calendar calendar2) {
|
||||
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR)
|
||||
&& calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH);
|
||||
}
|
||||
}
|
||||
|
@ -104,4 +104,12 @@ public class GBChangeLog extends ChangeLog {
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
public static GBChangeLog createChangeLog(Context context) {
|
||||
String css = GBChangeLog.DEFAULT_CSS;
|
||||
css += "body { "
|
||||
+ "color: " + AndroidUtils.getTextColorHex(context) + "; "
|
||||
+ "}";
|
||||
return new GBChangeLog(context, css);
|
||||
}
|
||||
}
|
||||
|
10
app/src/main/res/drawable/ic_dashboard.xml
Normal file
10
app/src/main/res/drawable/ic_dashboard.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
|
||||
</vector>
|
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimaryDark" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<include layout="@layout/activity_controlcenterv2_content_main" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
android:layout_margin="16dp" />
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
48
app/src/main/res/layout/activity_dashboard_calendar.xml
Normal file
48
app/src/main/res/layout/activity_dashboard_calendar.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<android.widget.LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activities.dashboard.DashboardCalendarActivity">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/calendar_month"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/calendar_month"
|
||||
android:textStyle="bold"
|
||||
android:textSize="30sp" />
|
||||
<TextView
|
||||
android:id="@+id/arrow_left"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\u003C"
|
||||
android:textStyle="bold"
|
||||
android:textSize="40sp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:layout_toLeftOf="@+id/arrow_right" />
|
||||
<TextView
|
||||
android:id="@+id/arrow_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\u003E"
|
||||
android:textStyle="bold"
|
||||
android:textSize="40sp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.gridlayout.widget.GridLayout
|
||||
android:id="@+id/dashboard_calendar_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
app:columnCount="7" />
|
||||
|
||||
</android.widget.LinearLayout>
|
@ -6,10 +6,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:openDrawer="start">
|
||||
tools:openDrawer="start"
|
||||
tools:context=".activities.ControlCenterv2">
|
||||
|
||||
<include
|
||||
layout="@layout/activity_controlcenterv2_app_bar_main"
|
||||
layout="@layout/activity_main_app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@ -19,7 +20,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header_main"
|
||||
app:menu="@menu/activity_controlcenterv2_main_drawer" />
|
||||
app:headerLayout="@layout/main_drawer_header"
|
||||
app:menu="@menu/activity_main_drawer" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
44
app/src/main/res/layout/activity_main_app_bar.xml
Normal file
44
app/src/main/res/layout/activity_main_app_bar.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".activities.ControlCenterv2">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbarlayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimaryDark" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
app:navGraph="@navigation/main"
|
||||
app:defaultNavHost="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbarlayout"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_nav_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
42
app/src/main/res/layout/dashboard_widget_active_time.xml
Normal file
42
app/src/main/res/layout/dashboard_widget_active_time.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardActiveTimeWidget">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/card_layout">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart"
|
||||
android:id="@+id/activetime_gauge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activetime_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="0:00"
|
||||
android:textSize="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/activetime_text"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/activity_list_summary_active_time" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
42
app/src/main/res/layout/dashboard_widget_distance.xml
Normal file
42
app/src/main/res/layout/dashboard_widget_distance.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardDistanceWidget">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/card_layout">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart"
|
||||
android:id="@+id/distance_gauge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/distance_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="0.0km"
|
||||
android:textSize="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/distance_text"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/distance" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
35
app/src/main/res/layout/dashboard_widget_goals.xml
Normal file
35
app/src/main/res/layout/dashboard_widget_goals.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardGoalsWidget">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/dashboard_goals_chart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="15dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:scaleType="fitXY" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dashboard_goals_legend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:textAlignment="center" />
|
||||
</LinearLayout>
|
42
app/src/main/res/layout/dashboard_widget_sleep.xml
Normal file
42
app/src/main/res/layout/dashboard_widget_sleep.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardSleepWidget">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/card_layout">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart"
|
||||
android:id="@+id/sleep_gauge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sleep_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="0:00"
|
||||
android:textSize="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/sleep_text"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/menuitem_sleep" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
42
app/src/main/res/layout/dashboard_widget_steps.xml
Normal file
42
app/src/main/res/layout/dashboard_widget_steps.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardStepsWidget">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/card_layout">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart"
|
||||
android:id="@+id/steps_gauge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/steps_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="0"
|
||||
android:textSize="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/steps_count"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/steps" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
35
app/src/main/res/layout/dashboard_widget_today.xml
Normal file
35
app/src/main/res/layout/dashboard_widget_today.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical"
|
||||
tools:context=".activities.dashboard.DashboardTodayWidget">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<ImageView
|
||||
android:id="@+id/dashboard_today_chart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="15dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:scaleType="fitXY" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dashboard_piechart_legend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:textAlignment="center" />
|
||||
</LinearLayout>
|
64
app/src/main/res/layout/fragment_dashboard.xml
Normal file
64
app/src/main/res/layout/fragment_dashboard.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.DashboardFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/dashboard_swipe_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/dashboard_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/activity_summary_today"
|
||||
android:textStyle="bold"
|
||||
android:textSize="30sp" />
|
||||
<TextView
|
||||
android:id="@+id/arrow_left"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\u003C"
|
||||
android:textStyle="bold"
|
||||
android:textSize="40sp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:layout_toLeftOf="@+id/arrow_right" />
|
||||
<TextView
|
||||
android:id="@+id/arrow_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\u003E"
|
||||
android:textStyle="bold"
|
||||
android:textSize="40sp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.gridlayout.widget.GridLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/dashboard_gridlayout"
|
||||
app:columnCount="2" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</RelativeLayout>
|
@ -6,9 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2"
|
||||
tools:showIn="@layout/activity_controlcenterv2_app_bar_main">
|
||||
|
||||
tools:context=".activities.DevicesFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/no_items_bg"
|
||||
@ -26,4 +24,13 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:divider="@null" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
android:layout_margin="16dp" />
|
||||
|
||||
</RelativeLayout>
|
@ -140,4 +140,4 @@
|
||||
</RelativeLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
13
app/src/main/res/menu/bottom_nav_menu.xml
Normal file
13
app/src/main/res/menu/bottom_nav_menu.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:title="@string/bottom_nav_dashboard"
|
||||
android:id="@+id/bottom_nav_dashboard"
|
||||
android:icon="@drawable/ic_dashboard"/>
|
||||
<item
|
||||
android:title="@string/bottom_nav_devices"
|
||||
android:id="@+id/bottom_nav_devices"
|
||||
android:icon="@drawable/ic_devices_other"/>
|
||||
|
||||
</menu>
|
12
app/src/main/res/menu/dashboard_menu.xml
Normal file
12
app/src/main/res/menu/dashboard_menu.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".activities.DashboardFragment">
|
||||
|
||||
<item
|
||||
android:id="@+id/dashboard_show_calendar"
|
||||
android:icon="@drawable/ic_calendar_from"
|
||||
android:title="@string/menuitem_calendar"
|
||||
app:iconTint="?attr/actionmenu_icon_color"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
26
app/src/main/res/navigation/main.xml
Normal file
26
app/src/main/res/navigation/main.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
app:startDestination="@id/bottom_nav_dashboard">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/bottom_nav_dashboard"
|
||||
android:name="nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment"
|
||||
android:label="fragment_dashboard"
|
||||
tools:layout="@layout/fragment_dashboard">
|
||||
<action
|
||||
android:id="@+id/dashboard_to_devices"
|
||||
app:destination="@id/bottom_nav_devices" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/bottom_nav_devices"
|
||||
android:name="nodomain.freeyourgadget.gadgetbridge.activities.DevicesFragment"
|
||||
android:label="fragment_devices"
|
||||
tools:layout="@layout/fragment_devices">
|
||||
<action
|
||||
android:id="@+id/devices_to_dashboard"
|
||||
app:destination="@id/bottom_nav_dashboard" />
|
||||
</fragment>
|
||||
</navigation>
|
@ -3843,4 +3843,22 @@
|
||||
<item>@string/pref_force_connection_type_ble_value</item>
|
||||
<item>@string/pref_force_connection_type_bt_classic_value</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_dashboard_widgets_order">
|
||||
<item>@string/pref_dashboard_widget_today_title</item>
|
||||
<item>@string/pref_dashboard_widget_goals_chart_title</item>
|
||||
<item>@string/steps</item>
|
||||
<item>@string/distance</item>
|
||||
<item>@string/active_time</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_dashboard_widgets_order_values">
|
||||
<item>today</item>
|
||||
<item>goals</item>
|
||||
<item>steps</item>
|
||||
<item>distance</item>
|
||||
<item>activetime</item>
|
||||
<item>sleep</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
@ -2417,7 +2417,7 @@
|
||||
<string name="huawei_reparse_workout_data">"Reparse workout data"</string>
|
||||
<string name="huawei_reparse_workout_data_description">"This will only do something after certain updates"</string>
|
||||
|
||||
<string name="info_no_devices_connected">no devices connected</string>
|
||||
<string name="info_no_devices_connected">No devices connected</string>
|
||||
<string name="info_connected_count">%d devices connected</string>
|
||||
<string name="controlcenter_set_parent_folder">Set parent folder</string>
|
||||
<string name="controlcenter_set_preferences">Set preferences</string>
|
||||
@ -2748,4 +2748,30 @@
|
||||
<string name="devicesetting_scannable_debounce_summary">After being scanned, the device will stick as scanned and ignored for the specified amount of time</string>
|
||||
<string name="devicesetting_scannable_minimum_unseen_summary">After being scanned, the device has to be unseen for this amount of time before being registered again</string>
|
||||
<string name="devicesetting_scannable_rssi_summary">The minimum RSSI threshold for detection</string>
|
||||
<string name="error_showing_changelog">Error showing Changelog</string>
|
||||
<string name="activity_type_worn">Worn</string>
|
||||
<string name="dashboard_settings">Dashboard settings</string>
|
||||
<string name="bottom_nav_dashboard">Dashboard</string>
|
||||
<string name="bottom_nav_devices">Devices</string>
|
||||
<string name="pref_dashboard_first_title">Show dashboard first</string>
|
||||
<string name="pref_dashboard_first_summary">Show the dashboard when Gadgetbridge starts, instead of the devices screen</string>
|
||||
<string name="pref_dashboard_cards_title">Show widgets on cards</string>
|
||||
<string name="pref_dashboard_cards_summary">Draw cards around the widgets on the dashboard</string>
|
||||
<string name="pref_dashboard_widget_settings">Widget settings</string>
|
||||
<string name="pref_dashboard_widget_today_title">Activity chart</string>
|
||||
<string name="pref_dashboard_widget_today_24h_title">24h mode</string>
|
||||
<string name="pref_dashboard_widget_double_size_title">Double size</string>
|
||||
<string name="pref_dashboard_widget_show_legend_title">Show legend</string>
|
||||
<string name="pref_dashboard_widget_goals_chart_title">Goals chart</string>
|
||||
<string name="pref_dashboard_devices_to_include">Devices to include</string>
|
||||
<string name="pref_dashboard_all_devices_title">All devices</string>
|
||||
<string name="pref_dashboard_select_devices_title">Select devices...</string>
|
||||
<string name="pref_dashboard_widgets_order_summary">Select which widgets are enabled and in what order they are displayed on the dashboard</string>
|
||||
<string name="pref_dashboard_widget_today_24h_summary">Show the activity in a single 24h circle instead of a double 12h circle</string>
|
||||
<string name="pref_dashboard_widget_double_size_summary">Allow the widget to take up two columns on the dashboard</string>
|
||||
<string name="pref_dashboard_widget_show_legend_summary">Show a legend below the widget explaining the colors</string>
|
||||
<string name="pref_dashboard_all_devices_summary">Combine activity data from all added devices for the totals on the dashboard</string>
|
||||
<string name="pref_dashboard_select_devices_summary">Combine activity data from specific devices for the totals on the dashboard</string>
|
||||
<string name="pref_dashboard_widget_today_hr_interval_title">Heart rate interval</string>
|
||||
<string name="pref_dashboard_widget_today_hr_interval_summary">The amount of minutes the chart shows \'worn\' after each successful heart rate measurement</string>
|
||||
</resources>
|
||||
|
@ -145,6 +145,7 @@
|
||||
</style>
|
||||
<style name="GadgetbridgeThemeDynamic.ActionBar" parent="Widget.Material3.ActionBar.Solid">
|
||||
<item name="background">?attr/colorSurface</item>
|
||||
<item name="elevation">0dp</item>
|
||||
</style>
|
||||
<style name="GadgetbridgeTheme.DrawerButtonStyle" parent="@style/Widget.AppCompat.DrawerArrowToggle">
|
||||
<item name="spinBars">true</item>
|
||||
|
115
app/src/main/res/xml/dashboard_preferences.xml
Normal file
115
app/src/main/res/xml/dashboard_preferences.xml
Normal file
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_dashboard_general"
|
||||
android:title="@string/pref_header_general"
|
||||
app:iconSpaceReserved="false">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_as_default_view"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_first_title"
|
||||
android:summary="@string/pref_dashboard_first_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_cards_enabled"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_cards_title"
|
||||
android:summary="@string/pref_dashboard_cards_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<com.mobeta.android.dslv.DragSortListPreference
|
||||
android:defaultValue="@array/pref_dashboard_widgets_order_values"
|
||||
android:dialogTitle="@string/menuitem_widgets"
|
||||
android:entries="@array/pref_dashboard_widgets_order"
|
||||
android:entryValues="@array/pref_dashboard_widgets_order_values"
|
||||
android:key="pref_dashboard_widgets_order"
|
||||
android:persistent="true"
|
||||
android:summary="@string/pref_dashboard_widgets_order_summary"
|
||||
android:title="@string/menuitem_widgets"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_dashboard_widgets"
|
||||
android:title="@string/pref_dashboard_widget_settings"
|
||||
app:iconSpaceReserved="false">
|
||||
<PreferenceScreen
|
||||
android:key="pref_key_dashboard_today"
|
||||
android:title="@string/pref_dashboard_widget_today_title"
|
||||
app:iconSpaceReserved="false">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="dashboard_widget_today_24h"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_widget_today_24h_title"
|
||||
android:summary="@string/pref_dashboard_widget_today_24h_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_widget_today_2columns"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_widget_double_size_title"
|
||||
android:summary="@string/pref_dashboard_widget_double_size_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_widget_today_legend"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_widget_show_legend_title"
|
||||
android:summary="@string/pref_dashboard_widget_show_legend_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="1"
|
||||
android:inputType="number"
|
||||
android:key="dashboard_widget_today_hr_interval"
|
||||
android:maxLength="4"
|
||||
android:summary="@string/pref_dashboard_widget_today_hr_interval_summary"
|
||||
android:title="@string/pref_dashboard_widget_today_hr_interval_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceScreen>
|
||||
<PreferenceScreen
|
||||
android:key="pref_key_dashboard_goals"
|
||||
android:title="@string/pref_dashboard_widget_goals_chart_title"
|
||||
app:iconSpaceReserved="false">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_widget_goals_2columns"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_widget_double_size_title"
|
||||
android:summary="@string/pref_dashboard_widget_double_size_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_widget_goals_legend"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_widget_show_legend_title"
|
||||
android:summary="@string/pref_dashboard_widget_show_legend_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceScreen>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_dashboard_devices"
|
||||
android:title="@string/pref_dashboard_devices_to_include"
|
||||
app:iconSpaceReserved="false">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dashboard_devices_all"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_dashboard_all_devices_title"
|
||||
android:summary="@string/pref_dashboard_all_devices_summary"
|
||||
android:disableDependentsState="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
<MultiSelectListPreference
|
||||
android:dependency="dashboard_devices_all"
|
||||
android:dialogTitle="@string/pref_dashboard_select_devices_title"
|
||||
android:entries="@array/empty_array"
|
||||
android:entryValues="@array/empty_array"
|
||||
android:key="dashboard_devices_multiselect"
|
||||
android:title="@string/pref_dashboard_select_devices_title"
|
||||
android:summary="@string/pref_dashboard_select_devices_summary"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
@ -58,6 +58,11 @@
|
||||
android:title="@string/pref_title_audio_player"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_category_dashboard"
|
||||
android:title="@string/bottom_nav_dashboard"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="pref_screen_theme"
|
||||
android:title="@string/pref_title_theme"
|
||||
|
Loading…
Reference in New Issue
Block a user