mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Introduce automatic zip export
This commit is contained in:
@@ -69,6 +69,7 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
|
||||
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
|
||||
@@ -181,6 +182,18 @@
|
||||
android:name=".activities.maps.MapsSettingsActivity"
|
||||
android:label="@string/maps_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.automations.AutomationsSettingsActivity"
|
||||
android:label="@string/pref_header_automations"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.automations.AutoExportDbSettingsActivity"
|
||||
android:label="@string/pref_header_auto_export_db"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.automations.AutoExportZipSettingsActivity"
|
||||
android:label="@string/pref_header_auto_export_zip"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.maps.MapsTrackActivity"
|
||||
android:label="@string/maps"
|
||||
@@ -516,6 +529,10 @@
|
||||
android:parentActivityName=".activities.app_specific_notifications.AppSpecificNotificationSettingsActivity"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".externalevents.NotificationListener"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -79,7 +79,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
@@ -103,6 +103,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITBIP;
|
||||
@@ -177,10 +178,6 @@ public class GBApplication extends Application {
|
||||
|
||||
private OpenTracksContentObserver openTracksObserver;
|
||||
|
||||
private long lastAutoExportTimestamp = 0;
|
||||
private long autoExportScheduledTimestamp = 0;
|
||||
|
||||
|
||||
/// flush the log buffer and stop file logging
|
||||
private static final class ShutdownHook implements Runnable {
|
||||
@Override
|
||||
@@ -330,7 +327,8 @@ public class GBApplication extends Application {
|
||||
loadAppsPebbleBlackList();
|
||||
|
||||
if (!GBEnvironment.env().isTest()) {
|
||||
PeriodicExporter.enablePeriodicExport(context);
|
||||
PeriodicDbExporter.INSTANCE.scheduleNextExecution(context);
|
||||
PeriodicZipExporter.INSTANCE.scheduleNextExecution(context);
|
||||
}
|
||||
|
||||
if (isRunningMarshmallowOrLater()) {
|
||||
@@ -2296,22 +2294,6 @@ public class GBApplication extends Application {
|
||||
return openTracksObserver;
|
||||
}
|
||||
|
||||
public long getLastAutoExportTimestamp() {
|
||||
return lastAutoExportTimestamp;
|
||||
}
|
||||
|
||||
public void setLastAutoExportTimestamp(long lastAutoExportTimestamp) {
|
||||
this.lastAutoExportTimestamp = lastAutoExportTimestamp;
|
||||
}
|
||||
|
||||
public long getAutoExportScheduledTimestamp() {
|
||||
return autoExportScheduledTimestamp;
|
||||
}
|
||||
|
||||
public void setAutoExportScheduledTimestamp(long autoExportScheduledTimestamp) {
|
||||
this.autoExportScheduledTimestamp = autoExportScheduledTimestamp;
|
||||
}
|
||||
|
||||
private static class GBActivityLifecycleCallbacks implements ActivityLifecycleCallbacks{
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
+17
-11
@@ -52,12 +52,15 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XDatePreference;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XDatePreferenceFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
|
||||
@@ -266,9 +269,11 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompa
|
||||
*/
|
||||
private class SharedPreferencesChangeHandler implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
LOG.trace("Preference changed: {}", key);
|
||||
|
||||
final Prefs prefs = new Prefs(sharedPreferences);
|
||||
|
||||
if (key == null) {
|
||||
LOG.warn("Preference null, ignoring");
|
||||
return;
|
||||
@@ -281,18 +286,20 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompa
|
||||
return;
|
||||
}
|
||||
|
||||
if (preference instanceof SeekBarPreference) {
|
||||
final SeekBarPreference seekBarPreference = (SeekBarPreference) preference;
|
||||
if (preference instanceof SeekBarPreference seekBarPreference) {
|
||||
seekBarPreference.setValue(prefs.getInt(key, seekBarPreference.getValue()));
|
||||
} else if (preference instanceof SwitchPreferenceCompat) {
|
||||
final SwitchPreferenceCompat switchPreference = (SwitchPreferenceCompat) preference;
|
||||
} else if (preference instanceof SwitchPreferenceCompat switchPreference) {
|
||||
switchPreference.setChecked(prefs.getBoolean(key, switchPreference.isChecked()));
|
||||
} else if (preference instanceof ListPreference) {
|
||||
final ListPreference listPreference = (ListPreference) preference;
|
||||
} else if (preference instanceof ListPreference listPreference) {
|
||||
listPreference.setValue(prefs.getString(key, listPreference.getValue()));
|
||||
} else if (preference instanceof EditTextPreference) {
|
||||
final EditTextPreference editTextPreference = (EditTextPreference) preference;
|
||||
} else if (preference instanceof EditTextPreference editTextPreference) {
|
||||
editTextPreference.setText(prefs.getString(key, editTextPreference.getText()));
|
||||
} else if (preference instanceof XTimePreference xTimePreference) {
|
||||
final LocalTime localTime = prefs.getLocalTime(key, xTimePreference.getPrefValue());
|
||||
xTimePreference.setValue(localTime.getHour(), localTime.getMinute());
|
||||
} else if (preference instanceof XDatePreference xDatePreference) {
|
||||
final LocalDate localDate = prefs.getLocalDate(key, xDatePreference.getPrefValue());
|
||||
xDatePreference.setValue(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth());
|
||||
} else if (preference instanceof PreferenceScreen || Preference.class.equals(preference.getClass())) {
|
||||
LOG.trace("Unknown preference class {} for {}, ignoring", preference.getClass(), key);
|
||||
} else {
|
||||
@@ -303,12 +310,11 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompa
|
||||
final String summary;
|
||||
|
||||
// For multi select preferences, let's set the summary to the values, comma-delimited
|
||||
if (preference instanceof MultiSelectListPreference) {
|
||||
if (preference instanceof MultiSelectListPreference multiSelectListPreference) {
|
||||
final Set<String> prefSetValue = prefs.getStringSet(key, Collections.emptySet());
|
||||
if (prefSetValue.isEmpty()) {
|
||||
summary = requireContext().getString(R.string.not_set);
|
||||
} else {
|
||||
final MultiSelectListPreference multiSelectListPreference = (MultiSelectListPreference) preference;
|
||||
final CharSequence[] entries = multiSelectListPreference.getEntries();
|
||||
final CharSequence[] entryValues = multiSelectListPreference.getEntryValues();
|
||||
final List<String> translatedEntries = new ArrayList<>();
|
||||
|
||||
+61
-53
@@ -86,77 +86,85 @@ public class BackupRestoreProgressActivity extends AbstractGBActivity {
|
||||
final TextView backupRestoreProgressText = binding.backupRestoreProgressText;
|
||||
final TextView backupRestoreProgressPercentage = binding.backupRestoreProgressPercentage;
|
||||
|
||||
final Handler mHandler = new Handler(getMainLooper());
|
||||
|
||||
final ZipBackupCallback zipBackupCallback = new ZipBackupCallback() {
|
||||
@Override
|
||||
public void onProgress(final int progress, final String message) {
|
||||
backupRestoreProgressBar.setIndeterminate(progress == 0);
|
||||
backupRestoreProgressBar.setProgress(progress);
|
||||
backupRestoreProgressText.setText(message);
|
||||
backupRestoreProgressPercentage.setText(getString(R.string.battery_percentage_str, String.valueOf(progress)));
|
||||
mHandler.post(() -> {
|
||||
backupRestoreProgressBar.setIndeterminate(progress == 0);
|
||||
backupRestoreProgressBar.setProgress(progress);
|
||||
backupRestoreProgressText.setText(message);
|
||||
backupRestoreProgressPercentage.setText(getString(R.string.battery_percentage_str, String.valueOf(progress)));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(final String warnings) {
|
||||
jobFinished = true;
|
||||
backupRestoreHint.setVisibility(View.GONE);
|
||||
backupRestoreProgressBar.setProgress(100);
|
||||
backupRestoreProgressPercentage.setText(getString(R.string.battery_percentage_str, "100"));
|
||||
mHandler.post(() -> {
|
||||
jobFinished = true;
|
||||
backupRestoreHint.setVisibility(View.GONE);
|
||||
backupRestoreProgressBar.setProgress(100);
|
||||
backupRestoreProgressPercentage.setText(getString(R.string.battery_percentage_str, "100"));
|
||||
|
||||
switch (action) {
|
||||
case "import":
|
||||
backupRestoreProgressText.setText(R.string.backup_restore_import_complete);
|
||||
switch (action) {
|
||||
case "import":
|
||||
backupRestoreProgressText.setText(R.string.backup_restore_import_complete);
|
||||
|
||||
final StringBuilder message = new StringBuilder();
|
||||
final StringBuilder message = new StringBuilder();
|
||||
|
||||
message.append(getString(R.string.backup_restore_restart_summary, getString(R.string.app_name)));
|
||||
message.append(getString(R.string.backup_restore_restart_summary, getString(R.string.app_name)));
|
||||
|
||||
if (warnings != null) {
|
||||
message.append("\n\n").append(warnings);
|
||||
}
|
||||
if (warnings != null) {
|
||||
message.append("\n\n").append(warnings);
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(BackupRestoreProgressActivity.this)
|
||||
.setCancelable(false)
|
||||
.setIcon(R.drawable.ic_sync)
|
||||
.setTitle(R.string.backup_restore_restart_title)
|
||||
.setMessage(message.toString())
|
||||
.setOnCancelListener((dialog -> {
|
||||
finish();
|
||||
GBApplication.restart();
|
||||
}))
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
finish();
|
||||
GBApplication.restart();
|
||||
}).show();
|
||||
break;
|
||||
case "export":
|
||||
backupRestoreProgressText.setText(R.string.backup_restore_export_complete);
|
||||
break;
|
||||
}
|
||||
new MaterialAlertDialogBuilder(BackupRestoreProgressActivity.this)
|
||||
.setCancelable(false)
|
||||
.setIcon(R.drawable.ic_sync)
|
||||
.setTitle(R.string.backup_restore_restart_title)
|
||||
.setMessage(message.toString())
|
||||
.setOnCancelListener((dialog -> {
|
||||
finish();
|
||||
GBApplication.restart();
|
||||
}))
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
finish();
|
||||
GBApplication.restart();
|
||||
}).show();
|
||||
break;
|
||||
case "export":
|
||||
backupRestoreProgressText.setText(R.string.backup_restore_export_complete);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@Nullable final String errorMessage) {
|
||||
jobFinished = true;
|
||||
mHandler.post(() -> {
|
||||
jobFinished = true;
|
||||
|
||||
switch (action) {
|
||||
case "import":
|
||||
backupRestoreHint.setText(R.string.backup_restore_error_import);
|
||||
break;
|
||||
case "export":
|
||||
backupRestoreHint.setText(R.string.backup_restore_error_export);
|
||||
break;
|
||||
}
|
||||
|
||||
backupRestoreProgressText.setText(getString(R.string.error_message, errorMessage));
|
||||
backupRestoreProgressText.setTypeface(backupRestoreProgressText.getTypeface(), Typeface.BOLD);
|
||||
backupRestoreProgressPercentage.setVisibility(View.GONE);
|
||||
|
||||
if ("export".equals(action)) {
|
||||
final DocumentFile documentFile = DocumentFile.fromSingleUri(BackupRestoreProgressActivity.this, uri);
|
||||
if (documentFile != null) {
|
||||
documentFile.delete();
|
||||
switch (action) {
|
||||
case "import":
|
||||
backupRestoreHint.setText(R.string.backup_restore_error_import);
|
||||
break;
|
||||
case "export":
|
||||
backupRestoreHint.setText(R.string.backup_restore_error_export);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
backupRestoreProgressText.setText(getString(R.string.error_message, errorMessage));
|
||||
backupRestoreProgressText.setTypeface(backupRestoreProgressText.getTypeface(), Typeface.BOLD);
|
||||
backupRestoreProgressPercentage.setVisibility(View.GONE);
|
||||
|
||||
if ("export".equals(action)) {
|
||||
final DocumentFile documentFile = DocumentFile.fromSingleUri(BackupRestoreProgressActivity.this, uri);
|
||||
if (documentFile != null) {
|
||||
documentFile.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-70
@@ -16,7 +16,6 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -53,15 +52,12 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.files.FileManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
|
||||
public class DataManagementActivity extends AbstractGBActivity {
|
||||
@@ -181,69 +177,12 @@ public class DataManagementActivity extends AbstractGBActivity {
|
||||
cleanExportDirectory();
|
||||
}
|
||||
});
|
||||
GBApplication gbApp = GBApplication.app();
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false);
|
||||
int autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
|
||||
//returns an ugly content://...
|
||||
//String autoExportLocation = prefs.getString(GBPrefs.AUTO_EXPORT_LOCATION, "");
|
||||
|
||||
int testExportVisibility = (autoExportInterval > 0 && autoExportEnabled) ? View.VISIBLE : View.GONE;
|
||||
boolean isExportEnabled = autoExportInterval > 0 && autoExportEnabled;
|
||||
TextView autoExportLocation_label = findViewById(R.id.autoExportLocation_label);
|
||||
autoExportLocation_label.setVisibility(testExportVisibility);
|
||||
|
||||
TextView autoExportLocation_path = findViewById(R.id.autoExportLocation_path);
|
||||
autoExportLocation_path.setVisibility(testExportVisibility);
|
||||
autoExportLocation_path.setText(getAutoExportLocationUserString() + " (" + getAutoExportLocationPreferenceString() + ")" );
|
||||
|
||||
TextView autoExportEnabled_label = findViewById(R.id.autoExportEnabled);
|
||||
if (isExportEnabled) {
|
||||
autoExportEnabled_label.setText(getString(R.string.activity_db_management_autoexport_enabled_yes));
|
||||
} else {
|
||||
autoExportEnabled_label.setText(getString(R.string.activity_db_management_autoexport_enabled_no));
|
||||
}
|
||||
|
||||
TextView autoExportScheduled = findViewById(R.id.autoExportScheduled);
|
||||
autoExportScheduled.setVisibility(testExportVisibility);
|
||||
long setAutoExportScheduledTimestamp = gbApp.getAutoExportScheduledTimestamp();
|
||||
if (setAutoExportScheduledTimestamp > 0) {
|
||||
autoExportScheduled.setText(getString(R.string.activity_db_management_autoexport_scheduled_yes,
|
||||
DateTimeUtils.formatDateTime(new Date(setAutoExportScheduledTimestamp))));
|
||||
} else {
|
||||
autoExportScheduled.setText(getResources().getString(R.string.activity_db_management_autoexport_scheduled_no));
|
||||
}
|
||||
|
||||
TextView autoExport_lastTime_label = findViewById(R.id.autoExport_lastTime_label);
|
||||
long lastAutoExportTimestamp = gbApp.getLastAutoExportTimestamp();
|
||||
|
||||
autoExport_lastTime_label.setVisibility(View.GONE);
|
||||
autoExport_lastTime_label.setText(getString(R.string.autoExport_lastTime_label,
|
||||
DateTimeUtils.formatDateTime(new Date(lastAutoExportTimestamp))));
|
||||
|
||||
if (lastAutoExportTimestamp > 0) {
|
||||
autoExport_lastTime_label.setVisibility(testExportVisibility);
|
||||
autoExport_lastTime_label.setVisibility(testExportVisibility);
|
||||
}
|
||||
|
||||
final Context context = getApplicationContext();
|
||||
Button testExportDBButton = findViewById(R.id.testExportDBButton);
|
||||
testExportDBButton.setVisibility(testExportVisibility);
|
||||
testExportDBButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GB.toast(context,
|
||||
context.getString(R.string.activity_DB_test_export_message),
|
||||
Toast.LENGTH_SHORT, GB.INFO);
|
||||
PeriodicExporter.trigger();
|
||||
}
|
||||
});
|
||||
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
}
|
||||
|
||||
private String getAutoExportLocationPreferenceString() {
|
||||
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null);
|
||||
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_DB_LOCATION, null);
|
||||
if (autoExportLocation == null) {
|
||||
return "";
|
||||
}
|
||||
@@ -277,14 +216,6 @@ public class DataManagementActivity extends AbstractGBActivity {
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getAutoExportLocationUserString() {
|
||||
String location = getAutoExportLocationUri();
|
||||
if (location == "") {
|
||||
return getString(R.string.activity_db_management_autoexport_location);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
private boolean hasOldActivityDatabase() {
|
||||
return new DBHelper(this).existsDB("ActivityDatabase");
|
||||
}
|
||||
|
||||
+13
-116
@@ -25,14 +25,11 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.database.Cursor;
|
||||
import android.location.Criteria;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
@@ -64,17 +61,15 @@ import java.util.Set;
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.automations.AutomationsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherCacheManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
@@ -101,6 +96,8 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
open(NotificationManagementActivity.class, result);
|
||||
} else if (result.getResourceFile() == R.xml.map_settings) {
|
||||
open(MapsSettingsActivity.class, result);
|
||||
} else if (result.getResourceFile() == R.xml.automations_settings) {
|
||||
open(AutomationsSettingsActivity.class, result);
|
||||
} else {
|
||||
super.onSearchResultClicked(result);
|
||||
}
|
||||
@@ -109,7 +106,6 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
public static class SettingsFragment extends AbstractPreferenceFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsActivity.class);
|
||||
|
||||
private static final int EXPORT_LOCATION_FILE_REQUEST_CODE = 4711;
|
||||
private EditText fitnessAppEditText = null;
|
||||
private int fitnessAppSelectionListSpinnerFirstRun = 0;
|
||||
|
||||
@@ -124,12 +120,11 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
index(R.xml.discovery_pairing_preferences, R.string.activity_prefs_discovery_pairing);
|
||||
index(R.xml.notifications_preferences, R.string.pref_header_notifications);
|
||||
index(R.xml.map_settings, R.string.maps_settings);
|
||||
index(R.xml.automations_settings, R.string.pref_header_automations);
|
||||
|
||||
setInputTypeFor("rtl_max_line_length", InputType.TYPE_CLASS_NUMBER);
|
||||
setInputTypeFor("location_latitude", InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
|
||||
setInputTypeFor("location_longitude", InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
|
||||
setInputTypeFor("auto_export_interval", InputType.TYPE_CLASS_NUMBER);
|
||||
setInputTypeFor("auto_fetch_interval_limit", InputType.TYPE_CLASS_NUMBER);
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
Preference pref = findPreference("pref_category_activity_personal");
|
||||
@@ -296,66 +291,6 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pref = findPreference(GBPrefs.AUTO_EXPORT_LOCATION);
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
i.setType("application/x-sqlite3");
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.putExtra(Intent.EXTRA_TITLE, "Gadgetbridge.db");
|
||||
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
String title = requireContext().getApplicationContext().getString(R.string.choose_auto_export_location);
|
||||
startActivityForResult(Intent.createChooser(i, title), EXPORT_LOCATION_FILE_REQUEST_CODE);
|
||||
return true;
|
||||
});
|
||||
pref.setSummary(getAutoExportLocationSummary());
|
||||
}
|
||||
|
||||
pref = findPreference(GBPrefs.AUTO_EXPORT_INTERVAL);
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, autoExportInterval) -> {
|
||||
String summary = String.format(
|
||||
requireContext().getApplicationContext().getString(R.string.pref_summary_auto_export_interval),
|
||||
Integer.valueOf((String) autoExportInterval));
|
||||
preference.setSummary(summary);
|
||||
boolean auto_export_enabled = GBApplication.getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false);
|
||||
PeriodicExporter.scheduleAlarm(requireContext().getApplicationContext(), Integer.valueOf((String) autoExportInterval), auto_export_enabled);
|
||||
return true;
|
||||
});
|
||||
int autoExportInterval = GBApplication.getPrefs().getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
|
||||
String summary = String.format(
|
||||
requireContext().getApplicationContext().getString(R.string.pref_summary_auto_export_interval),
|
||||
autoExportInterval);
|
||||
pref.setSummary(summary);
|
||||
}
|
||||
|
||||
pref = findPreference(GBPrefs.AUTO_EXPORT_ENABLED);
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, autoExportEnabled) -> {
|
||||
int autoExportInterval = GBApplication.getPrefs().getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
|
||||
PeriodicExporter.scheduleAlarm(requireContext().getApplicationContext(), autoExportInterval, (boolean) autoExportEnabled);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("auto_fetch_interval_limit");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, autoFetchInterval) -> {
|
||||
String summary = String.format(
|
||||
requireContext().getApplicationContext().getString(R.string.pref_auto_fetch_limit_fetches_summary),
|
||||
Integer.valueOf((String) autoFetchInterval));
|
||||
preference.setSummary(summary);
|
||||
return true;
|
||||
});
|
||||
|
||||
int autoFetchInterval = GBApplication.getPrefs().getInt("auto_fetch_interval_limit", 0);
|
||||
String summary = String.format(
|
||||
requireContext().getApplicationContext().getString(R.string.pref_auto_fetch_limit_fetches_summary),
|
||||
autoFetchInterval);
|
||||
pref.setSummary(summary);
|
||||
}
|
||||
|
||||
final ListPreference audioPlayer = findPreference("audio_player");
|
||||
if (audioPlayer != null) {
|
||||
// Get all receivers of Media Buttons
|
||||
@@ -406,6 +341,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("pref_screen_automations");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
Intent enableIntent = new Intent(requireContext(), AutomationsSettingsActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("pref_category_sleepasandroid");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
@@ -544,53 +488,6 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
if (requestCode == EXPORT_LOCATION_FILE_REQUEST_CODE && intent != null) {
|
||||
Uri uri = intent.getData();
|
||||
requireContext().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
GBApplication.getPrefs().getPreferences()
|
||||
.edit()
|
||||
.putString(GBPrefs.AUTO_EXPORT_LOCATION, uri.toString())
|
||||
.apply();
|
||||
String summary = getAutoExportLocationSummary();
|
||||
findPreference(GBPrefs.AUTO_EXPORT_LOCATION).setSummary(summary);
|
||||
boolean autoExportEnabled = GBApplication
|
||||
.getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false);
|
||||
int autoExportPeriod = GBApplication
|
||||
.getPrefs().getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
|
||||
PeriodicExporter.scheduleAlarm(requireContext().getApplicationContext(), autoExportPeriod, autoExportEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Either returns the file path of the selected document, or the display name, or an empty string
|
||||
*/
|
||||
public String getAutoExportLocationSummary() {
|
||||
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null);
|
||||
if (autoExportLocation == null) {
|
||||
return "";
|
||||
}
|
||||
Uri uri = Uri.parse(autoExportLocation);
|
||||
try {
|
||||
return AndroidUtils.getFilePath(requireContext().getApplicationContext(), uri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
try (
|
||||
Cursor cursor = requireContext().getContentResolver().query(
|
||||
uri,
|
||||
new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME},
|
||||
null, null, null, null
|
||||
)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
|
||||
}
|
||||
} catch (Exception e2) {
|
||||
LOG.warn("getAutoExportLocationSummary", e2);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* delayed execution so that the preferences are applied first
|
||||
*/
|
||||
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
/* 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.automations
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.InputType
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceGroup
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PeriodicExporter
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class AbstractAutoExportSettingsFragment(
|
||||
val exporter: PeriodicExporter,
|
||||
) : AbstractPreferenceFragment() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.auto_export_settings, rootKey)
|
||||
|
||||
val keyPrefix = exporter.getKeyPrefix()
|
||||
addPrefixToPreferenceKeys(preferenceScreen, keyPrefix)
|
||||
|
||||
val prefKeyEnabled = keyPrefix + GBPrefs.AUTO_EXPORT_ENABLED
|
||||
val prefKeyLocation = keyPrefix + GBPrefs.AUTO_EXPORT_LOCATION
|
||||
val prefKeyInterval = keyPrefix + GBPrefs.AUTO_EXPORT_INTERVAL
|
||||
val prefKeyStartTime = keyPrefix + "auto_export_start_time"
|
||||
val prefKeyRunNow = keyPrefix + "auto_export_run_now"
|
||||
|
||||
val gbPrefs = GBApplication.getPrefs()
|
||||
|
||||
val exportLocationPicker = registerForActivityResult<Intent?, ActivityResult?>(
|
||||
StartActivityForResult(),
|
||||
ActivityResultCallback { result: ActivityResult? ->
|
||||
if (result!!.resultCode != Activity.RESULT_OK) {
|
||||
return@ActivityResultCallback
|
||||
}
|
||||
val uri = result.data?.data
|
||||
if (uri == null) {
|
||||
LOG.error("Got no uri")
|
||||
return@ActivityResultCallback
|
||||
}
|
||||
requireContext().contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
gbPrefs.preferences.edit {
|
||||
putString(prefKeyLocation, uri.toString())
|
||||
}
|
||||
val summary = resolveLocationSummary(
|
||||
requireContext(),
|
||||
gbPrefs.getString(prefKeyLocation, "")
|
||||
)
|
||||
findPreference<Preference>(prefKeyLocation)?.setSummary(summary)
|
||||
}
|
||||
)
|
||||
|
||||
setInputTypeFor(prefKeyInterval, InputType.TYPE_CLASS_NUMBER)
|
||||
|
||||
val prefExportLocation = findPreference<Preference>(prefKeyLocation)
|
||||
if (prefExportLocation != null) {
|
||||
prefExportLocation.setOnPreferenceClickListener {
|
||||
val i = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
i.setType(exporter.getFileMimeType())
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.putExtra(Intent.EXTRA_TITLE, "Gadgetbridge.${exporter.getFileExtension()}")
|
||||
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
val title: String = requireContext().applicationContext.getString(R.string.choose_auto_export_location)
|
||||
exportLocationPicker.launch(Intent.createChooser(i, title))
|
||||
|
||||
true
|
||||
}
|
||||
prefExportLocation.setSummary(
|
||||
resolveLocationSummary(
|
||||
requireContext(),
|
||||
gbPrefs.getString(prefKeyLocation, "")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val prefExportInterval = findPreference<Preference>(prefKeyInterval)
|
||||
if (prefExportInterval != null) {
|
||||
prefExportInterval.setOnPreferenceChangeListener { preference: Preference?, autoExportInterval: Any? ->
|
||||
val summary = String.format(
|
||||
requireContext().applicationContext.getString(R.string.pref_summary_auto_export_interval),
|
||||
(autoExportInterval as String?)!!.toInt()
|
||||
)
|
||||
prefExportInterval.setSummary(summary)
|
||||
scheduleNextExecutionDelayed()
|
||||
true
|
||||
}
|
||||
val autoExportInterval = gbPrefs.getInt(prefKeyInterval, 0)
|
||||
val summary = String.format(
|
||||
requireContext().applicationContext.getString(R.string.pref_summary_auto_export_interval),
|
||||
autoExportInterval
|
||||
)
|
||||
prefExportInterval.setSummary(summary)
|
||||
}
|
||||
|
||||
val prefExportEnabled = findPreference<Preference>(prefKeyEnabled)
|
||||
prefExportEnabled?.setOnPreferenceChangeListener { preference: Preference?, autoExportEnabled: Any? ->
|
||||
scheduleNextExecutionDelayed()
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(prefKeyRunNow)?.setOnPreferenceClickListener {
|
||||
exporter.executeNow()
|
||||
true
|
||||
}
|
||||
|
||||
val prefStartTime = findPreference<Preference>(prefKeyStartTime)
|
||||
prefStartTime?.setOnPreferenceChangeListener { preference: Preference?, startFrom: Any? ->
|
||||
scheduleNextExecutionDelayed()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scheduled execution after a slight delay to allow for preferences to be persisted.
|
||||
*/
|
||||
private fun scheduleNextExecutionDelayed() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(200)
|
||||
exporter.scheduleNextExecution(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val workManager = WorkManager.getInstance(requireContext())
|
||||
|
||||
val gbPrefs = GBApplication.getPrefs()
|
||||
val prefKeyStatus = exporter.getKeyPrefix() + "auto_export_status"
|
||||
val prefKeyLastExecution = exporter.getKeyPrefix() + GBPrefs.AUTO_EXPORT_LAST_EXECUTION
|
||||
val prefKeyNextExecution = exporter.getKeyPrefix() + GBPrefs.AUTO_EXPORT_NEXT_EXECUTION
|
||||
val prefStatus = findPreference<Preference>(prefKeyStatus)
|
||||
val prefLastExecution = findPreference<Preference>(prefKeyLastExecution)
|
||||
val prefNextExecution = findPreference<Preference>(prefKeyNextExecution)
|
||||
workManager.getWorkInfosByTagLiveData(exporter.getWorkTag()).observe(viewLifecycleOwner) { workInfos ->
|
||||
LOG.debug("Got update for {} workInfos", workInfos.size)
|
||||
|
||||
val sortedWorkInfos = workInfos.sortedBy { workInfo ->
|
||||
workInfo.tags
|
||||
.find { it.startsWith(PeriodicExporter.TAG_CREATED_AT) }
|
||||
?.removePrefix(PeriodicExporter.TAG_CREATED_AT)
|
||||
?.toLongOrNull()
|
||||
?: 0L
|
||||
}
|
||||
|
||||
val lastExecution = sortedWorkInfos.findLast { workInfo -> workInfo.state != WorkInfo.State.ENQUEUED }
|
||||
val nextExecution = sortedWorkInfos.findLast { workInfo -> workInfo.state == WorkInfo.State.ENQUEUED }
|
||||
|
||||
if (lastExecution != null) {
|
||||
prefStatus?.summary = when (lastExecution.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
val progress = lastExecution.progress.getInt("progress", -1)
|
||||
if (progress >= 0) {
|
||||
requireContext().getString(
|
||||
R.string.work_info_running_percentage,
|
||||
getString(R.string.work_info_status_running),
|
||||
progress
|
||||
)
|
||||
} else {
|
||||
getString(R.string.work_info_status_running)
|
||||
}
|
||||
}
|
||||
|
||||
WorkInfo.State.ENQUEUED -> requireContext().getString(R.string.work_info_status_enqueued)
|
||||
WorkInfo.State.SUCCEEDED -> requireContext().getString(R.string.work_info_status_succeeded)
|
||||
WorkInfo.State.FAILED -> requireContext().getString(R.string.work_info_status_failed)
|
||||
WorkInfo.State.BLOCKED -> requireContext().getString(R.string.work_info_status_blocked)
|
||||
WorkInfo.State.CANCELLED -> requireContext().getString(R.string.work_info_status_cancelled)
|
||||
}
|
||||
} else {
|
||||
prefStatus?.summary = requireContext().getString(R.string.unknown)
|
||||
}
|
||||
|
||||
// We need to persist and fetch the timestamp from preferences, since the WorkInfo will not contain it
|
||||
val lastAutoExportTimestamp: Long = gbPrefs.getLong(prefKeyLastExecution, 0)
|
||||
if (lastAutoExportTimestamp > 0) {
|
||||
prefLastExecution?.summary = formatDateWithDiff(Date(lastAutoExportTimestamp))
|
||||
} else {
|
||||
prefLastExecution?.summary = getString(R.string.unknown)
|
||||
}
|
||||
|
||||
if (nextExecution != null) {
|
||||
prefNextExecution?.summary = formatDateWithDiff(Date(nextExecution.nextScheduleTimeMillis))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a prefix to the key and dependency of all preferences in a group.
|
||||
*/
|
||||
private fun addPrefixToPreferenceKeys(prefGroup: PreferenceGroup, prefix: String) {
|
||||
for (i in 0 until prefGroup.preferenceCount) {
|
||||
val pref = prefGroup.getPreference(i)
|
||||
|
||||
if (pref is PreferenceGroup) {
|
||||
addPrefixToPreferenceKeys(pref, prefix)
|
||||
}
|
||||
|
||||
if (!pref.key.isNullOrBlank() && !pref.key.startsWith(prefix)) {
|
||||
pref.key = prefix + pref.key
|
||||
}
|
||||
if (!pref.dependency.isNullOrBlank() && !pref.dependency?.startsWith(prefix)!!) {
|
||||
pref.dependency = prefix + pref.dependency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date, including the difference to the current time.
|
||||
*/
|
||||
private fun formatDateWithDiff(date: Date): String {
|
||||
val diffMillis = System.currentTimeMillis() - date.time
|
||||
return if (diffMillis > 0) {
|
||||
requireContext().getString(
|
||||
R.string.datetime_in_the_past,
|
||||
DateTimeUtils.formatDateTime(date),
|
||||
DateTimeUtils.formatDurationHoursMinutes(diffMillis, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
} else {
|
||||
requireContext().getString(
|
||||
R.string.datetime_in_the_future,
|
||||
DateTimeUtils.formatDateTime(date),
|
||||
DateTimeUtils.formatDurationHoursMinutes(-diffMillis, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG: Logger = LoggerFactory.getLogger(AbstractAutoExportSettingsFragment::class.java)
|
||||
|
||||
/**
|
||||
* Either returns the file path of the selected document, or the display name, or an error string
|
||||
*/
|
||||
fun resolveLocationSummary(context: Context, uriString: String): String {
|
||||
if (uriString == "") {
|
||||
return ""
|
||||
}
|
||||
val uri = uriString.toUri()
|
||||
try {
|
||||
return AndroidUtils.getFilePath(context.applicationContext, uri)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
LOG.warn("getAutoExportLocationSummary 1", e)
|
||||
try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME),
|
||||
null, null, null, null
|
||||
).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
LOG.warn("getAutoExportLocationSummary 2", e2)
|
||||
}
|
||||
}
|
||||
return context.getString(R.string.activity_db_management_autoexport_location)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/* 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.automations
|
||||
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter
|
||||
|
||||
class AutoExportDbSettingsActivity : AbstractSettingsActivityV2() {
|
||||
override fun newFragment(): PreferenceFragmentCompat {
|
||||
return AutoExportDbSettingsFragment()
|
||||
}
|
||||
|
||||
companion object {
|
||||
class AutoExportDbSettingsFragment : AbstractAutoExportSettingsFragment(PeriodicDbExporter)
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/* 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.automations
|
||||
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter
|
||||
|
||||
class AutoExportZipSettingsActivity : AbstractSettingsActivityV2() {
|
||||
override fun newFragment(): PreferenceFragmentCompat {
|
||||
return AutoExportZipSettingsFragment()
|
||||
}
|
||||
|
||||
companion object {
|
||||
class AutoExportZipSettingsFragment : AbstractAutoExportSettingsFragment(PeriodicZipExporter)
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/* 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.automations
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
|
||||
|
||||
class AutomationsSettingsActivity : AbstractSettingsActivityV2() {
|
||||
override fun newFragment(): PreferenceFragmentCompat {
|
||||
return AutomationsSettingsFragment()
|
||||
}
|
||||
|
||||
companion object {
|
||||
class AutomationsSettingsFragment : AbstractPreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.automations_settings, rootKey)
|
||||
|
||||
setInputTypeFor(GBPrefs.PREF_AUTO_FETCH_INTERVAL_LIMIT, InputType.TYPE_CLASS_NUMBER)
|
||||
|
||||
val prefAutoFetchInterval = findPreference<Preference>(GBPrefs.PREF_AUTO_FETCH_INTERVAL_LIMIT)
|
||||
if (prefAutoFetchInterval != null) {
|
||||
prefAutoFetchInterval.setOnPreferenceChangeListener { preference: Preference?, autoFetchInterval: Any? ->
|
||||
val summary = String.format(
|
||||
requireContext().applicationContext.getString(R.string.pref_auto_fetch_limit_fetches_summary),
|
||||
(autoFetchInterval as String?)!!.toInt()
|
||||
)
|
||||
preference!!.setSummary(summary)
|
||||
true
|
||||
}
|
||||
|
||||
val autoFetchInterval = GBApplication.getPrefs().getInt(GBPrefs.PREF_AUTO_FETCH_INTERVAL_LIMIT, 0)
|
||||
val summary = String.format(
|
||||
requireContext().applicationContext.getString(R.string.pref_auto_fetch_limit_fetches_summary),
|
||||
autoFetchInterval
|
||||
)
|
||||
prefAutoFetchInterval.setSummary(summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.edit
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
@@ -17,12 +18,21 @@ class DatabaseExportWorker(
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(mContext, workerParams) {
|
||||
override fun doWork(): Result {
|
||||
val dst = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null)
|
||||
val enabled = GBApplication.getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_DB_ENABLED, false)
|
||||
if (!enabled) {
|
||||
LOG.warn("DB export started, but is disabled")
|
||||
// Should not need i18n, this should never happen
|
||||
GB.updateExportFailedNotification("DB export started, but is disabled", mContext)
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val dst = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_DB_LOCATION, "")
|
||||
|
||||
LOG.info("Starting DB export, dst={}", dst)
|
||||
|
||||
if (dst == null) {
|
||||
LOG.warn("Unable to export DB, export location not set")
|
||||
GB.updateExportFailedNotification(mContext.getString(R.string.notif_export_location_not_set), mContext)
|
||||
broadcastSuccess(false)
|
||||
return Result.failure()
|
||||
}
|
||||
@@ -35,7 +45,9 @@ class DatabaseExportWorker(
|
||||
}
|
||||
}
|
||||
|
||||
GBApplication.app().lastAutoExportTimestamp = System.currentTimeMillis()
|
||||
GBApplication.getPrefs().preferences.edit {
|
||||
putLong(GBPrefs.AUTO_EXPORT_DB_LAST_EXECUTION, System.currentTimeMillis())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
GB.updateExportFailedNotification(mContext.getString(R.string.notif_export_failed_title), mContext)
|
||||
LOG.error("Exception while exporting DB", e)
|
||||
@@ -51,7 +63,7 @@ class DatabaseExportWorker(
|
||||
}
|
||||
|
||||
private fun broadcastSuccess(success: Boolean) {
|
||||
if (!GBApplication.getPrefs().getBoolean("intent_api_broadcast_export", false)) {
|
||||
if (!GBApplication.getPrefs().getBoolean(GBPrefs.INTENT_API_BROADCAST_EXPORT_DB, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Copyright (C) 2018-2024 Carsten Pfeiffer, Felix Konstantin Maurer,
|
||||
Ganblejs, 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.database
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PeriodicExporter
|
||||
|
||||
object PeriodicDbExporter: PeriodicExporter() {
|
||||
override fun getWorkerClass(): Class<out ListenableWorker> {
|
||||
return DatabaseExportWorker::class.java
|
||||
}
|
||||
|
||||
/**
|
||||
* DB export has no prefix for backwards compatibility
|
||||
*/
|
||||
override fun getKeyPrefix(): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getFileMimeType(): String {
|
||||
return "application/x-sqlite3"
|
||||
}
|
||||
|
||||
override fun getFileExtension(): String {
|
||||
return "db"
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/* Copyright (C) 2018-2024 Carsten Pfeiffer, Felix Konstantin Maurer,
|
||||
Ganblejs, 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.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object PeriodicExporter {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(PeriodicExporter::class.java)
|
||||
|
||||
private const val TAG = "exporter_db"
|
||||
|
||||
@JvmStatic
|
||||
fun enablePeriodicExport(context: Context) {
|
||||
val prefs: Prefs = GBApplication.getPrefs()
|
||||
val autoExportScheduled = GBApplication.app().autoExportScheduledTimestamp
|
||||
val autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false)
|
||||
val autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0)
|
||||
scheduleAlarm(context, autoExportInterval, autoExportEnabled && autoExportScheduled == 0L)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun scheduleAlarm(
|
||||
context: Context,
|
||||
autoExportInterval: Int,
|
||||
autoExportEnabled: Boolean
|
||||
) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
workManager.cancelAllWorkByTag(TAG)
|
||||
|
||||
if (!autoExportEnabled) {
|
||||
LOG.info("Not scheduling periodic export, either already scheduled or not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
if (autoExportInterval == 0) {
|
||||
LOG.info("Not scheduling periodic export, interval set to 0")
|
||||
return
|
||||
}
|
||||
|
||||
LOG.info("Scheduling periodic export for {}h in the future", autoExportInterval)
|
||||
|
||||
val exportPeriodMillis = autoExportInterval * 60 * 60 * 1000
|
||||
GBApplication.app().autoExportScheduledTimestamp = System.currentTimeMillis() + exportPeriodMillis
|
||||
|
||||
val exportRequest = PeriodicWorkRequestBuilder<DatabaseExportWorker>(
|
||||
autoExportInterval.toLong(),
|
||||
TimeUnit.HOURS
|
||||
).addTag(TAG).build()
|
||||
|
||||
workManager.enqueue(exportRequest)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun trigger() {
|
||||
val workManager = WorkManager.getInstance(GBApplication.getContext())
|
||||
val exportRequest = OneTimeWorkRequest.Builder(DatabaseExportWorker::class.java)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager.enqueue(exportRequest)
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/* Copyright (C) 2017-2024 Carsten Pfeiffer, Daniele Gobbetti, Felix
|
||||
Konstantin Maurer, 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.externalevents;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||
|
||||
public class AutoStartReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = AutoStartReceiver.class.getName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
if (GBApplication.getPrefs().getAutoStart() &&
|
||||
(Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())
|
||||
)) {
|
||||
Log.i(TAG, "Boot or reinstall completed, starting Gadgetbridge");
|
||||
if (GBApplication.getPrefs().getBoolean("general_autoconnectonbluetooth", false)) {
|
||||
Log.i(TAG, "Autoconnect is enabled, attempting to connect");
|
||||
GBApplication.deviceService().connect();
|
||||
}
|
||||
Log.i(TAG, "Going to enable periodic exporter");
|
||||
PeriodicExporter.enablePeriodicExport(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/* Copyright (C) 2017-2024 Carsten Pfeiffer, Daniele Gobbetti, Felix
|
||||
Konstantin Maurer, 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.externalevents
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter
|
||||
|
||||
class AutoStartReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (!GBApplication.getPrefs().autoStart) {
|
||||
return
|
||||
}
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action && Intent.ACTION_MY_PACKAGE_REPLACED != intent.action) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Boot or reinstall completed, starting Gadgetbridge")
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean(GBPrefs.AUTO_CONNECT_BLUETOOTH, false)) {
|
||||
Log.i(TAG, "Auto-connect is enabled, attempting to connect")
|
||||
GBApplication.deviceService().connect()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Going to enable periodic db exporter")
|
||||
PeriodicDbExporter.scheduleNextExecution(context)
|
||||
|
||||
Log.i(TAG, "Going to enable periodic zip exporter")
|
||||
PeriodicZipExporter.scheduleNextExecution(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = AutoStartReceiver::class.java.getName()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,7 +65,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prefs.getBoolean("general_autoconnectonbluetooth", false)) {
|
||||
if (!prefs.getBoolean(GBPrefs.AUTO_CONNECT_BLUETOOTH, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+25
-5
@@ -36,7 +36,7 @@ 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.database.PeriodicExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicDbExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter;
|
||||
|
||||
public class IntentApiReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IntentApiReceiver.class);
|
||||
@@ -52,7 +53,10 @@ public class IntentApiReceiver extends BroadcastReceiver {
|
||||
private static final String msgDebugNotAllowed = "Intent API Allow Debug Commands not allowed";
|
||||
|
||||
public static final String COMMAND_ACTIVITY_SYNC = "nodomain.freeyourgadget.gadgetbridge.command.ACTIVITY_SYNC";
|
||||
@Deprecated
|
||||
public static final String COMMAND_TRIGGER_EXPORT = "nodomain.freeyourgadget.gadgetbridge.command.TRIGGER_EXPORT";
|
||||
public static final String TRIGGER_DATABASE_EXPORT = "nodomain.freeyourgadget.gadgetbridge.command.TRIGGER_DATABASE_EXPORT";
|
||||
public static final String COMMAND_TRIGGER_ZIP_EXPORT = "nodomain.freeyourgadget.gadgetbridge.command.TRIGGER_ZIP_EXPORT";
|
||||
public static final String COMMAND_DEBUG_SEND_NOTIFICATION = "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_SEND_NOTIFICATION";
|
||||
public static final String COMMAND_DEBUG_INCOMING_CALL = "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_INCOMING_CALL";
|
||||
public static final String COMMAND_DEBUG_END_CALL = "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_END_CALL";
|
||||
@@ -97,15 +101,31 @@ public class IntentApiReceiver extends BroadcastReceiver {
|
||||
break;
|
||||
|
||||
case COMMAND_TRIGGER_EXPORT:
|
||||
LOG.warn(
|
||||
"The action {} is deprecated, please use {}",
|
||||
COMMAND_TRIGGER_EXPORT,
|
||||
TRIGGER_DATABASE_EXPORT
|
||||
);
|
||||
case TRIGGER_DATABASE_EXPORT:
|
||||
if (!prefs.getBoolean("intent_api_allow_trigger_export", false)) {
|
||||
LOG.warn("Intent API export trigger not allowed");
|
||||
LOG.warn("Intent API db export trigger not allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Triggering export");
|
||||
LOG.info("Triggering db export");
|
||||
|
||||
final Intent exportIntent = new Intent(context, PeriodicExporter.class);
|
||||
context.sendBroadcast(exportIntent);
|
||||
PeriodicDbExporter.INSTANCE.executeNow();
|
||||
break;
|
||||
|
||||
case COMMAND_TRIGGER_ZIP_EXPORT:
|
||||
if (!prefs.getBoolean("intent_api_allow_trigger_zip_export", false)) {
|
||||
LOG.warn("Intent API zip export trigger not allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Triggering zip export");
|
||||
|
||||
PeriodicZipExporter.INSTANCE.executeNow();
|
||||
break;
|
||||
|
||||
case COMMAND_DEBUG_SEND_NOTIFICATION:
|
||||
|
||||
+1
-2
@@ -1468,8 +1468,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
}
|
||||
}
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("auto_fetch_enabled", false) &&
|
||||
features.supportsActivityDataFetching() && mGBAutoFetchReceiver == null) {
|
||||
if (features.supportsActivityDataFetching() && mGBAutoFetchReceiver == null) {
|
||||
mGBAutoFetchReceiver = new GBAutoFetchReceiver();
|
||||
ContextCompat.registerReceiver(this, mGBAutoFetchReceiver, new IntentFilter("android.intent.action.USER_PRESENT"), ContextCompat.RECEIVER_EXPORTED);
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2018-2024 Daniele Gobbetti, José Rebelo, Martin
|
||||
/* Copyright (C) 2018-2025 Daniele Gobbetti, José Rebelo, Martin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.Date;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
|
||||
|
||||
public class GBAutoFetchReceiver extends BroadcastReceiver {
|
||||
@@ -37,6 +38,9 @@ public class GBAutoFetchReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (!GBApplication.getPrefs().getBoolean(GBPrefs.PREF_AUTO_FETCH_ENABLED, false)) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
final Date now = new Date();
|
||||
final long timeSinceLast = now.getTime() - lastSync.getTime();
|
||||
|
||||
@@ -256,11 +256,12 @@ public class AndroidUtils {
|
||||
String[] projection = {
|
||||
MediaStore.Images.Media.DATA
|
||||
};
|
||||
Cursor cursor = context.getContentResolver()
|
||||
.query(uri, projection, selection, selectionArgs, null);
|
||||
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(column_index);
|
||||
try (Cursor cursor = context.getContentResolver()
|
||||
.query(uri, projection, selection, selectionArgs, null)) {
|
||||
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
}
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
|
||||
@@ -76,6 +76,7 @@ public class GB {
|
||||
public static final String NOTIFICATION_CHANNEL_ID_SCAN_SERVICE = "gadgetbridge_scan_service";
|
||||
public static final String NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID = "gadgetbridge_high_priority";
|
||||
public static final String NOTIFICATION_CHANNEL_ID_TRANSFER = "gadgetbridge transfer";
|
||||
public static final String NOTIFICATION_CHANNEL_ID_EXPORT = "gadgetbridge export";
|
||||
public static final String NOTIFICATION_CHANNEL_ID_LOW_BATTERY = "low_battery";
|
||||
public static final String NOTIFICATION_CHANNEL_ID_FULL_BATTERY = "full_battery";
|
||||
public static final String NOTIFICATION_CHANNEL_ID_GPS = "gps";
|
||||
@@ -147,6 +148,12 @@ public class GB {
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channelTransfer);
|
||||
|
||||
NotificationChannel channelExport = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID_EXPORT,
|
||||
context.getString(R.string.pref_header_auto_export),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channelExport);
|
||||
|
||||
NotificationChannel channelLowBattery = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID_LOW_BATTERY,
|
||||
context.getString(R.string.notification_channel_low_battery_name),
|
||||
|
||||
@@ -48,16 +48,42 @@ public class GBPrefs extends Prefs {
|
||||
public static final String DEVICE_AUTO_RECONNECT = "prefs_key_device_auto_reconnect";
|
||||
public static final String DEVICE_CONNECT_BACK = "prefs_key_device_reconnect_on_acl";
|
||||
private static final String AUTO_START = "general_autostartonboot";
|
||||
public static final String AUTO_EXPORT_ENABLED = "auto_export_enabled";
|
||||
public static final String AUTO_EXPORT_LOCATION = "auto_export_location";
|
||||
public static final String AUTO_CONNECT_BLUETOOTH = "general_autoconnectonbluetooth";
|
||||
public static final String PING_TONE = "ping_tone";
|
||||
public static final String AUTO_EXPORT_INTERVAL = "auto_export_interval";
|
||||
private static final boolean AUTO_START_DEFAULT = true;
|
||||
public static final String RTL_SUPPORT = "rtl";
|
||||
public static final String RTL_CONTEXTUAL_ARABIC = "contextualArabic";
|
||||
public static boolean AUTO_RECONNECT_DEFAULT = true;
|
||||
public static final String PREF_ALLOW_INTENT_API = "prefs_key_allow_bluetooth_intent_api";
|
||||
|
||||
public static final String PREF_AUTO_FETCH_ENABLED = "auto_fetch_enabled";
|
||||
public static final String PREF_AUTO_FETCH_INTERVAL_LIMIT = "auto_fetch_interval_limit";
|
||||
|
||||
// These should get the prefix appended - see below
|
||||
public static final String AUTO_EXPORT_ENABLED = "auto_export_enabled";
|
||||
public static final String AUTO_EXPORT_LOCATION = "auto_export_location";
|
||||
public static final String AUTO_EXPORT_INTERVAL = "auto_export_interval";
|
||||
public static final String AUTO_EXPORT_LAST_EXECUTION = "auto_export_last_execution";
|
||||
public static final String AUTO_EXPORT_NEXT_EXECUTION = "auto_export_next_execution";
|
||||
|
||||
// DB export has no prefix
|
||||
public static final String AUTO_EXPORT_DB_ENABLED = AUTO_EXPORT_ENABLED;
|
||||
public static final String AUTO_EXPORT_DB_LOCATION = AUTO_EXPORT_LOCATION;
|
||||
public static final String AUTO_EXPORT_DB_INTERVAL = AUTO_EXPORT_INTERVAL;
|
||||
public static final String AUTO_EXPORT_DB_LAST_EXECUTION = AUTO_EXPORT_LAST_EXECUTION;
|
||||
public static final String AUTO_EXPORT_DB_NEXT_EXECUTION = AUTO_EXPORT_NEXT_EXECUTION;
|
||||
|
||||
// Zip export with "zip_" prefix
|
||||
public static final String AUTO_EXPORT_ZIP_ENABLED = "zip_auto_export_enabled";
|
||||
public static final String AUTO_EXPORT_ZIP_LOCATION = "zip_auto_export_location";
|
||||
public static final String AUTO_EXPORT_ZIP_INTERVAL = "zip_auto_export_interval";
|
||||
public static final String AUTO_EXPORT_ZIP_LAST_EXECUTION = "zip_auto_export_last_execution";
|
||||
public static final String AUTO_EXPORT_ZIP_NEXT_EXECUTION = "zip_auto_export_next_execution";
|
||||
|
||||
// Intent API
|
||||
public static final String INTENT_API_BROADCAST_EXPORT_DB = "intent_api_broadcast_export";
|
||||
public static final String INTENT_API_BROADCAST_EXPORT_ZIP = "intent_api_broadcast_zip_export";
|
||||
|
||||
public static final String RECONNECT_SCAN_KEY = "prefs_general_key_auto_reconnect_scan";
|
||||
public static final boolean RECONNECT_SCAN_DEFAULT = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Duration
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
abstract class PeriodicExporter {
|
||||
abstract fun getWorkerClass(): Class<out ListenableWorker>
|
||||
abstract fun getKeyPrefix(): String
|
||||
abstract fun getFileMimeType(): String
|
||||
abstract fun getFileExtension(): String
|
||||
|
||||
fun scheduleNextExecution(context: Context) {
|
||||
try {
|
||||
val keyPrefix = getKeyPrefix()
|
||||
val prefKeyEnabled = keyPrefix + GBPrefs.AUTO_EXPORT_ENABLED
|
||||
val prefKeyInterval = keyPrefix + GBPrefs.AUTO_EXPORT_INTERVAL
|
||||
val prefKeyNextExecution = keyPrefix + GBPrefs.AUTO_EXPORT_NEXT_EXECUTION
|
||||
val prefKeyStartTime = keyPrefix + "auto_export_start_time"
|
||||
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
workManager.cancelAllWorkByTag(getWorkTag())
|
||||
|
||||
val prefs: Prefs = GBApplication.getPrefs()
|
||||
|
||||
val autoExportEnabled = prefs.getBoolean(prefKeyEnabled, false)
|
||||
if (!autoExportEnabled) {
|
||||
LOG.info("Not scheduling {} - not enabled", getWorkerClass().simpleName)
|
||||
return
|
||||
}
|
||||
|
||||
val autoExportInterval = prefs.getInt(prefKeyInterval, 0)
|
||||
if (autoExportInterval == 0) {
|
||||
LOG.info("Not scheduling {}, interval set to 0", getWorkerClass().simpleName)
|
||||
return
|
||||
}
|
||||
val exportPeriodMillis = autoExportInterval * 60 * 60 * 1000L
|
||||
|
||||
val startTime = prefs.getLocalTime(prefKeyStartTime, "00:00")
|
||||
val nextExecution = nextExecution(startTime, autoExportInterval.toLong())
|
||||
val initialDelayMillis = nextExecution.toInstant().toEpochMilli() - System.currentTimeMillis()
|
||||
LOG.info(
|
||||
"Scheduling {} for {}h in the future from {} ({}ms)",
|
||||
getWorkerClass().simpleName,
|
||||
autoExportInterval,
|
||||
startTime,
|
||||
initialDelayMillis
|
||||
)
|
||||
|
||||
prefs.preferences.edit {
|
||||
putLong(prefKeyNextExecution, System.currentTimeMillis() + exportPeriodMillis)
|
||||
}
|
||||
|
||||
val exportRequest = PeriodicWorkRequest.Builder(
|
||||
getWorkerClass(),
|
||||
autoExportInterval.toLong(),
|
||||
TimeUnit.HOURS
|
||||
).setInitialDelay(initialDelayMillis, TimeUnit.MILLISECONDS)
|
||||
.addTag(getWorkTag())
|
||||
.addTag("$TAG_CREATED_AT${System.currentTimeMillis()}")
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
getWorkTag(),
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
exportRequest
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to schedule next execution for {}", getWorkerClass().simpleName, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextExecution(anchor: LocalTime, periodHours: Long): ZonedDateTime {
|
||||
val now = ZonedDateTime.now(ZoneId.systemDefault())
|
||||
|
||||
val candidate = now.with(anchor)
|
||||
|
||||
// If today's anchor is after now, it's the first execution
|
||||
if (candidate.isAfter(now)) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Otherwise, find how many periods have passed since anchor
|
||||
val elapsedMillis = ChronoUnit.MILLIS.between(candidate, now)
|
||||
val periodMillis = Duration.ofHours(periodHours).toMillis()
|
||||
|
||||
val periodsPassed = (elapsedMillis / periodMillis) + 1 // +1 = next occurrence
|
||||
return candidate.plus(periodsPassed * periodHours, ChronoUnit.HOURS)
|
||||
}
|
||||
|
||||
fun executeNow() {
|
||||
val workManager = WorkManager.getInstance(GBApplication.getContext())
|
||||
val exportRequest = OneTimeWorkRequest.Builder(getWorkerClass())
|
||||
.addTag(getWorkTag())
|
||||
.addTag("$TAG_CREATED_AT${System.currentTimeMillis()}")
|
||||
.build()
|
||||
workManager.enqueue(exportRequest)
|
||||
}
|
||||
|
||||
fun getWorkTag(): String {
|
||||
return "${getKeyPrefix()}exporter_worker"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(PeriodicExporter::class.java)
|
||||
|
||||
const val TAG_CREATED_AT = "createdAt-"
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class Prefs {
|
||||
|
||||
public String getString(String key, String defaultValue) {
|
||||
String value = preferences.getString(key, defaultValue);
|
||||
if (value == null || "".equals(value)) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -92,7 +92,7 @@ public class XDatePreference extends DialogPreference {
|
||||
return maxDate;
|
||||
}
|
||||
|
||||
String getPrefValue() {
|
||||
public String getPrefValue() {
|
||||
return String.format(Locale.ROOT, "%04d-%02d-%02d", year, month, day);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ public class XDatePreference extends DialogPreference {
|
||||
this.day = day;
|
||||
|
||||
persistStringValue(getPrefValue());
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
void updateSummary() {
|
||||
|
||||
@@ -31,34 +31,19 @@ public class XTimePreference extends DialogPreference {
|
||||
|
||||
protected Format format = Format.AUTO;
|
||||
|
||||
public XTimePreference(Context context, AttributeSet attrs) {
|
||||
public XTimePreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||
protected Object onGetDefaultValue(final TypedArray a, final int index) {
|
||||
return a.getString(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
|
||||
String time;
|
||||
|
||||
if (restoreValue) {
|
||||
if (defaultValue == null) {
|
||||
time = getPersistedString("00:00");
|
||||
} else {
|
||||
time = getPersistedString(defaultValue.toString());
|
||||
}
|
||||
} else {
|
||||
if (defaultValue != null) {
|
||||
time = defaultValue.toString();
|
||||
} else {
|
||||
time = "00:00";
|
||||
}
|
||||
}
|
||||
|
||||
String[] pieces = time.split(":");
|
||||
protected void onSetInitialValue(final Object defaultValue) {
|
||||
final String time = getPersistedString((String) defaultValue);
|
||||
final String[] pieces = time.split(":");
|
||||
|
||||
hour = Integer.parseInt(pieces[0]);
|
||||
minute = Integer.parseInt(pieces[1]);
|
||||
@@ -75,6 +60,8 @@ public class XTimePreference extends DialogPreference {
|
||||
this.minute = minute;
|
||||
|
||||
persistStringValue(getPrefValue());
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
void updateSummary() {
|
||||
@@ -85,14 +72,14 @@ public class XTimePreference extends DialogPreference {
|
||||
}
|
||||
|
||||
String getTime24h() {
|
||||
return String.format("%02d", hour) + ":" + String.format("%02d", minute);
|
||||
return String.format(Locale.ROOT, "%02d:%02d", hour, minute);
|
||||
}
|
||||
|
||||
private String getTime12h() {
|
||||
String suffix = hour < 12 ? " AM" : " PM";
|
||||
int h = hour > 12 ? hour - 12 : hour;
|
||||
final String suffix = hour < 12 ? "AM" : "PM";
|
||||
final int h = hour > 12 ? hour - 12 : hour;
|
||||
|
||||
return h + ":" + String.format("%02d", minute) + suffix;
|
||||
return String.format(Locale.ROOT, "%d:%02d %s",h, minute, suffix);
|
||||
}
|
||||
|
||||
public void setFormat(final Format format) {
|
||||
@@ -103,26 +90,21 @@ public class XTimePreference extends DialogPreference {
|
||||
return format;
|
||||
}
|
||||
|
||||
void persistStringValue(String value) {
|
||||
void persistStringValue(final String value) {
|
||||
persistString(value);
|
||||
}
|
||||
|
||||
public boolean is24HourFormat() {
|
||||
switch (format) {
|
||||
case FORMAT_24H:
|
||||
return true;
|
||||
case FORMAT_12H:
|
||||
return false;
|
||||
case AUTO:
|
||||
default:
|
||||
return DateFormat.is24HourFormat(getContext());
|
||||
}
|
||||
return switch (format) {
|
||||
case FORMAT_24H -> true;
|
||||
case FORMAT_12H -> false;
|
||||
default -> DateFormat.is24HourFormat(getContext());
|
||||
};
|
||||
}
|
||||
|
||||
public enum Format {
|
||||
AUTO,
|
||||
FORMAT_24H,
|
||||
FORMAT_12H,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -46,7 +46,6 @@ public abstract class AbstractZipBackupJob implements Runnable {
|
||||
.create();
|
||||
|
||||
private final Context mContext;
|
||||
private final Handler mHandler;
|
||||
private final ZipBackupCallback mCallback;
|
||||
|
||||
private final AtomicBoolean aborted = new AtomicBoolean(false);
|
||||
@@ -56,7 +55,6 @@ public abstract class AbstractZipBackupJob implements Runnable {
|
||||
|
||||
public AbstractZipBackupJob(final Context context, final ZipBackupCallback callback) {
|
||||
this.mContext = context;
|
||||
this.mHandler = new Handler(context.getMainLooper());
|
||||
this.mCallback = callback;
|
||||
}
|
||||
|
||||
@@ -83,18 +81,16 @@ public abstract class AbstractZipBackupJob implements Runnable {
|
||||
}
|
||||
lastProgressUpdateTs = now;
|
||||
lastProgressUpdateMessage = message;
|
||||
mHandler.post(() -> {
|
||||
mCallback.onProgress(percentage, getContext().getString(message, formatArgs));
|
||||
});
|
||||
mCallback.onProgress(percentage, getContext().getString(message, formatArgs));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected void onSuccess(final String warnings) {
|
||||
mHandler.post(() -> mCallback.onSuccess(warnings));
|
||||
mCallback.onSuccess(warnings);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected void onFailure(final String errorMessage) {
|
||||
mHandler.post(() -> mCallback.onFailure(errorMessage));
|
||||
mCallback.onFailure(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/* 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.util.backup
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PeriodicExporter
|
||||
|
||||
object PeriodicZipExporter: PeriodicExporter() {
|
||||
override fun getWorkerClass(): Class<out ListenableWorker> {
|
||||
return ZipExportWorker::class.java
|
||||
}
|
||||
|
||||
override fun getKeyPrefix(): String {
|
||||
return "zip_"
|
||||
}
|
||||
|
||||
override fun getFileMimeType(): String {
|
||||
return "application/zip"
|
||||
}
|
||||
|
||||
override fun getFileExtension(): String {
|
||||
return "zip"
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util.backup
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
|
||||
class ZipExportWorker(
|
||||
private val mContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(mContext, workerParams), ZipBackupCallback {
|
||||
private var success = false
|
||||
|
||||
override fun doWork(): Result {
|
||||
val enabled = GBApplication.getPrefs().getBoolean(GBPrefs.AUTO_EXPORT_ZIP_ENABLED, false)
|
||||
if (!enabled) {
|
||||
LOG.warn("Zip export started, but is disabled")
|
||||
// Should not need i18n, this should never happen
|
||||
onFailure("Zip export started, but is disabled");
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val dst = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_ZIP_LOCATION, null)
|
||||
|
||||
LOG.info("Starting zip export, dst={}", dst)
|
||||
|
||||
if (dst == null) {
|
||||
LOG.warn("Unable to export zip, export location not set")
|
||||
onFailure(mContext.getString(R.string.notif_export_location_not_set));
|
||||
broadcastSuccess(false)
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
setForegroundAsync(createForegroundInfo())
|
||||
|
||||
ZipBackupExportJob(mContext, this, dst.toUri()).run()
|
||||
|
||||
LOG.info("Zip export completed, success={}", success)
|
||||
|
||||
broadcastSuccess(success)
|
||||
|
||||
return if (success) Result.success() else Result.failure()
|
||||
}
|
||||
|
||||
override fun onProgress(progress: Int, message: String?) {
|
||||
setProgressAsync(Data.Builder().putInt("progress", progress).build())
|
||||
|
||||
val updatedNotification: Notification =
|
||||
NotificationCompat.Builder(mContext, GB.NOTIFICATION_CHANNEL_ID_EXPORT)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(mContext.getString(R.string.backup_restore_exporting))
|
||||
.setContentText(message)
|
||||
.setProgress(100, progress, false)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
notify(NOTIF_ID_PROGRESS, updatedNotification)
|
||||
}
|
||||
|
||||
override fun onSuccess(warnings: String?) {
|
||||
success = true
|
||||
GBApplication.getPrefs().preferences.edit {
|
||||
putLong(GBPrefs.AUTO_EXPORT_ZIP_LAST_EXECUTION, System.currentTimeMillis())
|
||||
}
|
||||
if (warnings != null) {
|
||||
val builder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(mContext, GB.NOTIFICATION_CHANNEL_ID_EXPORT)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setContentTitle(mContext.getString(R.string.backup_restore_export_complete))
|
||||
.setContentText(warnings)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
|
||||
notify(NOTIF_ID_WARNING, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(errorMessage: String?) {
|
||||
val builder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(mContext, GB.NOTIFICATION_CHANNEL_ID_EXPORT)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setContentTitle(mContext.getString(R.string.notif_export_zip_failed_title))
|
||||
.setContentText(errorMessage ?: mContext.getString(R.string.unknown_error))
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
notify(NOTIF_ID_FAILURE, builder.build())
|
||||
}
|
||||
|
||||
private fun createForegroundInfo(): ForegroundInfo {
|
||||
val notification: Notification =
|
||||
NotificationCompat.Builder(mContext, GB.NOTIFICATION_CHANNEL_ID_EXPORT)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(mContext.getString(R.string.backup_restore_exporting))
|
||||
.setProgress(100, 0, false)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(NOTIF_ID_PROGRESS, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
ForegroundInfo(NOTIF_ID_PROGRESS, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastSuccess(success: Boolean) {
|
||||
if (!GBApplication.getPrefs().getBoolean(GBPrefs.INTENT_API_BROADCAST_EXPORT_ZIP, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
LOG.info("Broadcasting zip export success={}", success)
|
||||
|
||||
val action: String = if (success) ACTION_ZIP_EXPORT_SUCCESS else ACTION_ZIP_EXPORT_FAIL
|
||||
val exportedNotifyIntent = Intent(action)
|
||||
mContext.sendBroadcast(exportedNotifyIntent)
|
||||
}
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(
|
||||
mContext,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
NotificationManagerCompat.from(mContext).notify(id, notification)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(ZipExportWorker::class.java)
|
||||
|
||||
private const val NOTIF_ID_PROGRESS: Int = 1001
|
||||
private const val NOTIF_ID_FAILURE: Int = 1002
|
||||
private const val NOTIF_ID_WARNING: Int = 1003
|
||||
|
||||
const val ACTION_ZIP_EXPORT_SUCCESS: String =
|
||||
"nodomain.freeyourgadget.gadgetbridge.action.ZIP_EXPORT_SUCCESS"
|
||||
const val ACTION_ZIP_EXPORT_FAIL: String =
|
||||
"nodomain.freeyourgadget.gadgetbridge.action.ZIP_EXPORT_FAIL"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,840q-151,0 -255.5,-46.5T120,680v-400q0,-66 105.5,-113T480,120q149,0 254.5,47T840,280v400q0,67 -104.5,113.5T480,840ZM480,361q89,0 179,-25.5T760,281q-11,-29 -100.5,-55T480,200q-91,0 -178.5,25.5T200,281q14,30 101.5,55T480,361ZM480,560q42,0 81,-4t74.5,-11.5q35.5,-7.5 67,-18.5t57.5,-25v-120q-26,14 -57.5,25t-67,18.5Q600,432 561,436t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,417 256,406t-56,-25v120q25,14 56,25t66.5,18.5Q358,552 398,556t82,4ZM480,760q46,0 93.5,-7t87.5,-18.5q40,-11.5 67,-26t32,-29.5v-98q-26,14 -57.5,25t-67,18.5Q600,632 561,636t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,617 256,606t-56,-25v99q5,15 31.5,29t66.5,25.5q40,11.5 88,18.5t94,7Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M640,480v-80h80v80h-80ZM640,560h-80v-80h80v80ZM640,640v-80h80v80h-80ZM447,320l-80,-80L160,240v480h400v-80h80v80h160v-400L640,320v80h-80v-80L447,320ZM160,800q-33,0 -56.5,-23.5T80,720v-480q0,-33 23.5,-56.5T160,160h240l80,80h320q33,0 56.5,23.5T880,320v400q0,33 -23.5,56.5T800,800L160,800ZM160,720v-480,480Z" />
|
||||
</vector>
|
||||
@@ -155,58 +155,6 @@
|
||||
android:layout_weight="1"
|
||||
android:text="@string/activity_db_management_clean_export_directory_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExportTitleLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/activity_db_management_autoexport_label"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@color/accent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExportEnabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExportScheduled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activity_db_management_autoexport_scheduled_no"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExport_lastTime_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/autoExport_lastTime_label"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExportLocation_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activity_db_management_autoexport_explanation"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/autoExportLocation_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/testExportDBButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activity_DB_test_export_button"
|
||||
grid:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mergeOldActivityDataTitle"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -625,6 +625,8 @@
|
||||
<string name="pref_sleep_mode_smart_enable_summary">Enable the sleep mode automatically when wearing the band during sleep</string>
|
||||
<!-- Auto export preferences -->
|
||||
<string name="pref_header_auto_export">Auto export</string>
|
||||
<string name="pref_header_auto_export_db">Auto export database</string>
|
||||
<string name="pref_header_auto_export_zip">Auto export zip</string>
|
||||
<string name="pref_title_auto_export_enabled">Auto export enabled</string>
|
||||
<string name="pref_title_auto_export_location">Export location</string>
|
||||
<string name="pref_title_auto_export_interval">Export interval</string>
|
||||
@@ -936,6 +938,9 @@
|
||||
<string name="notif_battery_full">%1$s battery full</string>
|
||||
<string name="notif_battery_low_extended">%1$s battery low: %2$s</string>
|
||||
<string name="notif_export_failed_title">Export database failed! Please check your settings.</string>
|
||||
<string name="notif_export_location_not_set">Export location not configured</string>
|
||||
<string name="notif_export_zip_failed_title">Export zip failed! Please check your settings.</string>
|
||||
<string name="unknown_error">Unknown error</string>
|
||||
<string name="prefs_charts_tabs">Charts tabs</string>
|
||||
<string name="prefs_charts_tabs_summary">Visible chart tabs</string>
|
||||
<string name="sleepchart_your_sleep">Sleep</string>
|
||||
@@ -1360,13 +1365,8 @@
|
||||
<string name="dbmanagementactivity_old_activity_db_successfully_deleted">Old activity data deleted.</string>
|
||||
<string name="dbmanagementactivity_old_activity_db_deletion_failed">Old Activity database deletion failed.</string>
|
||||
<string name="dbmanagementactivity_overwrite">Overwrite</string>
|
||||
<string name="activity_db_management_autoexport_explanation">Database autoexport location has been set to:</string>
|
||||
<string name="autoExport_lastTime_label">Last AutoExport: %1$s</string>
|
||||
<string name="activity_db_management_autoexport_enabled_yes">AutoExport is enabled.</string>
|
||||
<string name="activity_db_management_autoexport_enabled_no">AutoExport is not enabled.</string>
|
||||
<string name="activity_db_management_autoexport_scheduled_yes">AutoExport has (originally) been scheduled for %1$s</string>
|
||||
<string name="activity_db_management_autoexport_scheduled_no">AutoExport has not been not scheduled.</string>
|
||||
<string name="activity_db_management_autoexport_label">AutoExport</string>
|
||||
<string name="activity_db_management_autoexport_location">Location could not be understood. Likely an issue of newer Android permission system. Most likely, autoexport is not working now.</string>
|
||||
<string name="activity_DB_ExportButton">Export Data</string>
|
||||
<string name="activity_DB_import_button">Import Data</string>
|
||||
@@ -3452,8 +3452,13 @@
|
||||
<string name="intent_api_allow_trigger_export_summary">Allow triggering database export via Intent API</string>
|
||||
<string name="intent_api_broadcast_export_title">Broadcast on database export</string>
|
||||
<string name="intent_api_broadcast_export_summary">Broadcast an intent when database export finishes</string>
|
||||
<string name="intent_api_allow_trigger_zip_export_title">Allow zip export</string>
|
||||
<string name="intent_api_allow_trigger_zip_export_summary">Allow triggering zip export via Intent API</string>
|
||||
<string name="intent_api_broadcast_zip_export_title">Broadcast on zip export</string>
|
||||
<string name="intent_api_broadcast_zip_export_summary">Broadcast an intent when zip export finishes</string>
|
||||
<string name="intent_api_broadcast_activity_sync_title">Broadcast on activity sync finish</string>
|
||||
<string name="intent_api_broadcast_activity_sync_summary">Broadcast an intent when activity sync finishes for any device</string>
|
||||
<string name="intent_activity_sync_title">Activity sync</string>
|
||||
<string name="intent_api_allow_debug_commands_title">Allow Debug Commands</string>
|
||||
<string name="intent_api_allow_debug_commands_summary">Allow triggering debug menu commands via Intent API</string>
|
||||
<string name="devicetype_super_cars" translatable="false">Shell Racing</string>
|
||||
@@ -4265,4 +4270,15 @@
|
||||
<string name="connect">Connect</string>
|
||||
<string name="pref_new_sync_protocol_title">New sync protocol</string>
|
||||
<string name="pref_new_sync_protocol_summary">Enable this if some information is not being fetched. Experimental and potentially unstable.</string>
|
||||
<string name="auto_export_last_execution">Last execution</string>
|
||||
<string name="auto_export_next_execution">Next execution</string>
|
||||
<string name="work_info_status_running">Running</string>
|
||||
<string name="work_info_status_enqueued">Enqueued</string>
|
||||
<string name="work_info_status_succeeded">Succeeded</string>
|
||||
<string name="work_info_status_failed">Failed</string>
|
||||
<string name="work_info_status_blocked">Blocked</string>
|
||||
<string name="work_info_status_cancelled">Cancelled</string>
|
||||
<string name="work_info_running_percentage">%1$s (%2$d%%)</string>
|
||||
<string name="datetime_in_the_future">%1$s (in %2$s)</string>
|
||||
<string name="datetime_in_the_past">%1$s (%2$s ago)</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<!--
|
||||
WARNING: All the preferences in this page get a prefix added programmatically
|
||||
by AbstractAutoExportSettingsFragment
|
||||
-->
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_toggle_on"
|
||||
android:key="auto_export_enabled"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_title_auto_export_enabled"
|
||||
app:iconSpaceReserved="false" />
|
||||
<Preference
|
||||
android:dependency="auto_export_enabled"
|
||||
android:icon="@drawable/ic_file_export"
|
||||
android:key="auto_export_location"
|
||||
android:title="@string/pref_title_auto_export_location"
|
||||
app:iconSpaceReserved="false" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="3"
|
||||
android:dependency="auto_export_enabled"
|
||||
android:icon="@drawable/ic_timer"
|
||||
android:inputType="number"
|
||||
android:key="auto_export_interval"
|
||||
android:maxLength="3"
|
||||
android:summary="@string/pref_summary_auto_export_interval"
|
||||
android:title="@string/pref_title_auto_export_interval"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
|
||||
android:defaultValue="00:00"
|
||||
android:dependency="auto_export_enabled"
|
||||
android:icon="@drawable/ic_access_time"
|
||||
android:key="auto_export_start_time"
|
||||
android:title="@string/mi2_prefs_do_not_disturb_start" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_status"
|
||||
android:title="@string/status">
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_info"
|
||||
android:key="auto_export_status"
|
||||
android:summary="@string/unknown"
|
||||
android:title="@string/status" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_access_time"
|
||||
android:key="auto_export_last_execution"
|
||||
android:summary="@string/unknown"
|
||||
android:title="@string/auto_export_last_execution" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_access_time"
|
||||
android:key="auto_export_next_execution"
|
||||
android:summary="@string/unknown"
|
||||
android:title="@string/auto_export_next_execution" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_test"
|
||||
android:title="@string/test">
|
||||
|
||||
<Preference
|
||||
android:dependency="auto_export_enabled"
|
||||
android:icon="@drawable/ic_play"
|
||||
android:key="auto_export_run_now"
|
||||
android:title="@string/activity_DB_test_export_button" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_header_auto_export">
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_database"
|
||||
android:title="@string/pref_header_auto_export_db">
|
||||
<intent
|
||||
android:targetClass="nodomain.freeyourgadget.gadgetbridge.activities.automations.AutoExportDbSettingsActivity"
|
||||
android:targetPackage="@string/applicationId" />
|
||||
</Preference>
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_zip"
|
||||
android:title="@string/pref_header_auto_export_zip">
|
||||
<intent
|
||||
android:targetClass="nodomain.freeyourgadget.gadgetbridge.activities.automations.AutoExportZipSettingsActivity"
|
||||
android:targetPackage="@string/applicationId" />
|
||||
</Preference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_header_auto_fetch">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_refresh"
|
||||
android:key="auto_fetch_enabled"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/pref_auto_fetch_summary"
|
||||
android:title="@string/pref_auto_fetch"
|
||||
app:iconSpaceReserved="false" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="0"
|
||||
android:dependency="auto_fetch_enabled"
|
||||
android:icon="@drawable/ic_timer"
|
||||
android:inputType="number"
|
||||
android:key="auto_fetch_interval_limit"
|
||||
android:maxLength="3"
|
||||
android:summary="@string/pref_auto_fetch_limit_fetches_summary"
|
||||
android:title="@string/pref_auto_fetch_limit_fetches"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -246,55 +246,11 @@
|
||||
android:title="@string/maps"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
<Preference
|
||||
android:icon="@drawable/ic_auto_awesome"
|
||||
android:key="pref_screen_automations"
|
||||
android:summary="@string/pref_description_automations"
|
||||
android:title="@string/pref_header_automations">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_header_auto_export">
|
||||
<Preference
|
||||
android:key="auto_export_location"
|
||||
android:title="@string/pref_title_auto_export_location"
|
||||
android:icon="@drawable/ic_file_export" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="auto_export_enabled"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/pref_title_auto_export_enabled"
|
||||
android:icon="@drawable/ic_toggle_on" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="3"
|
||||
android:dependency="auto_export_enabled"
|
||||
android:inputType="number"
|
||||
android:key="auto_export_interval"
|
||||
android:maxLength="3"
|
||||
android:summary="@string/pref_summary_auto_export_interval"
|
||||
android:title="@string/pref_title_auto_export_interval"
|
||||
android:icon="@drawable/ic_timer" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_header_auto_fetch">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="auto_fetch_enabled"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/pref_auto_fetch_summary"
|
||||
android:title="@string/pref_auto_fetch"
|
||||
android:icon="@drawable/ic_refresh" />
|
||||
<EditTextPreference
|
||||
android:defaultValue="0"
|
||||
android:dependency="auto_fetch_enabled"
|
||||
android:inputType="number"
|
||||
android:key="auto_fetch_interval_limit"
|
||||
android:maxLength="3"
|
||||
android:summary="@string/pref_auto_fetch_limit_fetches_summary"
|
||||
android:title="@string/pref_auto_fetch_limit_fetches"
|
||||
android:icon="@drawable/ic_timer" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
android:title="@string/pref_header_automations" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_extension"
|
||||
@@ -408,53 +364,97 @@
|
||||
android:title="@string/pref_cache_weather"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceCategory
|
||||
<PreferenceScreen
|
||||
android:key="pref_screen_intent_api"
|
||||
android:title="@string/pref_header_intent_api"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="prefs_key_allow_bluetooth_intent_api"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/activity_prefs_summary_allow_bluetooth_intent_api"
|
||||
android:title="@string/activity_prefs_allow_bluetooth_intent_api"
|
||||
app:iconSpaceReserved="false" />
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_bluetooth"
|
||||
android:title="@string/menuitem_bluetooth"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_activity_sync"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_activity_sync_summary"
|
||||
android:title="@string/intent_api_allow_activity_sync_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="prefs_key_allow_bluetooth_intent_api"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/activity_prefs_summary_allow_bluetooth_intent_api"
|
||||
android:title="@string/activity_prefs_allow_bluetooth_intent_api"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_broadcast_activity_sync"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_broadcast_activity_sync_summary"
|
||||
android:title="@string/intent_api_broadcast_activity_sync_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_activity_sync"
|
||||
android:title="@string/intent_activity_sync_title"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_trigger_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_trigger_export_summary"
|
||||
android:title="@string/intent_api_allow_trigger_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_activity_sync"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_activity_sync_summary"
|
||||
android:title="@string/intent_api_allow_activity_sync_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_broadcast_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_broadcast_export_summary"
|
||||
android:title="@string/intent_api_broadcast_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_broadcast_activity_sync"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_broadcast_activity_sync_summary"
|
||||
android:title="@string/intent_api_broadcast_activity_sync_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_debug_commands"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_debug_commands_summary"
|
||||
android:title="@string/intent_api_allow_debug_commands_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_auto_export_db"
|
||||
android:title="@string/pref_header_auto_export_db"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_trigger_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_trigger_export_summary"
|
||||
android:title="@string/intent_api_allow_trigger_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_broadcast_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_broadcast_export_summary"
|
||||
android:title="@string/intent_api_broadcast_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_auto_export_zip"
|
||||
android:title="@string/pref_header_auto_export_zip"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_trigger_zip_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_trigger_zip_export_summary"
|
||||
android:title="@string/intent_api_allow_trigger_zip_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_broadcast_zip_export"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_broadcast_zip_export_summary"
|
||||
android:title="@string/intent_api_broadcast_zip_export_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="pref_header_debug"
|
||||
android:title="@string/action_debug"
|
||||
app:iconSpaceReserved="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="intent_api_allow_debug_commands"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/intent_api_allow_debug_commands_summary"
|
||||
android:title="@string/intent_api_allow_debug_commands_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen
|
||||
|
||||
Reference in New Issue
Block a user