mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-02-13 19:05:39 +01:00
Garmin: Add basic fit file viewer
This commit is contained in:
parent
0fe05fe9b8
commit
b49fe1730c
@ -189,6 +189,10 @@
|
||||
android:name=".activities.files.FileManagerActivity"
|
||||
android:label="@string/activity_data_management_directory_content_title"
|
||||
android:parentActivityName=".activities.DataManagementActivity" />
|
||||
<activity
|
||||
android:name=".activities.fit.FitViewerActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/fit_file_viewer" />
|
||||
<activity
|
||||
android:name=".activities.BackupRestoreProgressActivity"
|
||||
android:label="@string/activity_db_management_backup_restore_label"
|
||||
|
@ -30,7 +30,6 @@ import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
@ -75,11 +74,13 @@ import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry;
|
||||
@ -448,6 +449,11 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||
} else if (itemId == R.id.activity_action_share_gpx) {
|
||||
shareGpxTrack(ActivitySummaryDetail.this);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_inspect_file) {
|
||||
final Intent inspectFileIntent = new Intent(ActivitySummaryDetail.this, FitViewerActivity.class);
|
||||
inspectFileIntent.putExtra(FitViewerActivity.EXTRA_PATH, currentItem.getRawDetailsPath());
|
||||
startActivity(inspectFileIntent);
|
||||
return true;
|
||||
} else if (itemId == R.id.activity_action_dev_share_raw_summary) {
|
||||
shareRawSummary(ActivitySummaryDetail.this, currentItem);
|
||||
return true;
|
||||
@ -665,6 +671,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
||||
if (overflowMenu != null) {
|
||||
overflowMenu.findItem(R.id.activity_action_show_gpx).setVisible(hasGpx);
|
||||
overflowMenu.findItem(R.id.activity_action_share_gpx).setVisible(hasGpx);
|
||||
overflowMenu.findItem(R.id.activity_action_dev_inspect_file).setVisible(hasRawDetails && currentItem.getRawDetailsPath().toLowerCase(Locale.ROOT).endsWith(".fit"));
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary);
|
||||
overflowMenu.findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails);
|
||||
final MenuItem devToolsMenu = overflowMenu.findItem(R.id.activity_action_dev_tools);
|
||||
|
@ -40,8 +40,10 @@ import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
@ -94,6 +96,7 @@ public class FileManagerAdapter extends RecyclerView.Adapter<FileManagerAdapter.
|
||||
holder.menu.setOnClickListener(view -> {
|
||||
final PopupMenu menu = new PopupMenu(mContext, holder.menu);
|
||||
menu.inflate(R.menu.file_manager_file);
|
||||
menu.getMenu().findItem(R.id.file_manager_file_menu_view).setVisible(file.getPath().toLowerCase(Locale.ROOT).endsWith(".fit"));
|
||||
menu.setOnMenuItemClickListener(item -> {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.file_manager_file_menu_share) {
|
||||
@ -104,6 +107,12 @@ public class FileManagerAdapter extends RecyclerView.Adapter<FileManagerAdapter.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (itemId == R.id.file_manager_file_menu_view) {
|
||||
final Intent inspectFileIntent = new Intent(mContext, FitViewerActivity.class);
|
||||
inspectFileIntent.putExtra(FitViewerActivity.EXTRA_PATH, file.getPath());
|
||||
mContext.startActivity(inspectFileIntent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
@ -0,0 +1,154 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
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.fit;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
|
||||
public class FitRecordAdapter extends RecyclerView.Adapter<FitRecordAdapter.FitRecordViewHolder> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(FitRecordAdapter.class);
|
||||
|
||||
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
|
||||
|
||||
private final List<RecordData> fitRecords;
|
||||
private final List<RecordData> filteredRecords;
|
||||
private final Set<GlobalFITMessage> filter = new HashSet<>();
|
||||
private final Context mContext;
|
||||
|
||||
public FitRecordAdapter(final Context context, final FitFile fitFile) {
|
||||
mContext = context;
|
||||
fitRecords = new ArrayList<>(fitFile.getRecords());
|
||||
filteredRecords = new ArrayList<>(fitRecords.size());
|
||||
refreshFilter();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FitRecordViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
|
||||
final View view = LayoutInflater.from(mContext).inflate(R.layout.item_fit_record, parent, false);
|
||||
return new FitRecordViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final FitRecordViewHolder holder, int position) {
|
||||
final RecordData record = filteredRecords.get(position);
|
||||
|
||||
holder.title.setText(record.getGlobalFITMessage().name());
|
||||
if (record.getComputedTimestamp() != null) {
|
||||
holder.description.setText(SDF.format(new Date(record.getComputedTimestamp() * 1000L)));
|
||||
} else {
|
||||
holder.description.setText("");
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
final String recordInfo = record.getFieldDataList().stream()
|
||||
.sorted(Comparator.comparingInt(RecordData.FieldData::getNumber))
|
||||
.map(fieldData -> {
|
||||
final String fieldName;
|
||||
if (!StringUtils.isBlank(fieldData.getName())) {
|
||||
fieldName = fieldData.getName();
|
||||
} else {
|
||||
fieldName = "unknown_" + fieldData.getNumber() + fieldData;
|
||||
}
|
||||
Object o = fieldData.decode();
|
||||
final String fieldValueString;
|
||||
if (o == null) {
|
||||
fieldValueString = "null";
|
||||
} else if (o instanceof Object[]) {
|
||||
fieldValueString = "[" + StringUtils.join((Object[]) o, ",") + "]";
|
||||
} else {
|
||||
fieldValueString = o.toString();
|
||||
}
|
||||
return fieldName + " = " + fieldValueString;
|
||||
}).collect(Collectors.joining("\n"));
|
||||
|
||||
new MaterialAlertDialogBuilder(mContext)
|
||||
.setCancelable(true)
|
||||
.setTitle(record.getGlobalFITMessage().name())
|
||||
.setMessage(recordInfo)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
})
|
||||
.setNeutralButton(android.R.string.copy, (dialog, which) -> {
|
||||
final ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
final ClipData clip = ClipData.newPlainText(record.getGlobalFITMessage().name(), recordInfo);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return filteredRecords.size();
|
||||
}
|
||||
|
||||
public void updateFilter(final Set<GlobalFITMessage> filter) {
|
||||
this.filter.clear();
|
||||
this.filter.addAll(filter);
|
||||
refreshFilter();
|
||||
}
|
||||
|
||||
private void refreshFilter() {
|
||||
filteredRecords.clear();
|
||||
if (filter.isEmpty()) {
|
||||
filteredRecords.addAll(fitRecords);
|
||||
} else {
|
||||
filteredRecords.addAll(fitRecords.stream().filter(r -> filter.contains(r.getGlobalFITMessage())).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FitRecordViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView title;
|
||||
final TextView description;
|
||||
|
||||
FitRecordViewHolder(final View itemView) {
|
||||
super(itemView);
|
||||
|
||||
title = itemView.findViewById(R.id.fit_record_title);
|
||||
description = itemView.findViewById(R.id.fit_record_description);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
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.fit;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class FitViewerActivity extends AbstractGBActivity implements MenuProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FitViewerActivity.class);
|
||||
|
||||
public static final String EXTRA_PATH = "path";
|
||||
|
||||
private FitRecordAdapter fitRecordAdapter;
|
||||
private FitFile fitFile;
|
||||
private final Set<GlobalFITMessage> filter = new HashSet<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_fit_viewer);
|
||||
addMenuProvider(this);
|
||||
|
||||
if (!getIntent().hasExtra(EXTRA_PATH)) {
|
||||
GB.toast("Missing path", Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
final RecyclerView fileListView = findViewById(R.id.fitRecordView);
|
||||
fileListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
final File fitPath = new File(Objects.requireNonNull(getIntent().getStringExtra(EXTRA_PATH)));
|
||||
if (!fitPath.isFile() || !fitPath.canRead()) {
|
||||
GB.toast("Unable to read fit file", Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(fitPath.getName());
|
||||
}
|
||||
|
||||
try {
|
||||
fitFile = FitFile.parseIncoming(fitPath);
|
||||
} catch (final IOException e) {
|
||||
GB.toast("Failed to parse fit file", Toast.LENGTH_LONG, GB.ERROR);
|
||||
LOG.error("Failed to parse fit file", e);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
fitRecordAdapter = new FitRecordAdapter(this, fitFile);
|
||||
|
||||
fileListView.setAdapter(fitRecordAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.menu_fit_viewer, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem menuItem) {
|
||||
final int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.fit_viewer_filter) {
|
||||
final GlobalFITMessage[] globals = fitFile.getRecords().stream()
|
||||
.map(RecordData::getGlobalFITMessage)
|
||||
.distinct()
|
||||
.sorted((a, b) -> {
|
||||
if (a.name().startsWith("UNK_") && b.name().startsWith("UNK_")) {
|
||||
return Integer.compare(a.getNumber(), b.getNumber());
|
||||
} else {
|
||||
return a.name().compareToIgnoreCase(b.name());
|
||||
}
|
||||
})
|
||||
.toArray(GlobalFITMessage[]::new);
|
||||
|
||||
final boolean[] checked = new boolean[globals.length];
|
||||
for (int i = 0; i < globals.length; i++) {
|
||||
if (filter.contains(globals[i])) {
|
||||
checked[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
final CharSequence[] mEntries = Arrays.stream(globals)
|
||||
.map(GlobalFITMessage::name)
|
||||
.toArray(CharSequence[]::new);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.filter_mode)
|
||||
.setMultiChoiceItems(mEntries, checked, (dialog, which, isChecked) -> checked[which] = isChecked)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
filter.clear();
|
||||
for (int i = 0; i < globals.length; i++) {
|
||||
if (checked[i]) {
|
||||
filter.add(globals[i]);
|
||||
}
|
||||
}
|
||||
fitRecordAdapter.updateFilter(filter);
|
||||
fitRecordAdapter.notifyDataSetChanged();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
})
|
||||
.setNeutralButton(R.string.reset, (dialog, which) -> {
|
||||
filter.clear();
|
||||
fitRecordAdapter.updateFilter(filter);
|
||||
fitRecordAdapter.notifyDataSetChanged();
|
||||
})
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
@ -79,6 +79,10 @@ public class RecordData {
|
||||
return recordDefinition;
|
||||
}
|
||||
|
||||
public List<FieldData> getFieldDataList() {
|
||||
return fieldDataList;
|
||||
}
|
||||
|
||||
public Long parseDataMessage(final GarminByteBufferReader garminByteBufferReader, final Long currentTimestamp) {
|
||||
garminByteBufferReader.setByteOrder(valueHolder.order());
|
||||
computedTimestamp = currentTimestamp;
|
||||
@ -196,7 +200,7 @@ public class RecordData {
|
||||
return tsb.build();
|
||||
}
|
||||
|
||||
private class FieldData {
|
||||
public class FieldData {
|
||||
private final FieldDefinition fieldDefinition;
|
||||
private final int position;
|
||||
private final int size;
|
||||
@ -209,11 +213,11 @@ public class RecordData {
|
||||
this.baseSize = fieldDefinition.getBaseType().getSize();
|
||||
}
|
||||
|
||||
private String getName() {
|
||||
public String getName() {
|
||||
return fieldDefinition.getName();
|
||||
}
|
||||
|
||||
private int getNumber() {
|
||||
public int getNumber() {
|
||||
return fieldDefinition.getNumber();
|
||||
}
|
||||
|
||||
@ -263,7 +267,7 @@ public class RecordData {
|
||||
}
|
||||
}
|
||||
|
||||
private Object decode() {
|
||||
public Object decode() {
|
||||
goToPosition();
|
||||
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||
final byte[] bytes = new byte[size];
|
||||
|
16
app/src/main/res/layout/activity_fit_viewer.xml
Normal file
16
app/src/main/res/layout/activity_fit_viewer.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<RelativeLayout 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"
|
||||
tools:context=".activities.fit.FitViewerActivity">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/fitRecordView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:divider="@null"
|
||||
android:scrollbarSize="5dp"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</RelativeLayout>
|
37
app/src/main/res/layout/item_fit_record.xml
Normal file
37
app/src/main/res/layout/item_fit_record.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fit_record_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollHorizontally="false"
|
||||
android:text="-"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fit_record_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text=""
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -56,6 +56,11 @@
|
||||
android:title="@string/dev_tools"
|
||||
app:showAsAction="never">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/activity_action_dev_inspect_file"
|
||||
android:title="@string/activity_detail_inspect_file"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/activity_action_dev_share_raw_summary"
|
||||
android:title="@string/activity_detail_share_raw_summary"
|
||||
|
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/file_manager_file_menu_view"
|
||||
android:title="@string/view_file" />
|
||||
<item
|
||||
android:id="@+id/file_manager_file_menu_share"
|
||||
android:title="@string/share" />
|
||||
|
11
app/src/main/res/menu/menu_fit_viewer.xml
Normal file
11
app/src/main/res/menu/menu_fit_viewer.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<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="nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity">
|
||||
<item
|
||||
android:id="@+id/fit_viewer_filter"
|
||||
android:icon="@drawable/ic_filter_alt"
|
||||
android:title="@string/filter_mode"
|
||||
app:iconTint="?attr/actionmenu_icon_color"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
@ -1669,6 +1669,7 @@
|
||||
<string name="activity_error_share_failed">Sharing file failed.</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="view_file">View</string>
|
||||
<string name="share_screenshot">Share screenshot</string>
|
||||
<string name="screenshot_taken">Screenshot taken</string>
|
||||
<string name="reset_index">Reset fetch date</string>
|
||||
@ -2371,6 +2372,7 @@
|
||||
<string name="activity_detail_duration_label">Duration</string>
|
||||
<string name="activity_detail_show_gps_label">Show GPS Track</string>
|
||||
<string name="activity_detail_share_gps_label">Share GPS Track</string>
|
||||
<string name="activity_detail_inspect_file">Inspect file</string>
|
||||
<string name="activity_detail_share_raw_summary">Share Raw Summary</string>
|
||||
<string name="activity_detail_share_raw_details">Share Raw Details</string>
|
||||
<string name="activity_detail_share_json_details">Share JSON Details</string>
|
||||
@ -3096,6 +3098,8 @@
|
||||
<string name="changelog_full_title">Changelog</string>
|
||||
<string name="changelog_show_full">More…</string>
|
||||
<string name="changelog_ok_button">OK</string>
|
||||
<string name="fit_file_viewer">FIT File Viewer</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="changelog_title">What\'s New</string>
|
||||
<string name="loyalty_cards_catima_package">Catima package name</string>
|
||||
<string name="loyalty_cards_install">Install Catima</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user