Make workouts list async

This commit is contained in:
José Rebelo
2025-08-22 09:54:04 +01:00
parent 8daea07c39
commit 7476ea98c2
8 changed files with 1278 additions and 1248 deletions
+3 -3
View File
@@ -247,7 +247,7 @@
android:label="@string/notifications_app_icon_upload"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".activities.ActivitySummariesActivity"
android:name=".activities.workouts.WorkoutListActivity"
android:label="@string/activity_summaries"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
@@ -663,12 +663,12 @@
<activity
android:name=".activities.workouts.WorkoutDetailsActivity"
android:label="@string/activity_summary_detail"
android:parentActivityName=".activities.ActivitySummariesActivity"
android:parentActivityName=".activities.workouts.WorkoutListActivity"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.ActivitySummariesFilter"
android:label="@string/activity_filter_filter_title"
android:parentActivityName=".activities.ActivitySummariesActivity"
android:parentActivityName=".activities.workouts.WorkoutListActivity"
android:excludeFromRecents="true"/>
<activity
@@ -1,460 +0,0 @@
/* Copyright (C) 2017-2024 Andreas Shimokawa, Arjan Schrijver, Carsten
Pfeiffer, Daniele Gobbetti, José Rebelo, Petr Vaněk
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.app.DatePickerDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutDetailsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ActivitySummariesActivity extends AbstractListActivity<BaseActivitySummary> {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesActivity.class);
static final int ACTIVITY_FILTER = 1;
static final int ACTIVITY_DETAIL = 11;
HashMap<String, ActivityKind> activityKindMap = new HashMap<>(0);
int activityFilter = 0;
long dateFromFilter = 0;
long dateToFilter = 0;
long deviceFilter;
List<Long> itemsFilter;
String nameContainsFilter;
private GBDevice mGBDevice;
private BitSet selectedItems;
private SwipeRefreshLayout swipeLayout;
private ActionMode mActionMode;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
LOG.warn("Got unexpected action {}", intent.getAction());
return;
}
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device == null) {
LOG.error("Got device changed without device");
return;
}
if (!device.equals(mGBDevice)) {
return;
}
if (device.isBusy()) {
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
if (wasBusy) {
refresh();
}
}
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.activity_list_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
// back button, close drawer if open, otherwise exit
finish();
return true;
} else if (itemId == R.id.activity_action_manage_timestamp) {
resetFetchTimestampToChosenDate();
return true;
} else if (itemId == R.id.activity_action_filter) {
runFilterActivity();
return true;
}
return false;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == ACTIVITY_FILTER && resultData != null && resultData.getExtras() != null) {
Bundle bundle = resultData.getExtras();
activityFilter = bundle.getInt("activityFilter", 0);
dateFromFilter = bundle.getLong("dateFromFilter", 0);
dateToFilter = bundle.getLong("dateToFilter", 0);
deviceFilter = bundle.getLong("deviceFilter", 0);
nameContainsFilter = bundle.getString("nameContainsFilter");
//noinspection unchecked
itemsFilter = (List<Long>) bundle.getSerializable("itemsFilter");
setActivityKindFilter(activityFilter);
setDateFromFilter(dateFromFilter);
setDateToFilter(dateToFilter);
setNameContainsFilter(nameContainsFilter);
setItemsFilter(itemsFilter);
setDeviceFilter(deviceFilter);
refresh();
}
if (requestCode == ACTIVITY_DETAIL) {
refresh();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
deviceFilter = getDeviceId(mGBDevice);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
ActivitySummariesAdapter activitySummariesAdapter = new ActivitySummariesAdapter(this, mGBDevice, activityFilter, dateFromFilter, dateToFilter, nameContainsFilter, deviceFilter, itemsFilter);
selectedItems = activitySummariesAdapter.getSelectedItems();
activitySummariesAdapter.setOnItemClickListener(position -> {
if (!selectedItems.isEmpty()) {
selectedItems.set(position, !selectedItems.get(position));
activitySummariesAdapter.notifyItemChanged(position);
if (!selectedItems.isEmpty()) {
startActionMode();
} else {
stopActionMode();
}
return;
}
if (position == 0) return; // item 0 is empty for dashboard
ActivitySummary summary = activitySummariesAdapter.getItem(position);
if (summary != null) {
try {
showActivityDetail(position - 1);
} catch (Exception e) {
GB.toast(getApplicationContext(), "Unable to display Activity Detail, maybe the activity is not available yet: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
});
activitySummariesAdapter.setOnItemLongClickListener(position -> {
selectedItems.set(position, !selectedItems.get(position));
activitySummariesAdapter.notifyItemChanged(position);
if (!selectedItems.isEmpty()) {
startActionMode();
} else {
stopActionMode();
}
});
setItemAdapter(activitySummariesAdapter);
swipeLayout = findViewById(R.id.list_activity_swipe_layout);
swipeLayout.setOnRefreshListener(this::fetchTrackData);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> fetchTrackData());
activityKindMap = fillKindMap();
}
private void stopActionMode() {
if (mActionMode != null) {
mActionMode.finish();
mActionMode = null;
}
}
private void startActionMode() {
int[] numSelected = new int[]{0};
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
numSelected[0]++;
}
}
if (mActionMode != null) {
// already in action mode
mActionMode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
return;
}
mActionMode = startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
mode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
getMenuInflater().inflate(R.menu.activity_list_context_menu, menu);
findViewById(R.id.fab).setVisibility(View.INVISIBLE);
return true;
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
boolean processed = false;
final int itemId = menuItem.getItemId();
if (itemId == R.id.activity_action_delete) {
final List<BaseActivitySummary> toDelete = new ArrayList<>();
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
toDelete.add(getItemAdapter().getItem(i));
}
}
new MaterialAlertDialogBuilder(ActivitySummariesActivity.this)
.setTitle(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_title, toDelete.size()))
.setMessage(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_description, toDelete.size()))
.setIcon(R.drawable.ic_delete_forever)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> deleteItems(toDelete))
.setNegativeButton(android.R.string.no, null)
.show();
processed = true;
} else if (itemId == R.id.activity_action_export) {
final List<String> paths = new ArrayList<>();
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
BaseActivitySummary summary = getItemAdapter().getItem(i);
if (summary != null) {
File gpxTrack = ActivitySummaryUtils.getGpxFile(summary);
if (gpxTrack != null) {
paths.add(gpxTrack.getPath());
}
}
}
}
shareMultiple(paths);
processed = true;
} else if (itemId == R.id.activity_action_select_all) {
for (int i = 1; i < getItemAdapter().getItemCount() - 1; i++) {
if (!selectedItems.get(i)) {
numSelected[0]++;
selectedItems.set(i, true);
getItemAdapter().notifyItemChanged(i);
}
}
actionMode.setTitle(getString(R.string.number_selected_items, numSelected[0]));
return true; //don't finish actionMode in this case!
} else if (itemId == R.id.activity_action_addto_filter) {
final List<Long> toFilter = new ArrayList<>();
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
BaseActivitySummary summary = getItemAdapter().getItem(i);
if (summary != null && summary.getId() != null) {
Long id = summary.getId();
toFilter.add(id);
}
}
}
itemsFilter = toFilter;
setItemsFilter(itemsFilter);
refresh();
processed = true;
}
actionMode.finish();
mActionMode = null;
return processed;
}
@Override
public void onDestroyActionMode(final ActionMode mode) {
mActionMode = null;
for (int i = 0; i < selectedItems.length(); i++) {
if (selectedItems.get(i)) {
selectedItems.set(i, false);
getItemAdapter().notifyItemChanged(i);
}
}
findViewById(R.id.fab).setVisibility(View.VISIBLE);
}
});
}
private LinkedHashMap<String, ActivityKind> fillKindMap() {
LinkedHashMap<String, ActivityKind> newMap = new LinkedHashMap<>(0); //reset
newMap.put(getString(R.string.activity_summaries_all_activities), ActivityKind.UNKNOWN);
for (BaseActivitySummary item : getItemAdapter().getItems()) {
String activityName = ActivityKind.fromCode(item.getActivityKind()).getLabel(this);
if (!newMap.containsKey(activityName) && item.getActivityKind() != 0) {
newMap.put(activityName, ActivityKind.fromCode(item.getActivityKind()));
}
}
return newMap;
}
public void resetFetchTimestampToChosenDate() {
final Calendar currentDate = Calendar.getInstance();
new DatePickerDialog(this, (view, year, monthOfYear, dayOfMonth) -> {
Calendar date = Calendar.getInstance();
date.set(year, monthOfYear, dayOfMonth);
long timestamp = date.getTimeInMillis() - 1000;
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong("lastSportsActivityTimeMillis", timestamp);
editor.apply();
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
private void deleteItems(List<BaseActivitySummary> items) {
for (BaseActivitySummary item : items) {
try {
item.delete();
} catch (Exception e) {
//pass delete error
}
}
// Adapter is fully reloaded after refresh
refresh();
}
private void fetchTrackData() {
if (mGBDevice.isInitialized() && !mGBDevice.isBusy()) {
swipeLayout.setRefreshing(true);
GBApplication.deviceService(mGBDevice).onFetchRecordedData(RecordedDataTypes.TYPE_GPS_TRACKS);
} else {
swipeLayout.setRefreshing(false);
if (!mGBDevice.isInitialized()) {
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR);
}
}
}
private void shareMultiple(List<String> paths) {
ArrayList<Uri> uris = new ArrayList<>();
for (String path : paths) {
File file = new File(path);
uris.add(FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".screenshot_provider", file));
}
if (!uris.isEmpty()) {
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("application/gpx+xml");
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
startActivity(Intent.createChooser(intent, "SHARE"));
} else {
GB.toast(this, "No selected activity contains a GPX track to share", Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void showActivityDetail(final int position) {
final Intent intent = WorkoutDetailsActivity.Companion.createFilteredListIntent(
this,
mGBDevice,
position,
activityFilter,
dateFromFilter,
dateToFilter,
nameContainsFilter,
deviceFilter,
itemsFilter
);
startActivityForResult(intent, ACTIVITY_DETAIL);
}
private void runFilterActivity() {
Intent filterIntent = new Intent(this, ActivitySummariesFilter.class);
Bundle bundle = new Bundle();
bundle.putSerializable("activityKindMap", activityKindMap);
bundle.putSerializable("itemsFilter", (Serializable) itemsFilter);
bundle.putInt("activityFilter", activityFilter);
bundle.putLong("dateFromFilter", dateFromFilter);
bundle.putLong("dateToFilter", dateToFilter);
bundle.putLong("deviceFilter", deviceFilter);
bundle.putLong("initial_deviceFilter", getDeviceId(mGBDevice));
bundle.putString("nameContainsFilter", nameContainsFilter);
filterIntent.putExtras(bundle);
startActivityForResult(filterIntent, ACTIVITY_FILTER);
}
private long getDeviceId(final GBDevice device) {
try (DBHandler handler = GBApplication.acquireDB()) {
final Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
return Objects.requireNonNull(dbDevice).getId();
} catch (Exception e) {
LOG.error("Failed to get device id", e);
}
return 0;
}
}
@@ -0,0 +1,469 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
import android.app.DatePickerDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.FileProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractListActivity
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesFilter
import nodomain.freeyourgadget.gadgetbridge.adapter.WorkoutSummariesAdapter
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes
import nodomain.freeyourgadget.gadgetbridge.util.ActivitySummaryUtils
import nodomain.freeyourgadget.gadgetbridge.util.GB
import org.slf4j.LoggerFactory
import java.io.File
import java.util.BitSet
import java.util.Calendar
class WorkoutListActivity : AbstractListActivity<BaseActivitySummary>() {
private val viewModel: WorkoutListViewModel by viewModels()
private var activityKindMap = HashMap<String, ActivityKind>(0)
private var activityFilter = 0
private var dateFromFilter = 0L
private var dateToFilter = 0L
private var deviceFilter = 0L
private var itemsFilter: List<Long>? = null
private var nameContainsFilter: String? = null
private var gbDevice: GBDevice? = null
private var selectedItems: BitSet? = null
private lateinit var swipeLayout: SwipeRefreshLayout
private var actionMode: ActionMode? = null
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != GBDevice.ACTION_DEVICE_CHANGED) {
LOG.warn("Got unexpected action {}", intent.action)
return
}
val device = intent.getParcelableExtra<GBDevice>(GBDevice.EXTRA_DEVICE)
if (device == null) {
LOG.error("Got device changed without device")
return
}
if (device != gbDevice) {
return
}
if (device.isBusy) {
swipeLayout.isRefreshing = true
} else {
val wasBusy = swipeLayout.isRefreshing
swipeLayout.isRefreshing = false
if (wasBusy) {
refresh()
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.activity_list_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
R.id.activity_action_manage_timestamp -> {
resetFetchTimestampToChosenDate()
true
}
R.id.activity_action_filter -> {
runFilterActivity()
true
}
else -> false
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
resultData?.extras?.let { bundle ->
when (requestCode) {
ACTIVITY_FILTER -> {
activityFilter = bundle.getInt("activityFilter", 0)
dateFromFilter = bundle.getLong("dateFromFilter", 0)
dateToFilter = bundle.getLong("dateToFilter", 0)
deviceFilter = bundle.getLong("deviceFilter", 0)
nameContainsFilter = bundle.getString("nameContainsFilter")
@Suppress("UNCHECKED_CAST")
itemsFilter = bundle.getSerializable("itemsFilter") as? List<Long>
refresh()
}
ACTIVITY_DETAIL -> refresh()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE)
?: throw IllegalArgumentException("Must provide a device when invoking this activity")
deviceFilter = getDeviceId(gbDevice!!)
val filterLocal = IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filterLocal)
super.onCreate(savedInstanceState)
setupViews()
setupViewModel()
}
private fun setupViews() {
val workoutSummariesAdapter = WorkoutSummariesAdapter(
this,
gbDevice!!,
activityFilter,
dateFromFilter,
dateToFilter,
nameContainsFilter,
deviceFilter,
itemsFilter
)
selectedItems = workoutSummariesAdapter.selectedItems
workoutSummariesAdapter.setOnItemClickListener { position ->
if (!selectedItems!!.isEmpty) {
selectedItems!!.set(position, !selectedItems!!.get(position))
workoutSummariesAdapter.notifyItemChanged(position)
if (!selectedItems!!.isEmpty) {
startActionMode()
} else {
stopActionMode()
}
return@setOnItemClickListener
}
if (position == 0) return@setOnItemClickListener // item 0 is empty for dashboard
val summary = workoutSummariesAdapter.getItem(position)
if (summary != null) {
try {
showActivityDetail(position - 1)
} catch (e: Exception) {
GB.toast(
applicationContext,
"Unable to display Activity Detail: ${e.localizedMessage}",
Toast.LENGTH_LONG,
GB.ERROR,
e
)
}
}
}
workoutSummariesAdapter.setOnItemLongClickListener { position ->
selectedItems!!.set(position, !selectedItems!!.get(position))
workoutSummariesAdapter.notifyItemChanged(position)
if (!selectedItems!!.isEmpty) {
startActionMode()
} else {
stopActionMode()
}
}
setItemAdapter(workoutSummariesAdapter)
swipeLayout = findViewById(R.id.list_activity_swipe_layout)
swipeLayout.setOnRefreshListener { fetchTrackData() }
val fab: FloatingActionButton = findViewById(R.id.fab)
fab.setOnClickListener { fetchTrackData() }
}
private fun setupViewModel() {
viewModel.summaries.observe(this) { summaries ->
itemAdapter?.setItems(summaries, true)
activityKindMap = fillKindMap()
}
viewModel.isLoading.observe(this) { isLoading ->
swipeLayout.isRefreshing = isLoading
}
viewModel.error.observe(this) { error ->
error?.let {
GB.toast(this, it, Toast.LENGTH_LONG, GB.ERROR)
}
}
viewModel.isDashboardLoading.observe(this) { isLoading ->
(itemAdapter as WorkoutSummariesAdapter).isDashboardLoading = isLoading
itemAdapter?.notifyItemChanged(0)
}
viewModel.dashboardStats.observe(this) { stats ->
(itemAdapter as WorkoutSummariesAdapter).dashboardStats = stats
itemAdapter?.notifyItemChanged(0)
}
refresh()
}
private fun stopActionMode() {
actionMode?.finish()
actionMode = null
}
private fun startActionMode() {
val numSelected = selectedItems!!.cardinality()
if (actionMode != null) {
// already in action mode
actionMode?.title = getString(R.string.number_selected_items, numSelected)
return
}
actionMode = startActionMode(object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = getString(R.string.number_selected_items, numSelected)
menuInflater.inflate(R.menu.activity_list_context_menu, menu)
findViewById<View>(R.id.fab).visibility = View.INVISIBLE
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.activity_action_delete -> {
val toDelete = ArrayList<BaseActivitySummary>()
for (i in 0 until selectedItems!!.length()) {
if (selectedItems!!.get(i)) {
itemAdapter?.getItem(i)?.let { toDelete.add(it) }
}
}
MaterialAlertDialogBuilder(this@WorkoutListActivity)
.setTitle(getString(R.string.sports_activity_confirm_delete_title, toDelete.size))
.setMessage(getString(R.string.sports_activity_confirm_delete_description, toDelete.size))
.setIcon(R.drawable.ic_delete_forever)
.setPositiveButton(android.R.string.yes) { _, _ -> deleteItems(toDelete) }
.setNegativeButton(android.R.string.no, null)
.show()
return true
}
R.id.activity_action_export -> {
val paths = ArrayList<String>()
for (i in 0 until selectedItems!!.length()) {
if (selectedItems!!.get(i)) {
itemAdapter?.getItem(i)?.let { summary ->
ActivitySummaryUtils.getGpxFile(summary)?.let { file ->
paths.add(file.path)
}
}
}
}
shareMultiple(paths)
return true
}
R.id.activity_action_select_all -> {
for (i in 1 until itemAdapter!!.itemCount - 1) {
if (!selectedItems!!.get(i)) {
selectedItems!!.set(i, true)
itemAdapter!!.notifyItemChanged(i)
}
}
mode.title = getString(R.string.number_selected_items, selectedItems!!.cardinality())
return true
}
R.id.activity_action_addto_filter -> {
val toFilter = ArrayList<Long>()
for (i in 0 until selectedItems!!.length()) {
if (selectedItems!!.get(i)) {
itemAdapter?.getItem(i)?.id?.let { id ->
toFilter.add(id)
}
}
}
itemsFilter = toFilter
refresh()
return true
}
}
mode.finish()
actionMode = null
return false
}
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
for (i in 0 until selectedItems!!.length()) {
if (selectedItems!!.get(i)) {
selectedItems!!.set(i, false)
itemAdapter?.notifyItemChanged(i)
}
}
findViewById<View>(R.id.fab).visibility = View.VISIBLE
}
})
}
private fun fillKindMap(): HashMap<String, ActivityKind> {
val newMap = HashMap<String, ActivityKind>(0)
newMap[getString(R.string.activity_summaries_all_activities)] = ActivityKind.UNKNOWN
itemAdapter?.items?.forEach { item ->
val activityName = ActivityKind.fromCode(item.activityKind).getLabel(this)
if (!newMap.containsKey(activityName) && item.activityKind != 0) {
newMap[activityName] = ActivityKind.fromCode(item.activityKind)
}
}
return newMap
}
private fun resetFetchTimestampToChosenDate() {
val currentDate = Calendar.getInstance()
DatePickerDialog(
this,
{ _, year, monthOfYear, dayOfMonth ->
val date = Calendar.getInstance().apply {
set(year, monthOfYear, dayOfMonth)
}
val timestamp = date.timeInMillis - 1000
GBApplication.getDeviceSpecificSharedPrefs(gbDevice!!.address).edit().apply {
remove("lastSportsActivityTimeMillis")
putLong("lastSportsActivityTimeMillis", timestamp)
apply()
}
},
currentDate.get(Calendar.YEAR),
currentDate.get(Calendar.MONTH),
currentDate.get(Calendar.DATE)
).show()
}
override fun onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
super.onDestroy()
}
private fun deleteItems(items: List<BaseActivitySummary>) {
items.forEach { item ->
try {
item.delete()
} catch (e: Exception) {
// pass delete error
}
}
refresh()
}
private fun fetchTrackData() {
gbDevice?.let { device ->
if (device.isInitialized && !device.isBusy) {
swipeLayout.isRefreshing = true
GBApplication.deviceService(device).onFetchRecordedData(RecordedDataTypes.TYPE_GPS_TRACKS)
} else {
swipeLayout.isRefreshing = false
if (!device.isInitialized) {
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR)
}
}
}
}
private fun shareMultiple(paths: List<String>) {
val uris = paths.map { path ->
val file = File(path)
FileProvider.getUriForFile(
this,
"${applicationContext.packageName}.screenshot_provider",
file
)
}
if (uris.isNotEmpty()) {
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "application/gpx+xml"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
}
startActivity(Intent.createChooser(intent, "SHARE"))
} else {
GB.toast(this, "No selected activity contains a GPX track to share", Toast.LENGTH_SHORT, GB.ERROR)
}
}
private fun showActivityDetail(position: Int) {
val intent = WorkoutDetailsActivity.createFilteredListIntent(
this,
gbDevice!!,
position,
activityFilter,
dateFromFilter,
dateToFilter,
nameContainsFilter,
deviceFilter,
itemsFilter
)
startActivityForResult(intent, ACTIVITY_DETAIL)
}
private fun runFilterActivity() {
val filterIntent = Intent(this, ActivitySummariesFilter::class.java).apply {
putExtra("activityKindMap", activityKindMap)
putExtra("itemsFilter", ArrayList(itemsFilter ?: emptyList()))
putExtra("activityFilter", activityFilter)
putExtra("dateFromFilter", dateFromFilter)
putExtra("dateToFilter", dateToFilter)
putExtra("deviceFilter", deviceFilter)
putExtra("initial_deviceFilter", getDeviceId(gbDevice!!))
putExtra("nameContainsFilter", nameContainsFilter)
}
startActivityForResult(filterIntent, ACTIVITY_FILTER)
}
private fun getDeviceId(device: GBDevice): Long {
return try {
GBApplication.acquireDB().use { handler ->
DBHelper.findDevice(device, handler.daoSession)?.id ?: 0L
}
} catch (e: Exception) {
LOG.error("Failed to get device id", e)
0L
}
}
override fun refresh() {
gbDevice?.let { device ->
viewModel.loadSummaries(
device,
activityFilter,
dateFromFilter,
dateToFilter,
nameContainsFilter,
deviceFilter,
itemsFilter
)
}
}
companion object {
private val LOG = LoggerFactory.getLogger(WorkoutListActivity::class.java)
const val ACTIVITY_FILTER = 1
const val ACTIVITY_DETAIL = 11
}
}
@@ -0,0 +1,216 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser
import org.slf4j.LoggerFactory
import java.util.Date
class WorkoutListViewModel : ViewModel() {
private val _summaries = MutableLiveData<List<BaseActivitySummary>>()
val summaries: LiveData<List<BaseActivitySummary>> = _summaries
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
private val _dashboardStats = MutableLiveData<DashboardStats?>()
val dashboardStats: LiveData<DashboardStats?> = _dashboardStats
private val _isDashboardLoading = MutableLiveData<Boolean>()
val isDashboardLoading: LiveData<Boolean> = _isDashboardLoading
fun loadSummaries(
gbDevice: GBDevice,
activityKindFilter: Int,
dateFromFilter: Long,
dateToFilter: Long,
nameContainsFilter: String?,
deviceFilter: Long,
itemsFilter: List<Long>?
) {
_isLoading.value = true
_error.value = null
viewModelScope.launch {
try {
val summaries = withContext(Dispatchers.IO) {
loadSummariesFromDatabase(
gbDevice,
activityKindFilter,
dateFromFilter,
dateToFilter,
nameContainsFilter,
deviceFilter,
itemsFilter
)
}
val allSummaries: MutableList<BaseActivitySummary> = mutableListOf()
val dashboardSummary = BaseActivitySummary()
allSummaries.add(dashboardSummary) // dashboard
allSummaries.addAll(summaries)
allSummaries.add(BaseActivitySummary()) // empty
_summaries.value = allSummaries
loadDashboardStats(gbDevice, summaries)
} catch (e: Exception) {
LOG.error("Error loading summaries", e)
_error.value = "Error loading summaries: ${e.localizedMessage}"
} finally {
_isLoading.value = false
}
}
}
private fun loadDashboardStats(gbDevice: GBDevice, summaries: List<BaseActivitySummary>) {
_isDashboardLoading.value = true
viewModelScope.launch {
try {
val stats = withContext(Dispatchers.Default) {
val parser = gbDevice.deviceCoordinator.getActivitySummaryParser(gbDevice, GBApplication.app())
DashboardStats.from(parser, summaries)
}
_dashboardStats.value = stats
} catch (e: Exception) {
LOG.error("Error loading dashboard stats", e)
_error.value = "Error loading dashboard stats: ${e.localizedMessage}"
} finally {
_isDashboardLoading.value = false
}
}
}
private fun loadSummariesFromDatabase(
gbDevice: GBDevice,
activityKindFilter: Int,
dateFromFilter: Long,
dateToFilter: Long,
nameContainsFilter: String?,
deviceFilter: Long,
itemsFilter: List<Long>?
): List<BaseActivitySummary> {
return GBApplication.acquireDB().use { dbHandler ->
val summaryDao = dbHandler.daoSession.baseActivitySummaryDao
val dbDevice = DBHelper.findDevice(gbDevice, dbHandler.daoSession)
val queryBuilder = summaryDao.queryBuilder()
// Apply device filter
when {
deviceFilter != 0L && deviceFilter != ALL_DEVICES -> {
queryBuilder.where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceFilter))
}
dbDevice != null -> {
queryBuilder.where(BaseActivitySummaryDao.Properties.DeviceId.eq(dbDevice.id))
}
}
queryBuilder.orderDesc(BaseActivitySummaryDao.Properties.StartTime)
if (activityKindFilter != 0) {
queryBuilder.where(BaseActivitySummaryDao.Properties.ActivityKind.eq(activityKindFilter))
}
if (dateFromFilter != 0L) {
queryBuilder.where(BaseActivitySummaryDao.Properties.StartTime.gt(Date(dateFromFilter)))
}
if (dateToFilter != 0L) {
queryBuilder.where(BaseActivitySummaryDao.Properties.EndTime.lt(Date(dateToFilter)))
}
if (!nameContainsFilter.isNullOrEmpty()) {
queryBuilder.where(BaseActivitySummaryDao.Properties.Name.like("%${nameContainsFilter}%"))
}
if (!itemsFilter.isNullOrEmpty()) {
queryBuilder.where(BaseActivitySummaryDao.Properties.Id.`in`(itemsFilter))
}
queryBuilder.list()
}
}
data class DashboardStats(
val durationSum: Long,
val caloriesBurntSum: Double,
val distanceSum: Double,
val activeSecondsSum: Long,
val firstItemDate: Long,
val lastItemDate: Long,
val activityIcon: Int
) {
companion object {
fun from(summaryParser: ActivitySummaryParser, activities: List<BaseActivitySummary>): DashboardStats {
var durationSum = 0L
var caloriesBurntSum = 0.0
var distanceSum = 0.0
var activeSecondsSum = 0L
var firstItemDate = 0L
var lastItemDate = 0L
var activityIcon = 0
var activitySame = true
for (summary in activities) {
if (summary.startTime == null) continue // first item is empty, for dashboard
if (firstItemDate == 0L) firstItemDate = summary.startTime.time
lastItemDate = summary.endTime.time
durationSum += (summary.endTime.time - summary.startTime.time)
if (activityIcon == 0) {
activityIcon = summary.activityKind
} else if (activityIcon != summary.activityKind) {
activitySame = false
}
val activitySummaryJsonSummary = ActivitySummaryJsonSummary(summaryParser, summary)
val summarySubData = activitySummaryJsonSummary.getSummaryData(false)
if (summarySubData != null) {
if (summarySubData.has("caloriesBurnt")) {
caloriesBurntSum += summarySubData.getNumber("caloriesBurnt", 0).toDouble()
}
if (summarySubData.has("distanceMeters")) {
distanceSum += summarySubData.getNumber("distanceMeters", 0).toDouble()
}
if (summarySubData.has("activeSeconds")) {
activeSecondsSum += summarySubData.getNumber("activeSeconds", 0).toLong()
}
}
}
return DashboardStats(
durationSum = durationSum,
caloriesBurntSum = caloriesBurntSum,
distanceSum = distanceSum,
activeSecondsSum = activeSecondsSum,
firstItemDate = firstItemDate,
lastItemDate = lastItemDate,
activityIcon = if (activitySame) activityIcon else 0
)
}
}
}
companion object {
private val LOG = LoggerFactory.getLogger(WorkoutListViewModel::class.java)
const val ALL_DEVICES = 999L
}
}
@@ -1,394 +0,0 @@
/* Copyright (C) 2017-2024 Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti,
José Rebelo, Petr Vaněk
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.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityListItem;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesFilter.ALL_DEVICES;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<BaseActivitySummary> {
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesAdapter.class);
private final GBDevice device;
long dateFromFilter;
long dateToFilter;
long deviceFilter;
String nameContainsFilter;
List<Long> itemsFilter;
private int activityKindFilter;
public ActivitySummariesAdapter(Context context, GBDevice device, int activityKindFilter, long dateFromFilter, long dateToFilter, String nameContainsFilter, long deviceFilter, List<Long> itemsFilter) {
super(context);
this.device = device;
this.activityKindFilter = activityKindFilter;
this.dateFromFilter = dateFromFilter;
this.dateToFilter = dateToFilter;
this.nameContainsFilter = nameContainsFilter;
this.deviceFilter = deviceFilter;
this.itemsFilter = itemsFilter;
loadItems();
}
@Override
public void loadItems() {
try (DBHandler handler = GBApplication.acquireDB()) {
BaseActivitySummaryDao summaryDao = handler.getDaoSession().getBaseActivitySummaryDao();
Device dbDevice = DBHelper.findDevice(device, handler.getDaoSession());
assert dbDevice != null;
QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
if (deviceFilter == ALL_DEVICES) {
qb.orderDesc(BaseActivitySummaryDao.Properties.StartTime);
} else if (deviceFilter != 0) {
qb.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(
deviceFilter)).orderDesc(BaseActivitySummaryDao.Properties.StartTime);
} else {
qb.where(
BaseActivitySummaryDao.Properties.DeviceId.eq(
dbDevice.getId())).orderDesc(BaseActivitySummaryDao.Properties.StartTime);
}
if (activityKindFilter != 0) {
qb.where(
BaseActivitySummaryDao.Properties.ActivityKind.eq(activityKindFilter));
}
if (dateFromFilter != 0) {
qb.where(
BaseActivitySummaryDao.Properties.StartTime.gt(new Date(dateFromFilter)));
}
if (dateToFilter != 0) {
qb.where(
BaseActivitySummaryDao.Properties.EndTime.lt(new Date(dateToFilter)));
}
if (nameContainsFilter != null && !nameContainsFilter.isEmpty()) {
qb.where(
BaseActivitySummaryDao.Properties.Name.like("%" + nameContainsFilter + "%"));
}
if (itemsFilter != null) {
qb.where(
BaseActivitySummaryDao.Properties.Id.in(itemsFilter));
}
List<BaseActivitySummary> allSummaries = new ArrayList<>();
// HACK: Populate json in a dummy summary, so stats load faster
BaseActivitySummary dashboardSummary = new BaseActivitySummary();
final List<BaseActivitySummary> summaries = qb.build().list();
dashboardSummary.setSummaryData(StatsContainer.from(
device.getDeviceCoordinator().getActivitySummaryParser(device, getContext()),
summaries
).toJson());
allSummaries.add(dashboardSummary); // dashboard
allSummaries.addAll(summaries);
allSummaries.add(new BaseActivitySummary()); // empty
setItems(allSummaries, true);
} catch (Exception e) {
GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e);
}
}
@NonNull
@Override
public AbstractActivityListingViewHolder<BaseActivitySummary> onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
return switch (viewType) {
case 0 -> // dashboard
new DashboardViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_summary_dashboard_item, parent, false));
case 2 -> // item
new ActivityItemViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.activity_list_item, parent, false));
default -> super.onCreateViewHolder(parent, viewType);
};
}
public void setActivityKindFilter(int filter) {
this.activityKindFilter = filter;
}
public void setDateFromFilter(long date) {
this.dateFromFilter = date;
}
public void setDateToFilter(long date) {
this.dateToFilter = date;
}
public void setNameContainsFilter(String name) {
this.nameContainsFilter = name;
}
public void setItemsFilter(List<Long> items) {
this.itemsFilter = items;
}
public void setDeviceFilter(long device) {
this.deviceFilter = device;
}
public int getActivityKindFilter() {
return this.activityKindFilter;
}
public static class ActivityItemViewHolder extends AbstractActivityListingViewHolder<BaseActivitySummary> {
final View rootView;
final ActivityListItem activityListItem;
public ActivityItemViewHolder(@NonNull final View itemView) {
super(itemView);
this.rootView = itemView;
this.activityListItem = new ActivityListItem(itemView);
}
@Override
public void fill(final int position, final BaseActivitySummary summary, final boolean selected) {
final boolean hasGps;
if (summary.getGpxTrack() != null) {
hasGps = true;
} else if (summary.getSummaryData() != null && summary.getSummaryData().contains(ActivitySummaryEntries.INTERNAL_HAS_GPS)) {
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summary.getSummaryData());
hasGps = summaryData.getBoolean(INTERNAL_HAS_GPS, false);
} else {
hasGps = false;
}
this.activityListItem.update(
null,
null,
ActivityKind.fromCode(summary.getActivityKind()),
summary.getName(),
-1,
-1,
-1,
-1,
summary.getEndTime().getTime() - summary.getStartTime().getTime(),
hasGps,
summary.getStartTime(),
position % 2 == 1,
selected
);
}
}
public class DashboardViewHolder extends AbstractActivityListingViewHolder<BaseActivitySummary> {
final TextView durationSumView;
final TextView caloriesBurntSumView;
final TextView distanceSumView;
final TextView activeSecondsSumView;
final TextView timeStartView;
final TextView timeEndView;
final TextView activitiesCountView;
final TextView activityKindView;
final ImageView activityIconView;
final ImageView activityIconBigView;
public DashboardViewHolder(@NonNull final View itemView) {
super(itemView);
durationSumView = itemView.findViewById(R.id.summary_dashboard_layout_duration_label);
caloriesBurntSumView = itemView.findViewById(R.id.summary_dashboard_layout_calories_label);
distanceSumView = itemView.findViewById(R.id.summary_dashboard_layout_distance_label);
activeSecondsSumView = itemView.findViewById(R.id.summary_dashboard_layout_active_duration_label);
timeStartView = itemView.findViewById(R.id.summary_dashboard_layout_from_label);
timeEndView = itemView.findViewById(R.id.summary_dashboard_layout_to_label);
activitiesCountView = itemView.findViewById(R.id.summary_dashboard_layout_count_label);
activityKindView = itemView.findViewById(R.id.summary_dashboard_layout_activity_label);
activityIconView = itemView.findViewById(R.id.summary_dashboard_layout_activity_icon);
activityIconBigView = itemView.findViewById(R.id.summary_dashboard_layout_big_activity_icon);
}
@Override
public void fill(final int position, final BaseActivitySummary summary, final boolean selected) {
int activitiesCount = getItemCount() - 2; // remove dashboard and end spacer
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
String summaryData = summary.getSummaryData();
final StatsContainer stats;
if (StringUtils.isNotBlank(summaryData)) {
stats = StatsContainer.fromJson(summaryData);
} else {
stats = StatsContainer.from(
coordinator.getActivitySummaryParser(device, getContext()),
getItems()
);
}
DecimalFormat df = new DecimalFormat("#.##");
durationSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) stats.durationSum, TimeUnit.MILLISECONDS)));
caloriesBurntSumView.setText(String.format("%s %s", (long) stats.caloriesBurntSum, getContext().getString(R.string.calories_unit)));
distanceSumView.setText(String.format("%s %s", df.format(stats.distanceSum / 1000), getContext().getString(R.string.km)));
distanceSumView.setText(FormatUtils.getFormattedDistanceLabel(stats.distanceSum));
activeSecondsSumView.setText(String.format("%s", DateTimeUtils.formatDurationHoursMinutes((long) stats.activeSecondsSum, TimeUnit.SECONDS)));
activitiesCountView.setText(String.valueOf(activitiesCount));
String activityName = getContext().getString(R.string.activity_summaries_all_activities);
if (getActivityKindFilter() != 0) {
ActivityKind activityKind = ActivityKind.fromCode(getActivityKindFilter());
activityName = activityKind.getLabel(getContext());
activityIconView.setImageResource(activityKind.getIcon());
activityIconBigView.setImageResource(activityKind.getIcon());
} else if (stats.activityIcon != 0) {
ActivityKind activityKind = ActivityKind.fromCode(stats.activityIcon);
activityIconView.setImageResource(activityKind.getIcon());
activityIconBigView.setImageResource(activityKind.getIcon());
} else {
activityIconView.setImageResource(R.drawable.ic_activity_unknown_small);
activityIconBigView.setImageResource(R.drawable.ic_activity_unknown_small);
}
activityKindView.setText(activityName);
//start and end are inverted when filer not applied, because items are sorted the other way
timeStartView.setText((dateFromFilter != 0) ? DateTimeUtils.formatDate(new Date(dateFromFilter)) : DateTimeUtils.formatDate(new Date((long) stats.lastItemDate)));
timeEndView.setText((dateToFilter != 0) ? DateTimeUtils.formatDate(new Date(dateToFilter)) : DateTimeUtils.formatDate(new Date((long) stats.firstItemDate)));
}
}
private static class StatsContainer {
private static final Gson GSON = new GsonBuilder().create();
private final double durationSum;
private final double caloriesBurntSum;
private final double distanceSum;
private final double activeSecondsSum;
private final double firstItemDate;
private final double lastItemDate;
private final int activityIcon;
public StatsContainer(final double durationSum,
final double caloriesBurntSum,
final double distanceSum,
final double activeSecondsSum,
final double firstItemDate,
final double lastItemDate,
final int activityIcon) {
this.durationSum = durationSum;
this.caloriesBurntSum = caloriesBurntSum;
this.distanceSum = distanceSum;
this.activeSecondsSum = activeSecondsSum;
this.firstItemDate = firstItemDate;
this.lastItemDate = lastItemDate;
this.activityIcon = activityIcon;
}
private static StatsContainer from(final ActivitySummaryParser summaryParser,
final List<BaseActivitySummary> activities) {
double durationSum = 0;
double caloriesBurntSum = 0;
double distanceSum = 0;
double activeSecondsSum = 0;
double firstItemDate = 0;
double lastItemDate = 0;
int activityIcon = 0;
boolean activitySame = true;
for (BaseActivitySummary summary : activities) {
if (summary.getStartTime() == null) continue; //first item is empty, for dashboard
if (firstItemDate == 0) firstItemDate = summary.getStartTime().getTime();
lastItemDate = summary.getEndTime().getTime();
durationSum += summary.getEndTime().getTime() - summary.getStartTime().getTime();
if (activityIcon == 0) {
activityIcon = summary.getActivityKind();
} else {
if (activityIcon != summary.getActivityKind()) {
activitySame = false;
}
}
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, summary);
ActivitySummaryData summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
if (summarySubdata != null) {
if (summarySubdata.has("caloriesBurnt")) {
caloriesBurntSum += summarySubdata.getNumber("caloriesBurnt", 0).doubleValue();
}
if (summarySubdata.has("distanceMeters")) {
distanceSum += summarySubdata.getNumber("distanceMeters", 0).doubleValue();
}
if (summarySubdata.has("activeSeconds")) {
activeSecondsSum += summarySubdata.getNumber("activeSeconds", 0).doubleValue();
}
}
}
return new StatsContainer(
durationSum,
caloriesBurntSum,
distanceSum,
activeSecondsSum,
firstItemDate,
lastItemDate,
activitySame ? activityIcon : 0
);
}
public static StatsContainer fromJson(final String json) {
return GSON.fromJson(json, StatsContainer.class);
}
public String toJson() {
return GSON.toJson(this);
}
}
}
@@ -101,7 +101,7 @@ import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutListActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.BatteryInfoActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureReminders;
@@ -573,7 +573,7 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
@Override
public void onClick(View v) {
Intent startIntent;
startIntent = new Intent(context, ActivitySummariesActivity.class);
startIntent = new Intent(context, WorkoutListActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
@@ -0,0 +1,186 @@
package nodomain.freeyourgadget.gadgetbridge.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutListViewModel
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind
import nodomain.freeyourgadget.gadgetbridge.model.ActivityListItem
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils
import java.util.Date
import java.util.concurrent.TimeUnit
class WorkoutSummariesAdapter(
context: Context,
private val device: GBDevice,
private var activityKindFilter: Int,
private var dateFromFilter: Long,
private var dateToFilter: Long,
private var nameContainsFilter: String?,
private var deviceFilter: Long,
private var itemsFilter: List<Long>?
) : AbstractActivityListingAdapter<BaseActivitySummary>(context) {
var dashboardStats: WorkoutListViewModel.DashboardStats? = null
var isDashboardLoading: Boolean = false
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AbstractActivityListingViewHolder<BaseActivitySummary> {
return when (viewType) {
0 -> // dashboard
DashboardViewHolder(
LayoutInflater.from(context).inflate(R.layout.activity_summary_dashboard_item, parent, false)
)
2 -> // item
ActivityItemViewHolder(
device,
LayoutInflater.from(context).inflate(R.layout.activity_list_item, parent, false)
)
else -> super.onCreateViewHolder(parent, viewType)
}
}
override fun setActivityKindFilter(filter: Int) {
activityKindFilter = filter
}
override fun setDateFromFilter(date: Long) {
dateFromFilter = date
}
override fun setDateToFilter(date: Long) {
dateToFilter = date
}
override fun setNameContainsFilter(name: String?) {
nameContainsFilter = name
}
override fun setItemsFilter(items: List<Long>?) {
itemsFilter = items
}
override fun setDeviceFilter(device: Long) {
deviceFilter = device
}
fun getActivityKindFilter(): Int = activityKindFilter
class ActivityItemViewHolder(val device: GBDevice, itemView: View) : AbstractActivityListingViewHolder<BaseActivitySummary>(itemView) {
private val activityListItem = ActivityListItem(itemView)
override fun fill(position: Int, summary: BaseActivitySummary, selected: Boolean) {
val parser = device.deviceCoordinator.getActivitySummaryParser(device, itemView.context)
val workout = parser.parseWorkout(summary, false)
val hasGps = when {
workout.summary.gpxTrack != null -> true
workout.summary.summaryData?.contains(ActivitySummaryEntries.INTERNAL_HAS_GPS) == true -> {
workout.data.getBoolean(ActivitySummaryEntries.INTERNAL_HAS_GPS, false)
}
else -> false
}
activityListItem.update(
null,
null,
ActivityKind.fromCode(summary.activityKind),
summary.name,
-1,
-1f,
-1,
-1f,
summary.endTime.time - summary.startTime.time,
hasGps,
summary.startTime,
position % 2 == 1,
selected
)
}
}
inner class DashboardViewHolder(itemView: View) : AbstractActivityListingViewHolder<BaseActivitySummary>(itemView) {
private val durationSumView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_duration_label)
private val caloriesBurntSumView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_calories_label)
private val distanceSumView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_distance_label)
private val activeSecondsSumView: TextView =
itemView.findViewById(R.id.summary_dashboard_layout_active_duration_label)
private val timeStartView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_from_label)
private val timeEndView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_to_label)
private val activitiesCountView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_count_label)
private val activityKindView: TextView = itemView.findViewById(R.id.summary_dashboard_layout_activity_label)
private val activityIconView: ImageView = itemView.findViewById(R.id.summary_dashboard_layout_activity_icon)
private val activityIconBigView: ImageView =
itemView.findViewById(R.id.summary_dashboard_layout_big_activity_icon)
private val loadingSpinner: ProgressBar = itemView.findViewById(R.id.summary_dashboard_layout_loading)
private val contentLayout: View = itemView.findViewById(R.id.summary_dashboard_layout_content)
override fun fill(position: Int, summary: BaseActivitySummary, selected: Boolean) {
loadingSpinner.visibility = if (isDashboardLoading) View.VISIBLE else View.INVISIBLE
contentLayout.visibility = if (!isDashboardLoading) View.VISIBLE else View.INVISIBLE
if (isDashboardLoading || dashboardStats == null) {
return
}
val stats = dashboardStats!!
val activitiesCount = itemCount - 2 // remove dashboard and end spacer
durationSumView.text = DateTimeUtils.formatDurationHoursMinutes(stats.durationSum, TimeUnit.MILLISECONDS)
caloriesBurntSumView.text =
String.format("%s %s", stats.caloriesBurntSum.toLong(), context.getString(R.string.calories_unit))
distanceSumView.text = FormatUtils.getFormattedDistanceLabel(stats.distanceSum)
activeSecondsSumView.text =
DateTimeUtils.formatDurationHoursMinutes(stats.activeSecondsSum, TimeUnit.SECONDS)
activitiesCountView.text = activitiesCount.toString()
activityKindView.text = when {
activityKindFilter != 0 -> {
val activityKind = ActivityKind.fromCode(activityKindFilter)
activityIconView.setImageResource(activityKind.icon)
activityIconBigView.setImageResource(activityKind.icon)
activityKind.getLabel(context)
}
stats.activityIcon != 0 -> {
val activityKind = ActivityKind.fromCode(stats.activityIcon)
activityIconView.setImageResource(activityKind.icon)
activityIconBigView.setImageResource(activityKind.icon)
context.getString(R.string.activity_summaries_all_activities)
}
else -> {
activityIconView.setImageResource(R.drawable.ic_activity_unknown_small)
activityIconBigView.setImageResource(R.drawable.ic_activity_unknown_small)
context.getString(R.string.activity_summaries_all_activities)
}
}
// start and end are inverted when filter not applied, because items are sorted the other way
timeStartView.text = if (dateFromFilter != 0L) {
DateTimeUtils.formatDate(Date(dateFromFilter))
} else {
DateTimeUtils.formatDate(Date(stats.lastItemDate))
}
timeEndView.text = if (dateToFilter != 0L) {
DateTimeUtils.formatDate(Date(dateToFilter))
} else {
DateTimeUtils.formatDate(Date(stats.firstItemDate))
}
}
}
}
@@ -1,476 +1,489 @@
<?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:id="@+id/list_item_parent_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--
android:background="?android:attr/activatedBackgroundIndicator"
-->
<LinearLayout
android:id="@+id/summary_dashboard"
<FrameLayout
android:id="@+id/summary_dashboard_layout_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
android:layout_marginBottom="10dp">
<ProgressBar
android:id="@+id/summary_dashboard_layout_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="0dp"
android:baselineAligned="false"
android:gravity="center"
android:orientation="horizontal">
android:orientation="vertical">
<LinearLayout
android:id="@+id/summary_dashboard_layout_big_activity_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="0dp"
android:layout_weight="1"
android:baselineAligned="false"
android:gravity="center"
android:orientation="vertical">
android:orientation="horizontal">
<ImageView
android:id="@+id/summary_dashboard_layout_big_activity_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_activity_unknown_small" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/summary_dashboard_layout_distance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:layout_marginTop="0dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="center"
android:orientation="vertical">
android:orientation="horizontal">
<ImageView
android:id="@+id/summary_dashboard_layout_distance_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_distance" />
<TextView
android:id="@+id/summary_dashboard_layout_distance_title"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/summary_dashboard_layout_distance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/distance"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
android:orientation="vertical">
<TextView
android:id="@+id/summary_dashboard_layout_distance_label"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/summary_dashboard_layout_distance_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_distance" />
<TextView
android:id="@+id/summary_dashboard_layout_distance_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_distance_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/distance"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_distance_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15000"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_calories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15000"
android:textSize="18sp" />
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="bottom|center"
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_calories_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_calories" />
<TextView
android:id="@+id/summary_dashboard_layout_calories_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_calories_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/caloriesBurnt"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_calories_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="30000"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_calories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="bottom|center"
android:orientation="vertical">
android:orientation="horizontal">
<ImageView
android:id="@+id/summary_dashboard_layout_calories_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_calories" />
<TextView
android:id="@+id/summary_dashboard_layout_calories_title"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/summary_dashboard_layout_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/caloriesBurnt"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<TextView
android:id="@+id/summary_dashboard_layout_calories_label"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/summary_dashboard_layout_duration_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_timer" />
<TextView
android:id="@+id/summary_dashboard_layout_duration_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_duration_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_detail_duration_label"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_duration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="1d 20h 35m"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_active_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="30000"
android:textSize="18sp" />
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
</LinearLayout>
<ImageView
android:id="@+id/summary_dashboard_layout_active_duration_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_timer" />
<TextView
android:id="@+id/summary_dashboard_layout_active_duration_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_active_duration_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_list_summary_active_time"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="bottom|center"
android:orientation="horizontal">
<TextView
android:id="@+id/summary_dashboard_layout_active_duration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="1d 13h 30m"
android:textSize="18sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:id="@+id/summary_dashboard_layout_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_duration_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_timer" />
<TextView
android:id="@+id/summary_dashboard_layout_duration_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_detail_duration_label"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_duration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="1d 20h 35m"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_active_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_active_duration_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_timer" />
<TextView
android:id="@+id/summary_dashboard_layout_active_duration_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_list_summary_active_time"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_active_duration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="1d 13h 30m"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/summary_dashboard_layout_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="center"
android:orientation="vertical">
android:orientation="horizontal">
<ImageView
android:id="@+id/summary_dashboard_layout_activity_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_activity_unknown_small" />
<TextView
android:id="@+id/summary_dashboard_layout_activity_title"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/summary_dashboard_layout_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/Activity"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
android:orientation="vertical">
<TextView
android:id="@+id/summary_dashboard_layout_activity_label"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/summary_dashboard_layout_activity_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_activity_unknown_small" />
<TextView
android:id="@+id/summary_dashboard_layout_activity_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_activity_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/Activity"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_activity_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="All Activities"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="All Activities"
android:textSize="18sp" />
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_count_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_activity_stacked" />
<TextView
android:id="@+id/summary_dashboard_layout_count_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_count_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_list_summary_activities"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_count_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="bottom|center"
android:orientation="horizontal">
<ImageView
android:id="@+id/summary_dashboard_layout_count_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_activity_stacked" />
<TextView
android:id="@+id/summary_dashboard_layout_count_title"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/summary_dashboard_layout_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_list_summary_activities"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<TextView
android:id="@+id/summary_dashboard_layout_count_label"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/summary_dashboard_layout_from_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_date_range" />
<TextView
android:id="@+id/summary_dashboard_layout_from_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_from_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_filter_date_from"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_from_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="January 1, 1970"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="15"
android:textSize="18sp" />
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_to_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_date_range" />
<TextView
android:id="@+id/summary_dashboard_layout_to_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:labelFor="@id/summary_dashboard_layout_to_label"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_filter_date_to"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_to_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="December 1, 2020"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:baselineAligned="false"
android:gravity="bottom|center"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/summary_dashboard_layout_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
</FrameLayout>
<ImageView
android:id="@+id/summary_dashboard_layout_from_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_date_range" />
<TextView
android:id="@+id/summary_dashboard_layout_from_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_filter_date_from"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_from_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="January 1, 1970"
android:textSize="18sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/summary_dashboard_layout_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:layout_weight="1"
android:gravity="center|top"
android:orientation="vertical">
<ImageView
android:id="@+id/summary_dashboard_layout_to_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center"
android:contentDescription="@string/candidate_item_device_image"
app:srcCompat="@drawable/ic_date_range" />
<TextView
android:id="@+id/summary_dashboard_layout_to_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/activity_filter_date_to"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_dashboard_layout_to_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-black"
android:gravity="center"
android:maxLines="1"
android:scrollHorizontally="false"
android:text="December 1, 2020"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_below="@id/summary_dashboard"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/row_separator"
/>
</RelativeLayout>
android:layout_below="@id/summary_dashboard_layout_content"
android:background="?attr/row_separator" />
</RelativeLayout>