Revamp Debug activity

This commit is contained in:
José Rebelo
2025-12-26 20:02:37 +00:00
parent 8dd1af66f9
commit ad1e0c5589
65 changed files with 3801 additions and 2015 deletions
+3
View File
@@ -116,12 +116,15 @@
<w>morpheuz</w>
<w>mosenkovs</w>
<w>multisport</w>
<w>musicspec</w>
<w>musicstatespec</w>
<w>nack</w>
<w>nephiel</w>
<w>nodomain</w>
<w>nordhøy</w>
<w>normano</w>
<w>novotny</w>
<w>opentracks</w>
<w>oraclejdk</w>
<w>oukitel</w>
<w>paddleboarding</w>
+6 -2
View File
@@ -664,10 +664,14 @@
dialog when changing orientation
-->
<activity
android:name=".activities.DebugActivity"
android:name=".activities.debug.DebugActivityV2"
android:label="@string/title_activity_debug"
android:parentActivityName=".activities.ControlCenterv2"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.debug.preferences.PreferenceManagerActivity"
android:label="@string/menuitem_settings"
android:parentActivityName=".activities.debug.DebugActivityV2"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.AboutActivity"
@@ -141,7 +141,7 @@ public class GBApplication extends Application {
private static final Lock dbLock = new ReentrantLock();
private static DeviceService deviceService;
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
public static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 54;
@@ -65,6 +65,7 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.debug.DebugActivityV2;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -344,7 +345,7 @@ public class ControlCenterv2 extends AppCompatActivity
startActivityForResult(settingsIntent, MENU_REFRESH_CODE);
return false;
} else if (itemId == R.id.action_debug) {
final Intent debugIntent = new Intent(this, DebugActivity.class);
final Intent debugIntent = new Intent(this, DebugActivityV2.class);
startActivity(debugIntent);
return false;
} else if (itemId == R.id.action_data_management) {
@@ -19,12 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
@@ -145,81 +142,9 @@ public class DataManagementActivity extends AbstractGBActivity {
startActivity(fileManagerIntent);
});
int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE;
TextView deleteOldActivityTitle = findViewById(R.id.mergeOldActivityDataTitle);
deleteOldActivityTitle.setVisibility(oldDBVisibility);
Button deleteOldActivityDBButton = findViewById(R.id.deleteOldActivityDB);
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteOldActivityDbFile();
}
});
Button deleteDBButton = findViewById(R.id.emptyDBButton);
deleteDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteActivityDatabase();
}
});
TextView dbPath2 = findViewById(R.id.activity_data_management_path2);
dbPath2.setText(getExternalPath());
Button cleanExportDirectoryButton = findViewById(R.id.cleanExportDirectoryButton);
cleanExportDirectoryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
cleanExportDirectory();
}
});
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
}
private String getAutoExportLocationPreferenceString() {
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_DB_LOCATION, null);
if (autoExportLocation == null) {
return "";
}
return autoExportLocation;
}
private String getAutoExportLocationUri() {
String autoExportLocation = getAutoExportLocationPreferenceString();
if (autoExportLocation == null) {
return "";
}
Uri uri = Uri.parse(autoExportLocation);
try {
return AndroidUtils.getFilePath(getApplicationContext(), uri);
} catch (IllegalArgumentException e) {
LOG.error("getFilePath did not work, trying to resolve content provider path");
try {
Cursor cursor = 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 exception) {
LOG.error("Error getting export path", exception);
}
}
return "";
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).existsDB("ActivityDatabase");
}
private String getExternalPath() {
try {
return FileUtils.getExternalFilesDir().getAbsolutePath();
@@ -351,93 +276,6 @@ public class DataManagementActivity extends AbstractGBActivity {
.show();
}
private void deleteActivityDatabase() {
new MaterialAlertDialogBuilder(this)
.setCancelable(true)
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteActivityDatabase(DataManagementActivity.this)) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_database_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void deleteOldActivityDbFile() {
new MaterialAlertDialogBuilder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_delete_old_activity_db)
.setIcon(R.drawable.ic_warning)
.setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteOldActivityDatabase(DataManagementActivity.this)) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
});
new MaterialAlertDialogBuilder(this).setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
new MaterialAlertDialogBuilder(this).show();
}
private void cleanExportDirectory() {
new MaterialAlertDialogBuilder(this)
.setCancelable(true)
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.activity_DB_clean_export_directory_warning_title)
.setMessage(getString(R.string.activity_DB_clean_export_directory_warning_message))
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
File externalFilesDir = FileUtils.getExternalFilesDir();
String autoexportFile = getAutoExportLocationUri();
for (File file : externalFilesDir.listFiles()) {
if (file.isFile() &&
(!FileUtils.getExtension(file.toString()).toLowerCase().equals("gpx")) && //keep GPX files
(!file.toString().equals(autoexportFile)) // do not remove autoexport
) {
LOG.debug("Deleting file: " + file);
try {
file.delete();
} catch (Exception exception) {
LOG.error("Error erasing file: " + exception);
}
}
}
GB.toast(getString(R.string.dbmanagementactivity_export_finished), Toast.LENGTH_SHORT, GB.INFO);
} catch (Exception ex) {
GB.toast(DataManagementActivity.this, getString(R.string.dbmanagementactivity_error_cleaning_export_directory, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
@@ -66,8 +66,6 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesA
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsActivity;
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.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -186,17 +184,6 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
}
}
pref = findPreference("cache_weather");
if (pref != null) {
pref.setOnPreferenceChangeListener((preference, newVal) -> {
boolean doEnable = Boolean.TRUE.equals(newVal);
Weather.initializeCache(new WeatherCacheManager(requireContext().getCacheDir(), doEnable));
return true;
});
}
pref = findPreference("language");
if (pref != null) {
pref.setOnPreferenceChangeListener((preference, newVal) -> {
@@ -0,0 +1,119 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.text.InputType
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.util.GB
import java.util.UUID
abstract class AbstractDebugFragment : AbstractPreferenceFragment() {
protected fun runOnDebugDevices(title: String? = null, forceDialog: Boolean = false, function: (GBDevice) -> Unit) {
val selectedDevices = GBApplication.app().deviceManager.selectedDevices
.filter { it.isInitialized }
.sortedBy { it.aliasOrName }
if (selectedDevices.isEmpty()) {
GB.toast(
requireContext(),
requireContext().getString(R.string.info_no_devices_connected),
Toast.LENGTH_LONG,
GB.ERROR
)
return
}
if (selectedDevices.size > 1 || forceDialog) {
val deviceNames = selectedDevices.map { it.aliasOrName }.toTypedArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(title ?: getString(R.string.choose_device))
.setItems(deviceNames) { _, which ->
function(selectedDevices[which])
}
.setNegativeButton(R.string.Cancel, null)
.show()
} else {
function(selectedDevices[0])
}
}
protected fun onClick(prefKey: String, function: () -> Unit) {
findPreference<Preference>(prefKey)!!.setOnPreferenceClickListener {
function.invoke()
return@setOnPreferenceClickListener true
}
}
protected fun setInputTypeNumber(prefKey: String) {
findPreference<EditTextPreference>(prefKey)!!.setOnBindEditTextListener {
it.setInputType(InputType.TYPE_CLASS_NUMBER)
}
}
protected fun setListPreferenceEntries(prefKey: String, entries: Array<String>) {
val statePref = findPreference<ListPreference>(prefKey)!!
statePref.entries = entries
statePref.entryValues = entries
}
protected fun removeDynamicPrefs(group: PreferenceGroup) {
var i = 0
while (i < group.preferenceCount) {
val preference: Preference = group.getPreference(i)
if (preference.key.startsWith(PREF_DYNAMIC_PREFIX)) {
group.removePreference(preference)
i--
}
i++
}
}
protected fun addDynamicPref(
group: PreferenceGroup? = preferenceScreen,
title: String,
summary: String = "",
icon: Int = 0,
onClickFunction: (() -> Unit)? = null
): Preference {
val pref = Preference(requireContext())
pref.setKey("${PREF_DYNAMIC_PREFIX}_${UUID.randomUUID()}")
pref.isPersistent = false
pref.isSelectable = onClickFunction != null
pref.title = title
pref.summary = summary
if (icon != 0) {
pref.setIcon(icon)
} else if (!title.isEmpty()) {
pref.isIconSpaceReserved = false
}
if (onClickFunction != null) {
pref.setOnPreferenceClickListener {
onClickFunction.invoke()
return@setOnPreferenceClickListener true
}
}
group?.addPreference(pref)
return pref
}
protected fun goTo(fragment: AbstractDebugFragment) {
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.settings_container, fragment)
.addToBackStack(null)
.commit()
}
companion object {
protected const val PREF_DYNAMIC_PREFIX: String = "pref_debug_dynamic"
}
}
@@ -0,0 +1,82 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.os.Bundle
import androidx.core.content.edit
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec
class CallsDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setupPreferences()
}
private fun setupPreferences() {
setPreferencesFromResource(R.xml.debug_preferences_calls, null)
onClick(PREF_DEBUG_CALL_SEND) { sendCallSpec() }
onClick(PREF_DEBUG_CALL_RESET) { resetPreferences() }
setListPreferenceEntries(
PREF_DEBUG_CALL_COMMAND,
CallSpec.Command.entries.map { it.name }.toTypedArray()
)
}
private fun sendCallSpec() {
val sharedPreferences = preferenceManager.sharedPreferences!!
val callSpec = CallSpec()
callSpec.command =
CallSpec.Command.valueOf(sharedPreferences.getString(PREF_DEBUG_CALL_COMMAND, "INCOMING")!!).ordinal
callSpec.number = sharedPreferences.getString(PREF_DEBUG_CALL_NUMBER, "6365553226")
callSpec.name = sharedPreferences.getString(PREF_DEBUG_CALL_NAME, "Mr. Plow")
callSpec.sourceName = sharedPreferences.getString(PREF_DEBUG_CALL_SOURCE_NAME, null)
callSpec.sourceAppId = sharedPreferences.getString(PREF_DEBUG_CALL_SOURCE_APP_ID, null)
callSpec.key = sharedPreferences.getString(PREF_DEBUG_CALL_KEY, null)
callSpec.channelId = sharedPreferences.getString(PREF_DEBUG_CALL_CHANNEL_ID, null)
callSpec.category = sharedPreferences.getString(PREF_DEBUG_CALL_CATEGORY, null)
callSpec.isVoip = sharedPreferences.getBoolean(PREF_DEBUG_CALL_IS_VOIP, false)
callSpec.dndSuppressed = if (sharedPreferences.getBoolean(PREF_DEBUG_CALL_DND_SUPPRESSED_BOOL, false)) 1 else 0
runOnDebugDevices("Send CallSpec") {
GBApplication.deviceService(it).onSetCallState(callSpec)
}
}
private fun resetPreferences() {
preferenceScreen.removeAll()
preferenceManager.sharedPreferences!!.edit(true) {
remove(PREF_DEBUG_CALL_COMMAND)
remove(PREF_DEBUG_CALL_NUMBER)
remove(PREF_DEBUG_CALL_NAME)
remove(PREF_DEBUG_CALL_SOURCE_NAME)
remove(PREF_DEBUG_CALL_SOURCE_APP_ID)
remove(PREF_DEBUG_CALL_KEY)
remove(PREF_DEBUG_CALL_CHANNEL_ID)
remove(PREF_DEBUG_CALL_CATEGORY)
remove(PREF_DEBUG_CALL_IS_VOIP)
remove(PREF_DEBUG_CALL_DND_SUPPRESSED_BOOL)
}
// Reload the preference screen to reflect the changes
setupPreferences()
}
companion object {
private const val PREF_DEBUG_CALL_SEND = "pref_debug_call_send"
private const val PREF_DEBUG_CALL_RESET = "pref_debug_call_reset"
private const val PREF_DEBUG_HEADER_CALLSPEC = "pref_debug_header_callspec"
private const val PREF_DEBUG_CALL_COMMAND = "pref_debug_call_command"
private const val PREF_DEBUG_CALL_NUMBER = "pref_debug_call_number"
private const val PREF_DEBUG_CALL_NAME = "pref_debug_call_name"
private const val PREF_DEBUG_CALL_SOURCE_NAME = "pref_debug_call_source_name"
private const val PREF_DEBUG_CALL_SOURCE_APP_ID = "pref_debug_call_source_app_id"
private const val PREF_DEBUG_CALL_KEY = "pref_debug_call_key"
private const val PREF_DEBUG_CALL_CHANNEL_ID = "pref_debug_call_channel_id"
private const val PREF_DEBUG_CALL_CATEGORY = "pref_debug_call_category"
private const val PREF_DEBUG_CALL_IS_VOIP = "pref_debug_call_is_voip"
private const val PREF_DEBUG_CALL_DND_SUPPRESSED_BOOL = "pref_debug_call_dnd_suppressed_bool"
}
}
@@ -0,0 +1,191 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.Manifest
import android.app.Activity
import android.bluetooth.BluetoothDevice
import android.companion.AssociationInfo
import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.IntentSender.SendIntentException
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.core.app.ActivityCompat
import androidx.preference.PreferenceCategory
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil
import nodomain.freeyourgadget.gadgetbridge.util.GB
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Locale
@RequiresApi(Build.VERSION_CODES.O)
class CompanionDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_companion, rootKey)
reloadDevices()
}
private fun reloadDevices() {
val companionDevicesHeader: PreferenceCategory = findPreference(PREF_HEADER_COMPANION_DEVICES)!!
val otherDevicesHeader: PreferenceCategory = findPreference(PREF_HEADER_OTHER_DEVICES)!!
removeDynamicPrefs(companionDevicesHeader)
removeDynamicPrefs(otherDevicesHeader)
val manager = GBApplication.getContext().getSystemService(Context.COMPANION_DEVICE_SERVICE)
as CompanionDeviceManager
val associations: Set<String> = manager.associations.map { it.uppercase(Locale.ROOT) }.toSet()
val companionDevices = associations
.map {
val deviceByAddress = GBApplication.app().deviceManager.getDeviceByAddress(it)
Device(
it,
deviceByAddress?.aliasOrName ?: getString(R.string.unknown),
deviceByAddress?.deviceCoordinator?.defaultIconResource ?: R.drawable.ic_device_unknown
)
}
.sortedBy { it.name }
val otherDevices = GBApplication.app().deviceManager.devices
.filter { !associations.contains(it.address.uppercase(Locale.ROOT)) }
.map {
Device(
it.address,
it.aliasOrName,
it.deviceCoordinator.defaultIconResource
)
}
if (!companionDevices.isEmpty()) {
for (device in companionDevices) {
addDynamicPref(companionDevicesHeader, device.name, device.address, device.icon) {
MaterialAlertDialogBuilder(requireContext())
.setCancelable(true)
.setIcon(device.icon)
.setTitle("Unpair companion device")
.setMessage("Unpair ${device.name} (${device.address}) from companion device?")
.setNeutralButton(R.string.Cancel) { _, _ -> }
.setPositiveButton(R.string.ok) { _, _ -> unpairCompanion(device) }
.show()
}
}
} else {
addDynamicPref(companionDevicesHeader, "", "No companion devices")
}
if (!otherDevices.isEmpty()) {
otherDevicesHeader.isVisible = true
for (device in otherDevices) {
addDynamicPref(otherDevicesHeader, device.name, device.address, device.icon) {
MaterialAlertDialogBuilder(requireContext())
.setCancelable(true)
.setIcon(device.icon)
.setTitle("Pair as companion")
.setMessage("Pair ${device.name} (${device.address}) as companion device?")
.setNeutralButton(R.string.Cancel) { _, _ -> }
.setPositiveButton(R.string.ok) { _, _ -> pairAsCompanion(device) }
.show()
}
}
} else {
otherDevicesHeader.isVisible = false
}
}
private data class Device(val address: String, val name: String, val icon: Int)
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == SELECT_DEVICE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val deviceToPair = data?.getParcelableExtra<BluetoothDevice?>(CompanionDeviceManager.EXTRA_DEVICE)
if (deviceToPair != null) {
if (deviceToPair.getBondState() != BluetoothDevice.BOND_BONDED) {
GB.toast("Creating bond...", Toast.LENGTH_SHORT, GB.INFO)
deviceToPair.createBond()
} else {
GB.toast("Bonding complete", Toast.LENGTH_LONG, GB.INFO)
reloadDevices()
}
} else {
GB.toast("No device to pair", Toast.LENGTH_LONG, GB.ERROR)
}
}
}
private fun unpairCompanion(device: Device) {
BondingUtil.Disassociate(requireContext(), device.address)
reloadDevices()
}
private fun pairAsCompanion(device: Device) {
val manager = requireActivity().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
if (manager.associations.contains(device.address)) {
GB.toast(device.name + " already paired as companion", Toast.LENGTH_LONG, GB.INFO)
return
}
val deviceFilter = BluetoothDeviceFilter.Builder()
.setAddress(device.address)
.build()
val pairingRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.build()
val callback: CompanionDeviceManager.Callback = object : CompanionDeviceManager.Callback() {
override fun onFailure(error: CharSequence?) {
GB.toast("Companion pairing failed: $error", Toast.LENGTH_LONG, GB.ERROR)
}
override fun onAssociationPending(chooserLauncher: IntentSender) {
GB.toast("Found device", Toast.LENGTH_SHORT, GB.INFO)
try {
ActivityCompat.startIntentSenderForResult(
requireActivity(),
chooserLauncher,
SELECT_DEVICE_REQUEST_CODE,
null,
0,
0,
0,
null
)
} catch (e: SendIntentException) {
LOG.error("Failed to send intent", e)
}
}
override fun onAssociationCreated(associationInfo: AssociationInfo) {
GB.toast("Companion pairing success", Toast.LENGTH_SHORT, GB.INFO)
reloadDevices()
}
}
manager.associate(pairingRequest, callback, null)
}
companion object {
private val LOG: Logger = LoggerFactory.getLogger(CompanionDebugFragment::class.java)
const val SELECT_DEVICE_REQUEST_CODE: Int = 1424
private const val PREF_HEADER_COMPANION_DEVICES = "pref_header_companion_devices"
private const val PREF_HEADER_OTHER_DEVICES = "pref_header_other_devices"
}
}
@@ -0,0 +1,149 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.os.Bundle
import android.widget.Toast
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
import nodomain.freeyourgadget.gadgetbridge.util.GB
class DatabaseDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_database, rootKey)
onClick(PREF_DEBUG_EMPTY_DATABASE) { emptyDatabase() }
onClick(PREF_DEBUG_DELETE_OLD_DATABASE) { deleteOldActivityDbFile() }
if (!hasOldActivityDatabase()) {
findPreference<Preference>(PREF_DEBUG_DELETE_OLD_DATABASE)!!.isVisible = false
}
addTables()
}
private fun addTables() {
val tablesHeader: PreferenceCategory = findPreference(PREF_HEADER_DATABASE_TABLES)!!
removeDynamicPrefs(tablesHeader)
val tableNames = mutableListOf<String>()
try {
GBApplication.acquireDB().use { db ->
findPreference<Preference>(PREF_DEBUG_DATABASE_VERSION)?.summary = db.database.version.toString()
val cursor = db.database.rawQuery(
"""
SELECT name
FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'android_%'
AND name NOT LIKE 'sqlite_%'
""".trimIndent(),
null
)
cursor.use {
while (it.moveToNext()) {
val name = it.getString(it.getColumnIndexOrThrow("name"))
tableNames.add(name)
}
}
tableNames.sortWith(Comparator { o1, o2 -> o1.compareTo(o2, true) })
}
} catch (e: Exception) {
GB.log("Error accessing database", GB.ERROR, e)
}
for (tableName in tableNames) {
addDynamicPref(tablesHeader, tableName) {
goTo(
DatabaseTableDebugFragment().apply {
arguments = Bundle().apply { putString("tableName", tableName) }
}
)
}
}
}
private fun hasOldActivityDatabase(): Boolean {
return DBHelper(requireContext()).existsDB("ActivityDatabase")
}
private fun emptyDatabase() {
MaterialAlertDialogBuilder(requireContext())
.setCancelable(true)
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
.setPositiveButton(R.string.Delete) { _, _ ->
if (GBApplication.deleteActivityDatabase(requireContext())) {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_database_successfully_deleted),
Toast.LENGTH_SHORT,
GB.INFO
)
GBApplication.restart()
} else {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_db_deletion_failed),
Toast.LENGTH_SHORT,
GB.INFO
)
}
}
.setNegativeButton(R.string.Cancel) { _, _ -> }
.show()
}
private fun deleteOldActivityDbFile() {
MaterialAlertDialogBuilder(requireContext())
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_delete_old_activity_db)
.setIcon(R.drawable.ic_warning)
.setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation)
.setPositiveButton(R.string.Delete) { _, _ ->
if (GBApplication.deleteOldActivityDatabase(requireContext())) {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted),
Toast.LENGTH_SHORT,
GB.INFO
)
} else {
GB.toast(
requireContext(),
getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed),
Toast.LENGTH_SHORT,
GB.INFO
)
}
if (!hasOldActivityDatabase()) {
findPreference<Preference>(PREF_DEBUG_DELETE_OLD_DATABASE)!!.isVisible = false
}
}.setNegativeButton(R.string.Cancel) { _, _ -> }.show()
}
// TODO ability to run raw sql from the UI
private fun runSql() {
try {
GBApplication.acquireDB().use { db ->
db.database.execSQL("DROP TABLE IF EXISTS TABLE_NAME_TO_DROP_HERE")
}
} catch (e: Exception) {
GB.log("Error accessing database", GB.ERROR, e)
}
}
companion object {
private const val PREF_DEBUG_DATABASE_VERSION = "pref_debug_database_version"
private const val DANGEROUS_ACTIONS = "dangerous_actions"
private const val PREF_DEBUG_DELETE_OLD_DATABASE = "pref_debug_delete_old_database"
private const val PREF_DEBUG_EMPTY_DATABASE = "pref_debug_empty_database"
private const val PREF_HEADER_DATABASE_TABLES = "pref_header_database_tables"
}
}
@@ -0,0 +1,109 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.widget.TextView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import kotlin.use
class DatabaseTableDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_empty, rootKey)
val tableName = arguments?.getString("tableName")!!
preferenceScreen?.title = tableName
val ddl = getTableDdl(tableName)
val pref = object : Preference(requireContext()) {
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val summary = holder.findViewById(android.R.id.summary) as? TextView
summary?.let {
// HACK: sql can be very long
summary.isSingleLine = false
summary.maxLines = ddl.lines().size + 1
}
}
}
pref.key = "database_table_sql"
pref.title = "SQL"
pref.summary = ddl
pref.isPersistent = false
pref.isIconSpaceReserved = false
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(tableName, ddl)
clipboard.setPrimaryClip(clip)
true
}
preferenceScreen?.addPreference(pref)
}
private fun getTableDdl(tableName: String): String {
try {
GBApplication.acquireDB().use { db ->
val cursor = db.database.rawQuery(
"""
SELECT sql
FROM sqlite_master
WHERE name='${tableName}' OR tbl_name='${tableName}';
""".trimIndent(),
null
)
val sqlQueries = mutableListOf<String>()
cursor.use {
while (it.moveToNext()) {
if (it.getColumnIndex("sql") >= 0) {
sqlQueries.add(formatSql(it.getString(it.getColumnIndexOrThrow("sql")).trim()) + ";")
}
}
}
return sqlQueries.joinToString("\n\n")
}
} catch (e: Exception) {
return e.message ?: "Failed to get DDL for table $tableName"
}
}
private fun formatSql(sql: String): String {
val indentStep = " "
val sb = StringBuilder()
var indentLevel = 0
var i = 0
while (i < sql.length) {
when (sql[i]) {
'(' -> {
indentLevel++
sb.append("(\n")
sb.append(indentStep.repeat(indentLevel))
}
')' -> {
indentLevel--
sb.append("\n")
sb.append(indentStep.repeat(indentLevel))
sb.append(")")
}
',' -> {
sb.append(",\n")
sb.append(indentStep.repeat(indentLevel))
}
else -> sb.append(sql[i])
}
i++
}
return sb.toString().trim()
}
}
@@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import androidx.preference.PreferenceFragmentCompat
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
class DebugActivityV2 : AbstractSettingsActivityV2(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
override fun newFragment(): PreferenceFragmentCompat? {
return MainDebugFragment()
}
}
@@ -0,0 +1,67 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.os.Bundle
import android.os.Handler
import androidx.preference.Preference
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class LocationDebugFragment : AbstractDebugFragment() {
private val handler = Handler()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_location, rootKey)
onClick(PREF_DEBUG_OPENTRACKS_START) { OpenTracksController.startRecording(requireContext()) }
onClick(PREF_DEBUG_OPENTRACKS_STOP) { OpenTracksController.stopRecording(requireContext()) }
onClick(PREF_DEBUG_GPS_LISTENER_STOP) { GBLocationService.stop(requireContext(), null) }
refreshOpenTracksStatus()
scheduleRefresh()
}
override fun onStart() {
super.onStart()
refreshOpenTracksStatus()
scheduleRefresh()
}
override fun onStop() {
super.onStop()
handler.removeCallbacksAndMessages(null)
}
private fun scheduleRefresh() {
handler.postDelayed({ refreshOpenTracksStatus(); scheduleRefresh() }, 1000)
}
private fun refreshOpenTracksStatus() {
val openTracksObserver = GBApplication.app().openTracksObserver
LOG.debug("Refreshing OpenTracks status - {}", openTracksObserver != null)
val pref = findPreference<Preference>(PREF_DEBUG_OPENTRACKS_STATUS)
if (openTracksObserver == null) {
pref?.summary = "Not running"
return
} else {
val timeSecs = openTracksObserver.getTimeMillisChange() / 1000
val distanceCM = openTracksObserver.getDistanceMeterChange() * 100
pref?.summary = "TimeSec: $timeSecs, distanceCM $distanceCM"
}
}
companion object {
val LOG: Logger = LoggerFactory.getLogger(LocationDebugFragment::class.java)
private const val PREF_HEADER_LOCATION_OPENTRACKS = "pref_header_location_opentracks"
private const val PREF_DEBUG_OPENTRACKS_STATUS = "pref_debug_opentracks_status"
private const val PREF_DEBUG_OPENTRACKS_START = "pref_debug_opentracks_start"
private const val PREF_DEBUG_OPENTRACKS_STOP = "pref_debug_opentracks_stop"
private const val PREF_HEADER_LOCATION_GPS_LISTENER = "pref_header_location_gps_listener"
private const val PREF_DEBUG_GPS_LISTENER_STOP = "pref_debug_gps_listener_stop"
}
}
@@ -0,0 +1,155 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity
import nodomain.freeyourgadget.gadgetbridge.activities.PermissionsActivity
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.util.TestDeviceDialog
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
import java.lang.Boolean
import kotlin.Any
import kotlin.String
import kotlin.let
class MainDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_main, rootKey)
onClick(PREF_DEBUG_ADD_TEST_DEVICE) {
TestDeviceDialog(requireActivity(), null).show { address, deviceType ->
TestDeviceDialog.createTestDevice(requireContext(), deviceType, address, null)
}
}
onClick(PREF_DEBUG_SHARE_LOGS) { showLogSharingWarning() }
findPreference<SwitchPreferenceCompat>(LOG_TO_FILE)?.let {
it.setOnPreferenceChangeListener { _: Preference?, newVal: Any? ->
val doEnable = Boolean.TRUE == newVal
try {
if (doEnable) {
FileUtils.getExternalFilesDir() // ensures that it is created
}
GBApplication.setupLogging(doEnable)
} catch (ex: IOException) {
GB.toast(
requireContext().applicationContext,
getString(R.string.error_creating_directory_for_logfiles, ex.localizedMessage),
Toast.LENGTH_LONG,
GB.ERROR,
ex
)
}
true
}
// If we didn't manage to initialize file logging, disable the preference
if (!GBApplication.getLogging().isFileLoggerInitialized) {
it.isEnabled = false
it.setSummary(R.string.pref_write_logfiles_not_available)
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
findPreference<Preference>(PREF_DEBUG_COMPANION_DEVICES)?.isVisible = false
}
onClick(PREF_DEBUG_ACTIVITY_CAMERA) {
val intent = Intent(requireContext().applicationContext, CameraActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra(
CameraActivity.intentExtraEvent,
GBDeviceEventCameraRemote.eventToInt(GBDeviceEventCameraRemote.Event.OPEN_CAMERA)
)
}
requireContext().startActivity(intent)
}
onClick(PREF_DEBUG_ACTIVITY_WELCOME) {
requireContext().startActivity(Intent(requireContext().applicationContext, WelcomeActivity::class.java))
}
onClick(PREF_DEBUG_ACTIVITY_PERMISSIONS) {
requireContext().startActivity(Intent(requireContext().applicationContext, PermissionsActivity::class.java))
}
}
private fun showLogSharingWarning() {
MaterialAlertDialogBuilder(requireContext())
.setCancelable(true)
.setTitle(R.string.warning)
.setMessage(R.string.share_log_warning)
.setPositiveButton(R.string.ok) { _, _ -> shareLog() }
.setNegativeButton(R.string.Cancel) { _, _ -> }
.show()
}
private fun shareLog() {
val fileName = GBApplication.getLogPath()
if (fileName == null || fileName.isEmpty()) {
return
}
// Flush the logs, so that we ensure latest lines are also there
GBApplication.getLogging().setImmediateFlush(true)
LOG.debug("Flushing logs before sharing")
GBApplication.getLogging().setImmediateFlush(false)
val logFile = File(fileName)
if (!logFile.exists()) {
GB.toast("File does not exist", Toast.LENGTH_LONG, GB.INFO)
return
}
val providerUri = FileProvider.getUriForFile(
requireContext(),
requireContext().applicationContext.packageName + ".screenshot_provider",
logFile
)
val emailIntent = Intent(Intent.ACTION_SEND)
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
emailIntent.setType("text/plain")
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Gadgetbridge log file")
emailIntent.putExtra(Intent.EXTRA_STREAM, providerUri)
startActivity(Intent.createChooser(emailIntent, "Share File"))
}
companion object {
private val LOG: Logger = LoggerFactory.getLogger(MainDebugFragment::class.java)
private const val PREF_DEBUG_ADD_TEST_DEVICE = "pref_debug_add_test_device"
private const val PREF_HEADER_LOGS = "pref_header_logs"
private const val LOG_TO_FILE = "log_to_file"
private const val PREF_DEBUG_SHARE_LOGS = "pref_debug_share_logs"
private const val PREF_HEADER_DEBUG = "pref_header_debug"
private const val PREF_DEBUG_NOTIFICATIONS = "pref_debug_notifications"
private const val PREF_DEBUG_CALLS = "pref_debug_calls"
private const val PREF_DEBUG_MUSIC = "pref_debug_music"
private const val PREF_DEBUG_WEATHER = "pref_debug_weather"
private const val PREF_DEBUG_DATABASE = "pref_debug_database"
private const val PREF_DEBUG_PREFERENCES = "pref_debug_preferences"
private const val PREF_DEBUG_COMPANION_DEVICES = "pref_debug_companion_devices"
private const val PREF_DEBUG_WIDGETS = "pref_debug_widgets"
private const val PREF_DEBUG_LOCATION = "pref_debug_location"
private const val PREF_DEBUG_ACTIVITY_LEGACY = "pref_debug_activity_legacy"
private const val PREF_HEADER_ACTIVITIES = "pref_header_activities"
private const val PREF_DEBUG_ACTIVITY_CAMERA = "pref_debug_activity_camera"
private const val PREF_DEBUG_ACTIVITY_WELCOME = "pref_debug_activity_welcome"
private const val PREF_DEBUG_ACTIVITY_PERMISSIONS = "pref_debug_activity_permissions"
}
}
@@ -0,0 +1,154 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.os.Bundle
import android.widget.Toast
import androidx.core.content.edit
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec
import nodomain.freeyourgadget.gadgetbridge.util.GB
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager
class MusicDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setupPreferences()
}
private fun setupPreferences() {
setPreferencesFromResource(R.xml.debug_preferences_music, null)
onClick(PREF_DEBUG_MUSIC_SEND_MUSICSPEC) { sendMusicSpec() }
onClick(PREF_DEBUG_MUSIC_SEND_MUSICSTATESPEC) { sendMusicStateSpec() }
onClick(PREF_DEBUG_MUSIC_PULL) { pullMusic() }
onClick(PREF_DEBUG_MUSIC_RESET) { resetPreferences() }
setInputTypeNumber(PREF_DEBUG_MUSIC_DURATION)
setInputTypeNumber(PREF_DEBUG_MUSIC_TRACKCOUNT)
setInputTypeNumber(PREF_DEBUG_MUSIC_TRACKNR)
setInputTypeNumber(PREF_DEBUG_MUSIC_POSITION)
setInputTypeNumber(PREF_DEBUG_MUSIC_PLAYRATE)
setListPreferenceEntries(PREF_DEBUG_MUSIC_STATE, arrayOf(
"STATE_UNKNOWN",
"STATE_PLAYING",
"STATE_PAUSED",
"STATE_STOPPED",
))
}
private fun sendMusicSpec() {
val sharedPreferences = preferenceManager.sharedPreferences!!
val musicSpec = MusicSpec()
musicSpec.artist = sharedPreferences.getString(PREF_DEBUG_MUSIC_ARTIST, "The Artist")
musicSpec.album = sharedPreferences.getString(PREF_DEBUG_MUSIC_ALBUM, "The Album")
musicSpec.track = sharedPreferences.getString(PREF_DEBUG_MUSIC_TRACK, "The Track Name")
musicSpec.duration = sharedPreferences.getString(PREF_DEBUG_MUSIC_DURATION, "120")!!.toInt()
musicSpec.trackCount = sharedPreferences.getString(PREF_DEBUG_MUSIC_TRACKCOUNT, "5")!!.toInt()
musicSpec.trackNr = sharedPreferences.getString(PREF_DEBUG_MUSIC_TRACKNR, "2")!!.toInt()
runOnDebugDevices("Send MusicSpec") {
GBApplication.deviceService(it).onSetMusicInfo(musicSpec)
}
}
private fun sendMusicStateSpec() {
val sharedPreferences = preferenceManager.sharedPreferences!!
val stateSpec = MusicStateSpec()
stateSpec.state = when (sharedPreferences.getString(PREF_DEBUG_MUSIC_STATE, "STATE_PLAYING")) {
"STATE_PLAYING" -> 0
"STATE_PAUSED" -> 1
"STATE_STOPPED" -> 2
"STATE_UNKNOWN" -> -1
else -> -1
}
stateSpec.position = sharedPreferences.getString(PREF_DEBUG_MUSIC_POSITION, "30")!!.toInt()
stateSpec.playRate = sharedPreferences.getString(PREF_DEBUG_MUSIC_PLAYRATE, "100")!!.toInt()
stateSpec.shuffle = if (sharedPreferences.getBoolean(PREF_DEBUG_MUSIC_SHUFFLE_BOOL, false)) 1 else 0
stateSpec.repeat = if (sharedPreferences.getBoolean(PREF_DEBUG_MUSIC_REPEAT_BOOL, false)) 1 else 0
runOnDebugDevices("Send MusicStateSpec") {
GBApplication.deviceService(it).onSetMusicState(stateSpec)
}
}
private fun pullMusic() {
val mediaManager = MediaManager(requireContext())
mediaManager.refresh()
if (mediaManager.bufferMusicSpec == null || mediaManager.bufferMusicStateSpec == null) {
GB.toast(requireContext(), "No media playing?", Toast.LENGTH_SHORT, GB.ERROR)
}
preferenceManager.sharedPreferences!!.edit {
mediaManager.bufferMusicSpec?.let {
putString(PREF_DEBUG_MUSIC_ARTIST, it.artist)
putString(PREF_DEBUG_MUSIC_ALBUM, it.album)
putString(PREF_DEBUG_MUSIC_TRACK, it.track)
putString(PREF_DEBUG_MUSIC_DURATION, it.duration.toString())
putString(PREF_DEBUG_MUSIC_TRACKCOUNT, it.trackCount.toString())
putString(PREF_DEBUG_MUSIC_TRACKNR, it.trackNr.toString())
}
mediaManager.bufferMusicStateSpec?.let {
putString(
PREF_DEBUG_MUSIC_STATE, when (it.state) {
0.toByte() -> "STATE_PLAYING"
1.toByte() -> "STATE_PAUSED"
2.toByte() -> "STATE_STOPPED"
else -> "STATE_UNKNOWN"
}
)
putString(PREF_DEBUG_MUSIC_POSITION, it.position.toString())
putString(PREF_DEBUG_MUSIC_PLAYRATE, it.playRate.toString())
putBoolean(PREF_DEBUG_MUSIC_SHUFFLE_BOOL, it.shuffle != 0.toByte())
putBoolean(PREF_DEBUG_MUSIC_REPEAT_BOOL, it.repeat != 0.toByte())
}
}
}
private fun resetPreferences() {
preferenceScreen.removeAll()
preferenceManager.sharedPreferences!!.edit(true) {
remove(PREF_HEADER_MUSICSPEC)
remove(PREF_DEBUG_MUSIC_ARTIST)
remove(PREF_DEBUG_MUSIC_ALBUM)
remove(PREF_DEBUG_MUSIC_TRACK)
remove(PREF_DEBUG_MUSIC_DURATION)
remove(PREF_DEBUG_MUSIC_TRACKCOUNT)
remove(PREF_DEBUG_MUSIC_TRACKNR)
remove(PREF_HEADER_MUSICSTATESPEC)
remove(PREF_DEBUG_MUSIC_STATE)
remove(PREF_DEBUG_MUSIC_POSITION)
remove(PREF_DEBUG_MUSIC_PLAYRATE)
remove(PREF_DEBUG_MUSIC_SHUFFLE_BOOL)
remove(PREF_DEBUG_MUSIC_REPEAT_BOOL)
}
// Reload the preference screen to reflect the changes
setupPreferences()
}
companion object {
private const val PREF_DEBUG_MUSIC_SEND_MUSICSPEC = "pref_debug_music_send_musicspec"
private const val PREF_DEBUG_MUSIC_SEND_MUSICSTATESPEC = "pref_debug_music_send_musicstatespec"
private const val PREF_DEBUG_MUSIC_PULL = "pref_debug_music_pull"
private const val PREF_DEBUG_MUSIC_RESET = "pref_debug_music_reset"
private const val PREF_HEADER_MUSICSPEC = "pref_header_musicspec"
private const val PREF_DEBUG_MUSIC_ARTIST = "pref_debug_music_artist"
private const val PREF_DEBUG_MUSIC_ALBUM = "pref_debug_music_album"
private const val PREF_DEBUG_MUSIC_TRACK = "pref_debug_music_track"
private const val PREF_DEBUG_MUSIC_DURATION = "pref_debug_music_duration"
private const val PREF_DEBUG_MUSIC_TRACKCOUNT = "pref_debug_music_trackCount"
private const val PREF_DEBUG_MUSIC_TRACKNR = "pref_debug_music_trackNr"
private const val PREF_HEADER_MUSICSTATESPEC = "pref_header_musicstatespec"
private const val PREF_DEBUG_MUSIC_STATE = "pref_debug_music_state"
private const val PREF_DEBUG_MUSIC_POSITION = "pref_debug_music_position"
private const val PREF_DEBUG_MUSIC_PLAYRATE = "pref_debug_music_playRate"
private const val PREF_DEBUG_MUSIC_SHUFFLE_BOOL = "pref_debug_music_shuffle_bool"
private const val PREF_DEBUG_MUSIC_REPEAT_BOOL = "pref_debug_music_repeat_bool"
}
}
@@ -0,0 +1,274 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.preference.ListPreference
import nodomain.freeyourgadget.gadgetbridge.BuildConfig
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType
import nodomain.freeyourgadget.gadgetbridge.util.GB
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.util.Objects
class NotificationsDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setupPreferences()
}
private fun setupPreferences() {
setPreferencesFromResource(R.xml.debug_preferences_notifications, null)
onClick(PREF_DEBUG_NOTIFICATIONS_SEND) { sendNotificationSpec() }
onClick(PREF_DEBUG_PEBBLEKIT_NOTIFICATION) { testPebbleKitNotification() }
onClick(PREF_DEBUG_CREATE_TEST_NOTIFICATION) { createTestNotification() }
onClick(PREF_DEBUG_NOTIFICATIONS_RESET) { resetPreferences() }
val callCommandPref = findPreference<ListPreference>(PREF_DEBUG_NOTIFICATIONS_TYPE)!!
callCommandPref.entries = NotificationType.sortedValues().map { it.name }.toList().toTypedArray()
callCommandPref.entryValues = callCommandPref.entries
}
override fun onStart() {
super.onStart()
val filter = IntentFilter()
filter.addAction(ACTION_REPLY)
ContextCompat.registerReceiver(requireContext(), mReceiver, filter, ContextCompat.RECEIVER_EXPORTED)
}
override fun onStop() {
super.onStop()
try {
requireContext().unregisterReceiver(mReceiver)
} catch (e: Exception) {
LOG.error("Failed to unregister receiver", e)
}
}
private fun sendNotificationSpec() {
val sharedPreferences = preferenceManager.sharedPreferences!!
val notificationSpec = NotificationSpec()
notificationSpec.flags = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_FLAGS, "0")!!.toInt()
notificationSpec.key = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_KEY, "0|nodomain.freeyourgadget.gadgetbridge|-493519667|null|10679")
notificationSpec.sender = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_SENDER, null)
notificationSpec.phoneNumber = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_PHONENUMBER, null)
notificationSpec.title = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_TITLE, "Title")
notificationSpec.subject = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_SUBJECT, "Subject")
notificationSpec.body = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_BODY, "Notification body")
notificationSpec.type = NotificationType.valueOf(sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_TYPE, "UNKNOWN")!!)
notificationSpec.sourceName = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_SOURCENAME, "Gadgetbridge")
if (notificationSpec.type != NotificationType.GENERIC_SMS) {
// SMS notifications don't have a source app ID when sent by the SMSReceiver,
// so let's not set it here as well for consistency
notificationSpec.sourceAppId = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_SOURCEAPPID, BuildConfig.APPLICATION_ID)
}
notificationSpec.channelId = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_CHANNELID, "the_channel")
notificationSpec.category = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_CATEGORY, null)
notificationSpec.iconId = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_ICONID, "0")!!.toInt()
if (sharedPreferences.getBoolean(PREF_DEBUG_NOTIFICATIONS_PICTUREPATH_BOOL, false)) {
notificationSpec.picturePath = getTestPicture()?.absolutePath
}
notificationSpec.dndSuppressed = if (sharedPreferences.getBoolean(PREF_DEBUG_NOTIFICATIONS_DNDSUPPRESSED, false)) 1 else 0
// TODO notificationSpec.cannedReplies = sharedPreferences.getString(PREF_DEBUG_NOTIFICATIONS_CANNEDREPLIES, null)
notificationSpec.attachedActions = ArrayList<NotificationSpec.Action?>()
// DISMISS action
val dismissAction = NotificationSpec.Action()
dismissAction.title = getString(R.string.dismiss)
dismissAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS
notificationSpec.attachedActions.add(dismissAction)
if (sharedPreferences.getBoolean(PREF_DEBUG_NOTIFICATIONS_ATTACHEDACTIONS_REPLY, false)) {
// REPLY action
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
val replyAction = NotificationSpec.Action()
replyAction.title = getString(R.string._pebble_watch_reply)
replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR
notificationSpec.attachedActions.add(replyAction)
} else {
val replyAction = NotificationSpec.Action()
replyAction.title = getString(R.string._pebble_watch_reply)
replyAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY
notificationSpec.attachedActions.add(replyAction)
}
}
runOnDebugDevices("Send NotificationSpec") {
GBApplication.deviceService(it).onNotification(notificationSpec)
}
}
private fun resetPreferences() {
preferenceScreen.removeAll()
preferenceManager.sharedPreferences!!.edit(true) {
remove(PREF_DEBUG_NOTIFICATIONS_FLAGS)
remove(PREF_DEBUG_NOTIFICATIONS_KEY)
remove(PREF_DEBUG_NOTIFICATIONS_SENDER)
remove(PREF_DEBUG_NOTIFICATIONS_PHONENUMBER)
remove(PREF_DEBUG_NOTIFICATIONS_TITLE)
remove(PREF_DEBUG_NOTIFICATIONS_SUBJECT)
remove(PREF_DEBUG_NOTIFICATIONS_BODY)
remove(PREF_DEBUG_NOTIFICATIONS_TYPE)
remove(PREF_DEBUG_NOTIFICATIONS_SOURCENAME)
remove(PREF_DEBUG_NOTIFICATIONS_CHANNELID)
remove(PREF_DEBUG_NOTIFICATIONS_CATEGORY)
remove(PREF_DEBUG_NOTIFICATIONS_ATTACHEDACTIONS_REPLY)
remove(PREF_DEBUG_NOTIFICATIONS_SOURCEAPPID)
remove(PREF_DEBUG_NOTIFICATIONS_ICONID)
remove(PREF_DEBUG_NOTIFICATIONS_PICTUREPATH_BOOL)
}
// Reload the preference screen to reflect the changes
setupPreferences()
}
private fun createTestNotification() {
val notificationIntent = Intent(requireContext(), DebugActivityV2::class.java)
notificationIntent.setPackage(BuildConfig.APPLICATION_ID)
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
val pendingIntent = PendingIntent.getActivity(requireContext(), 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
val remoteInput = RemoteInput.Builder(EXTRA_REPLY).build()
val replyIntent = Intent(ACTION_REPLY)
replyIntent.setPackage(BuildConfig.APPLICATION_ID)
val replyPendingIntent = PendingIntent.getBroadcast(requireContext(), 0, replyIntent, PendingIntent.FLAG_MUTABLE)
val action = NotificationCompat.Action.Builder(android.R.drawable.ic_input_add, "Reply", replyPendingIntent)
.addRemoteInput(remoteInput)
.build()
val wearableExtender = NotificationCompat.WearableExtender().addAction(action)
val pictureUri = getTestPicture()?.let {
@SuppressLint("SetWorldReadable") it.setReadable(true, false)
FileProvider.getUriForFile(
requireContext(),
"${BuildConfig.APPLICATION_ID}.screenshot_provider",
it
)
}
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(requireContext(), GB.NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.test_notification))
.setContentText(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
.setTicker(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.extend(wearableExtender)
if (pictureUri != null) {
try {
val bitmap = BitmapFactory.decodeStream(requireContext().contentResolver.openInputStream(pictureUri))
builder.setLargeIcon(bitmap)
.setStyle(NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null as android.graphics.Bitmap?))
} catch (e: Exception) {
LOG.error("Failed to decode image for notification", e)
}
}
GB.notify(System.currentTimeMillis().toInt(), builder.build(), requireContext())
}
private fun getTestPicture(): File? {
try {
requireContext().assets.open("fossil_hr/default_background.png").use {
val tempDir = File(requireContext().cacheDir, "images")
tempDir.mkdir()
val tempFile = File(tempDir, "test_notification_image.png")
tempFile.outputStream().use { output ->
it.copyTo(output)
}
return tempFile
}
} catch (e: Exception) {
LOG.error("Failed to copy test picture to cache", e)
GB.toast("Test picture failed", Toast.LENGTH_SHORT, GB.ERROR)
}
return null
}
private fun testPebbleKitNotification() {
val pebbleKitIntent = Intent("com.getpebble.action.SEND_NOTIFICATION")
pebbleKitIntent.putExtra("messageType", "PEBBLE_ALERT")
pebbleKitIntent.putExtra(
"notificationData",
"""
[{
"title": "PebbleKitTest",
"body": "Sent from Gadgetbridge"
}]
""".trimIndent()
)
GBApplication.getContext().sendBroadcast(pebbleKitIntent)
}
private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
when (Objects.requireNonNull(intent.action)) {
ACTION_REPLY -> {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
val reply = remoteInput!!.getCharSequence(EXTRA_REPLY)
LOG.info("Got wearable reply: $reply")
GB.toast(context, "Reply: $reply", Toast.LENGTH_SHORT, GB.INFO)
}
else -> LOG.warn("Unknown intent action: {}", intent.action)
}
}
}
companion object {
private val LOG: Logger = LoggerFactory.getLogger(DebugActivityV2::class.java)
private const val EXTRA_REPLY = "reply"
private const val ACTION_REPLY = "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply"
private const val PREF_DEBUG_NOTIFICATIONS_SEND = "pref_debug_notifications_send"
private const val PREF_DEBUG_PEBBLEKIT_NOTIFICATION = "pref_debug_pebblekit_notification"
private const val PREF_DEBUG_CREATE_TEST_NOTIFICATION = "pref_debug_create_test_notification"
private const val PREF_DEBUG_NOTIFICATIONS_RESET = "pref_debug_notifications_reset"
private const val PREF_DEBUG_HEADER_CALLSPEC = "pref_header_callspec"
private const val PREF_DEBUG_NOTIFICATIONS_FLAGS = "pref_debug_notifications_flags"
private const val PREF_DEBUG_NOTIFICATIONS_KEY = "pref_debug_notifications_key"
private const val PREF_DEBUG_NOTIFICATIONS_SENDER = "pref_debug_notifications_sender"
private const val PREF_DEBUG_NOTIFICATIONS_PHONENUMBER = "pref_debug_notifications_phoneNumber"
private const val PREF_DEBUG_NOTIFICATIONS_TITLE = "pref_debug_notifications_title"
private const val PREF_DEBUG_NOTIFICATIONS_SUBJECT = "pref_debug_notifications_subject"
private const val PREF_DEBUG_NOTIFICATIONS_BODY = "pref_debug_notifications_body"
private const val PREF_DEBUG_NOTIFICATIONS_TYPE = "pref_debug_notifications_type"
private const val PREF_DEBUG_NOTIFICATIONS_SOURCENAME = "pref_debug_notifications_sourceName"
private const val PREF_DEBUG_NOTIFICATIONS_CHANNELID = "pref_debug_notifications_channelId"
private const val PREF_DEBUG_NOTIFICATIONS_CATEGORY = "pref_debug_notifications_category"
private const val PREF_DEBUG_NOTIFICATIONS_ATTACHEDACTIONS_REPLY = "pref_debug_notifications_attachedActions_reply"
private const val PREF_DEBUG_NOTIFICATIONS_SOURCEAPPID = "pref_debug_notifications_sourceAppId"
private const val PREF_DEBUG_NOTIFICATIONS_ICONID = "pref_debug_notifications_iconId"
private const val PREF_DEBUG_NOTIFICATIONS_PICTUREPATH_BOOL = "pref_debug_notifications_picturePath_bool"
private const val PREF_DEBUG_NOTIFICATIONS_DNDSUPPRESSED = "pref_debug_notifications_dndSuppressed"
}
}
@@ -0,0 +1,124 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.app.DatePickerDialog
import android.os.Bundle
import android.widget.DatePicker
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol
import nodomain.freeyourgadget.gadgetbridge.util.GB
import java.util.Calendar
import androidx.core.content.edit
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
class OtherDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setupPreferences()
}
private fun setupPreferences() {
setPreferencesFromResource(R.xml.debug_preferences_other_actions, null)
onClick(PREF_DEBUG_SET_TIME) {
runOnDebugDevices("Set time") {
GBApplication.deviceService(it).onSetTime()
}
}
onClick(PREF_DEBUG_SET_ACTIVITY_FETCH_TIME) {
val currentDate = Calendar.getInstance()
runOnDebugDevices {
DatePickerDialog(
requireActivity(),
{ _: DatePicker?, year: Int, monthOfYear: Int, dayOfMonth: Int ->
val date = DateTimeUtils.dayStart(year, monthOfYear, dayOfMonth)
GB.toast("Fetch time: ${date.time}", Toast.LENGTH_LONG, GB.INFO)
GBApplication.getDeviceSpecificSharedPrefs(it.address).edit {
//FIXME: key reconstruction is BAD
remove("lastSyncTimeMillis")
remove("lastTemperatureTimeMillis")
remove("lastStressManualTimeMillis")
remove("lastStressAutoTimeMillis")
remove("lastDebugTimeMillis")
remove("lastStatisticsTimeMillis")
remove("lastSportsActivityTimeMillis")
remove("lastSpo2sleepTimeMillis")
remove("lastSpo2normalTimeMillis")
remove("lastSleepSessionTimeMillis")
remove("lastPaiTimeMillis")
remove("lastHeartRateManualTimeMillis")
remove("lastHeartRateRestingTimeMillis")
remove("lastHrvTimeMillis")
remove("lastHeartRateMaxTimeMillis")
remove("lastSleepRespiratoryRateTimeMillis")
putLong("lastSyncTimeMillis", date.time)
putLong("lastTemperatureTimeMillis", date.time)
putLong("lastStressManualTimeMillis", date.time)
putLong("lastStressAutoTimeMillis", date.time)
putLong("lastDebugTimeMillis", date.time)
putLong("lastStatisticsTimeMillis", date.time)
putLong("lastSportsActivityTimeMillis", date.time)
putLong("lastSpo2sleepTimeMillis", date.time)
putLong("lastSpo2normalTimeMillis", date.time)
putLong("lastSleepSessionTimeMillis", date.time)
putLong("lastPaiTimeMillis", date.time)
putLong("lastHeartRateManualTimeMillis", date.time)
putLong("lastHeartRateRestingTimeMillis", date.time)
putLong("lastHrvTimeMillis", date.time)
putLong("lastHeartRateMaxTimeMillis", date.time)
putLong("lastSleepRespiratoryRateTimeMillis", date.time)
}
},
currentDate.get(Calendar.YEAR),
currentDate.get(Calendar.MONTH),
currentDate.get(
Calendar.DATE
)
).show()
}
}
onClick(PREF_DEBUG_FETCH_DEBUG_LOGS) {
runOnDebugDevices("Fetch debug logs", true) {
GBApplication.deviceService(it).onFetchRecordedData(RecordedDataTypes.TYPE_DEBUGLOGS)
}
}
onClick(PREF_DEBUG_REBOOT) {
runOnDebugDevices("Reboot", true) {
GBApplication.deviceService(it).onReset(GBDeviceProtocol.RESET_FLAGS_REBOOT)
}
}
onClick(PREF_DEBUG_FACTORY_RESET) {
MaterialAlertDialogBuilder(requireActivity())
.setCancelable(true)
.setTitle(R.string.debugactivity_really_factoryreset_title)
.setMessage(R.string.debugactivity_really_factoryreset)
.setPositiveButton(R.string.ok) { _, _ ->
runOnDebugDevices(getString(R.string.factory_reset), true) {
GBApplication.deviceService(it).onReset(GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET)
}
}
.setNegativeButton(R.string.Cancel) { _, _ -> }
.show()
}
}
companion object {
private const val PREF_DEBUG_SET_TIME = "pref_debug_set_time"
private const val ACTIVITY_LIST_DEBUG_EXTRA_TIME_RANGE = "activity_list_debug_extra_time_range"
private const val PREF_DEBUG_HEADER_FETCH_RECORDED_DATA = "pref_debug_header_fetch_recorded_data"
private const val PREF_DEBUG_SET_ACTIVITY_FETCH_TIME = "pref_debug_set_activity_fetch_time"
private const val PREF_DEBUG_FETCH_DEBUG_LOGS = "pref_debug_fetch_debug_logs"
private const val PREF_DEBUG_HEADER_RESET = "pref_debug_header_reset"
private const val PREF_DEBUG_REBOOT = "pref_debug_reboot"
private const val PREF_DEBUG_FACTORY_RESET = "pref_debug_factory_reset"
}
}
@@ -0,0 +1,54 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.content.Intent
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.debug.preferences.PreferenceManagerActivity
class PreferencesDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_preferences, rootKey)
findPreference<Preference>(PREF_DEBUG_PREFERENCES_VERSION)!!.summary = GBApplication.getPrefs().getString(
GBApplication.PREFS_VERSION,
getString(R.string.unknown)
)
onClick(PREF_DEBUG_GLOBAL_PREFERENCES) {
editPreferences(
requireContext().packageName + "_preferences",
requireContext().getString(R.string.global_preferences)
)
}
val devicePreferences = findPreference<PreferenceCategory>(PREF_HEADER_DEVICE_PREFERENCES)!!
removeDynamicPrefs(devicePreferences)
for (device in GBApplication.app().deviceManager.devices.sortedBy { it.aliasOrName }) {
addDynamicPref(
devicePreferences,
device.aliasOrName,
device.address,
device.deviceCoordinator.defaultIconResource
) {
editPreferences("devicesettings_" + device.address.toString().uppercase(), device.aliasOrName)
}
}
}
private fun editPreferences(name: String, title: String) {
val intent = Intent(requireContext(), PreferenceManagerActivity::class.java)
intent.putExtra(PreferenceManagerActivity.EXTRA_NAME, name)
intent.putExtra(PreferenceManagerActivity.EXTRA_TITLE, title)
requireContext().startActivity(intent)
}
companion object {
private const val PREF_DEBUG_PREFERENCES_VERSION = "pref_debug_preferences_version";
private const val PREF_DEBUG_GLOBAL_PREFERENCES = "pref_debug_global_preferences";
private const val PREF_HEADER_DEVICE_PREFERENCES = "pref_header_device_preferences";
}
}
@@ -0,0 +1,168 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.content.DialogInterface
import android.os.Bundle
import android.widget.Toast
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.adapter.SimpleIconListAdapter
import nodomain.freeyourgadget.gadgetbridge.model.RunnableListIconItem
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather
import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherCacheManager
import nodomain.freeyourgadget.gadgetbridge.util.GB
import java.lang.Boolean
import kotlin.Any
import kotlin.Int
import kotlin.String
import kotlin.apply
import kotlin.plus
class WeatherDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_weather, rootKey)
onClick(PREF_DEBUG_WEATHER_SEND) {
if (Weather.getWeatherSpecs().isEmpty()) {
GB.toast(requireContext(), "No cached weather to send", Toast.LENGTH_SHORT, GB.ERROR)
return@onClick
}
runOnDebugDevices("Send weather to devices") {
GBApplication.deviceService(it).onSendWeather()
}
}
onClick(PREF_DEBUG_WEATHER_ADD_TEST) {
val weatherSpec = WeatherSpec.createTestWeather()
if (!Weather.getWeatherSpecs().isEmpty()) {
weatherSpec.location += " (${Weather.getWeatherSpecs().size})"
}
Weather.setWeatherSpec((Weather.getWeatherSpecs() + weatherSpec).toMutableList())
reloadCachedWeather()
}
findPreference<Preference>(CACHE_WEATHER)!!.setOnPreferenceChangeListener { _: Preference?, newVal: Any? ->
val doEnable = Boolean.TRUE == newVal
Weather.initializeCache(WeatherCacheManager(requireContext().cacheDir, doEnable))
true
}
reloadCachedWeather()
}
override fun onResume() {
super.onResume()
reloadCachedWeather()
}
private fun reloadCachedWeather() {
val cachedWeatherHeader: PreferenceCategory = findPreference(PREF_HEADER_CACHED_WEATHER)!!
val weatherSpecs: List<WeatherSpec> = Weather.getWeatherSpecs()
removeDynamicPrefs(cachedWeatherHeader)
if (weatherSpecs.isEmpty()) {
addDynamicPref(cachedWeatherHeader, "", "No cached weather")
return
}
for ((i, weatherSpec) in weatherSpecs.withIndex()) {
addDynamicPref(
group = cachedWeatherHeader,
title = weatherSpec.location ?: "Unknown location",
icon = R.drawable.ic_wb_sunny,
onClickFunction = { onWeatherClick(weatherSpecs, i) }
)
}
}
fun <T> List<T>.moveTo(from: Int, to: Int): List<T> {
val target = to.coerceIn(indices)
if (from == target) return this
return toMutableList().apply {
add(target, removeAt(from))
}
}
private fun onWeatherClick(weatherSpecs: List<WeatherSpec>, i: Int) {
val weatherSpec = weatherSpecs[i]
val items: MutableList<RunnableListIconItem?> = ArrayList(4)
// Show details
items.add(
RunnableListIconItem(
"Show details",
R.drawable.ic_wb_sunny
) {
goTo(
WeatherSpecDebugFragment().apply {
arguments = Bundle().apply { putParcelable("weatherSpec", weatherSpec) }
}
)
}
)
// Move up
if (i > 0) {
items.add(
RunnableListIconItem(
requireContext().getString(R.string.widget_move_up),
R.drawable.ic_arrow_upward
) {
Weather.setWeatherSpec(Weather.getWeatherSpecs().moveTo(i, i - 1))
reloadCachedWeather()
}
)
}
// Move down
if (i < weatherSpecs.size - 1) {
items.add(
RunnableListIconItem(
requireContext().getString(R.string.widget_move_down),
R.drawable.ic_arrow_downward
) {
Weather.setWeatherSpec(Weather.getWeatherSpecs().moveTo(i, i + 1))
reloadCachedWeather()
}
)
}
// Delete
items.add(
RunnableListIconItem(
requireContext().getString(R.string.Delete),
R.drawable.ic_delete
) {
val newList = Weather.getWeatherSpecs().toMutableList()
newList.removeAt(i)
Weather.setWeatherSpec(newList)
reloadCachedWeather()
}
)
val adapter = SimpleIconListAdapter(context, items)
MaterialAlertDialogBuilder(requireContext())
.setAdapter(adapter) { _: DialogInterface?, i1: Int -> items[i1]!!.action.run() }
.setTitle(weatherSpec.location)
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> }
.create()
.show()
}
companion object {
private const val PREF_HEADER_WEATHER = "pref_header_weather"
private const val PREF_DEBUG_WEATHER_SEND = "pref_debug_weather_send"
private const val CACHE_WEATHER = "cache_weather"
private const val PREF_DEBUG_WEATHER_ADD_TEST = "pref_debug_weather_add_test"
private const val PREF_HEADER_CACHED_WEATHER = "pref_header_cached_weather"
}
}
@@ -0,0 +1,178 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.os.Bundle
import androidx.preference.PreferenceCategory
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
class WeatherSpecDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_empty, rootKey)
@Suppress("DEPRECATION")
val weatherSpec = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable("weatherSpec", WeatherSpec::class.java)!!
} else {
arguments?.getParcelable<WeatherSpec>("weatherSpec")!!
}
val day = arguments?.getBoolean("days", false)
val hour = arguments?.getBoolean("hours", false)
preferenceScreen?.title = weatherSpec.location
if (day == true) {
showDays(weatherSpec)
} else if (hour == true) {
showHours(weatherSpec)
} else {
showMainWeather(weatherSpec)
}
}
private fun addPreference(title: String, summary: Any?, onClickFunction: (() -> Unit)? = null) {
addDynamicPref(preferenceScreen, title, summary?.toString() ?: "<null>", R.drawable.ic_widgets) {
onClickFunction?.invoke()
}
}
private fun addCategory(title: String) {
val pref = PreferenceCategory(requireContext())
pref.key = "${PREF_DYNAMIC_PREFIX}_category_${UUID.randomUUID()})"
pref.title = title
pref.isPersistent = false
pref.isIconSpaceReserved = false
preferenceScreen?.addPreference(pref)
}
private fun showMainWeather(weatherSpec: WeatherSpec) {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)
addPreference("Location", weatherSpec.location)
addPreference("Location", weatherSpec.location)
addPreference("Timestamp", sdf.format(Date(weatherSpec.timestamp * 1000L)))
addPreference("Current Temp", "${weatherSpec.currentTemp} K (${weatherSpec.currentTemp - 273} °C)")
addPreference("Max Temp", "${weatherSpec.todayMaxTemp} K (${weatherSpec.todayMaxTemp - 273} °C)")
addPreference("Min Temp", "${weatherSpec.todayMinTemp} K (${weatherSpec.todayMinTemp - 273} °C)")
addPreference("Condition", weatherSpec.currentCondition)
addPreference("Condition Code", weatherSpec.currentConditionCode)
addPreference("Humidity", weatherSpec.currentHumidity)
addPreference("Wind Speed", "${weatherSpec.windSpeed} kmph")
addPreference("Wind Direction", "${weatherSpec.windDirection} deg")
addPreference("UV Index", weatherSpec.uvIndex)
addPreference("Precip Probability", "${weatherSpec.precipProbability} %")
addPreference("Dew Point", "${weatherSpec.dewPoint} K (${weatherSpec.dewPoint - 273} °C)")
addPreference("Pressure", "${weatherSpec.pressure} mb")
addPreference("Cloud Cover", "${weatherSpec.cloudCover} %")
addPreference("Visibility", "${weatherSpec.visibility} m")
addPreference("Sun Rise", sdf.format(Date(weatherSpec.sunRise * 1000L)))
addPreference("Sun Set", sdf.format(Date(weatherSpec.sunSet * 1000L)))
addPreference("Moon Rise", sdf.format(Date(weatherSpec.moonRise * 1000L)))
addPreference("Moon Set", sdf.format(Date(weatherSpec.moonSet * 1000L)))
addPreference("Moon Phase", "${weatherSpec.moonPhase} deg")
addPreference("Latitude", weatherSpec.latitude)
addPreference("Longitude", weatherSpec.longitude)
addPreference("Feels Like Temp", "${weatherSpec.feelsLikeTemp} K (${weatherSpec.feelsLikeTemp - 273} °C)")
addPreference("Is Current Location", weatherSpec.getIsCurrentLocation())
if (weatherSpec.airQuality != null) {
addPreference("Air Quality aqi", weatherSpec.airQuality!!.aqi)
addPreference("Air Quality co", weatherSpec.airQuality!!.co)
addPreference("Air Quality no2", weatherSpec.airQuality!!.no2)
addPreference("Air Quality o3", weatherSpec.airQuality!!.o3)
addPreference("Air Quality pm10", weatherSpec.airQuality!!.pm10)
addPreference("Air Quality pm25", weatherSpec.airQuality!!.pm25)
addPreference("Air Quality so2", weatherSpec.airQuality!!.so2)
addPreference("Air Quality coAqi", weatherSpec.airQuality!!.coAqi)
addPreference("Air Quality no2Aqi", weatherSpec.airQuality!!.no2Aqi)
addPreference("Air Quality o3Aqi", weatherSpec.airQuality!!.o3Aqi)
addPreference("Air Quality pm10Aqi", weatherSpec.airQuality!!.pm10Aqi)
addPreference("Air Quality pm25Aqi", weatherSpec.airQuality!!.pm25Aqi)
addPreference("Air Quality so2Aqi", weatherSpec.airQuality!!.so2Aqi)
} else {
addPreference("Air Quality", null)
}
addPreference("Daily Forecasts", "${weatherSpec.forecasts.size} entries") {
if (weatherSpec.forecasts.isNotEmpty()) {
goTo(
WeatherSpecDebugFragment().apply {
arguments = Bundle().apply {
putParcelable("weatherSpec", weatherSpec)
putBoolean("days", true)
}
}
)
}
}
addPreference("Hourly Forecasts", "${weatherSpec.hourly.size} entries") {
if (weatherSpec.hourly.isNotEmpty()) {
goTo(
WeatherSpecDebugFragment().apply {
arguments = Bundle().apply {
putParcelable("weatherSpec", weatherSpec)
putBoolean("hours", true)
}
}
)
}
}
}
private fun showDays(weatherSpec: WeatherSpec) {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)
for ((i, daily) in weatherSpec.forecasts.withIndex()) {
addCategory("Day $i")
addPreference("Max Temp", "${daily.maxTemp} K (${daily.maxTemp - 273} °C)")
addPreference("Min Temp", "${daily.minTemp} K (${daily.minTemp - 273} °C)")
addPreference("Condition Code", daily.conditionCode)
addPreference("Humidity", daily.humidity)
addPreference("Wind Speed", "${daily.windSpeed} kmph")
addPreference("Wind Direction", "${daily.windDirection} deg")
addPreference("UV Index", daily.uvIndex)
addPreference("Precip Probability", "${daily.precipProbability} %")
addPreference("Sun Rise", sdf.format(Date(daily.sunRise * 1000L)))
addPreference("Sun Set", sdf.format(Date(daily.sunSet * 1000L)))
addPreference("Moon Rise", sdf.format(Date(daily.moonRise * 1000L)))
addPreference("Moon Set", sdf.format(Date(daily.moonSet * 1000L)))
addPreference("Moon Phase", "${daily.moonPhase} deg")
if (daily.airQuality != null) {
addPreference("Air Quality aqi", daily.airQuality!!.aqi)
addPreference("Air Quality co", daily.airQuality!!.co)
addPreference("Air Quality no2", daily.airQuality!!.no2)
addPreference("Air Quality o3", daily.airQuality!!.o3)
addPreference("Air Quality pm10", daily.airQuality!!.pm10)
addPreference("Air Quality pm25", daily.airQuality!!.pm25)
addPreference("Air Quality so2", daily.airQuality!!.so2)
addPreference("Air Quality coAqi", daily.airQuality!!.coAqi)
addPreference("Air Quality no2Aqi", daily.airQuality!!.no2Aqi)
addPreference("Air Quality o3Aqi", daily.airQuality!!.o3Aqi)
addPreference("Air Quality pm10Aqi", daily.airQuality!!.pm10Aqi)
addPreference("Air Quality pm25Aqi", daily.airQuality!!.pm25Aqi)
addPreference("Air Quality so2Aqi", daily.airQuality!!.so2Aqi)
} else {
addPreference("Air Quality", null)
}
}
}
private fun showHours(weatherSpec: WeatherSpec) {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)
for ((i, hourly) in weatherSpec.hourly.withIndex()) {
addCategory("Hour $i - ${sdf.format(Date(hourly.timestamp * 1000L))}")
addPreference("Max Temp", "${hourly.temp} K (${hourly.temp - 273} °C)")
addPreference("Condition Code", hourly.conditionCode)
addPreference("Humidity", hourly.humidity)
addPreference("Wind Speed", "${hourly.windSpeed} kmph")
addPreference("Wind Direction", "${hourly.windDirection} deg")
addPreference("UV Index", hourly.uvIndex)
addPreference("Precip Probability", "${hourly.precipProbability} %")
}
}
}
@@ -0,0 +1,98 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import androidx.preference.PreferenceCategory
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.Widget
import nodomain.freeyourgadget.gadgetbridge.adapter.SimpleIconListAdapter
import nodomain.freeyourgadget.gadgetbridge.model.RunnableListIconItem
import nodomain.freeyourgadget.gadgetbridge.util.WidgetPreferenceStorage
class WidgetsDebugFragment : AbstractDebugFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.debug_preferences_widgets, rootKey)
onClick(PREF_DEBUG_WIDGETS_PREFS_SHOW) { showAppWidgetsPrefs() }
onClick(PREF_DEBUG_WIDGETS_PREFS_DELETE) { deleteWidgetsPrefs() }
reloadWidgets()
}
private fun reloadWidgets() {
val widgetsHeader: PreferenceCategory = findPreference(PREF_HEADER_WIDGETS)!!
removeDynamicPrefs(widgetsHeader)
// https://stackoverflow.com/questions/17387191/check-if-a-widget-is-exists-on-homescreen-using-appwidgetid
val appWidgetManager = AppWidgetManager.getInstance(requireContext())
val appWidgetHost = AppWidgetHost(requireContext(), 1) // for removing phantoms
val appWidgetIDs = appWidgetManager.getAppWidgetIds(ComponentName(requireContext(), Widget::class.java))
if (appWidgetIDs.isEmpty()) {
addDynamicPref(widgetsHeader, "", "No widgets found")
return
}
for ((i, appWidgetID) in appWidgetIDs.withIndex()) {
addDynamicPref(widgetsHeader, "Widget $i", "id = $appWidgetID") {
val items: MutableList<RunnableListIconItem?> = ArrayList(4)
items.add(
RunnableListIconItem(getString(R.string.Delete), R.drawable.ic_delete_forever) {
appWidgetHost.deleteAppWidgetId(appWidgetID)
reloadWidgets()
}
)
val adapter = SimpleIconListAdapter(context, items)
MaterialAlertDialogBuilder(requireContext())
.setAdapter(adapter) { _: DialogInterface?, i1: Int -> items[i1]!!.action.run() }
.setTitle("Widget $i / id=$appWidgetID")
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> }
.create()
.show()
}
}
}
private fun deleteWidgetsPrefs() {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Delete widgets preferences")
.setMessage(R.string.are_you_sure)
.setPositiveButton(android.R.string.ok) { _, _ ->
val widgetPreferenceStorage = WidgetPreferenceStorage()
widgetPreferenceStorage.deleteWidgetsPrefs(requireContext())
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun showAppWidgetsPrefs() {
val widgetPreferenceStorage = WidgetPreferenceStorage()
val appWidgetsPrefs = widgetPreferenceStorage.getAppWidgetsPrefs(requireContext())
MaterialAlertDialogBuilder(requireContext())
.setTitle("Saved widget preferences")
.setMessage(appWidgetsPrefs)
.setNeutralButton(android.R.string.copy) { _, _ ->
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Saved widget preferences", appWidgetsPrefs)
clipboard.setPrimaryClip(clip)
}
.setPositiveButton(android.R.string.ok, null)
.show()
}
companion object {
private const val PREF_DEBUG_WIDGETS_PREFS_SHOW = "pref_debug_widgets_prefs_show"
private const val PREF_DEBUG_WIDGETS_PREFS_DELETE = "pref_debug_widgets_prefs_delete"
private const val PREF_HEADER_WIDGETS = "pref_header_widgets"
}
}
@@ -0,0 +1,4 @@
package nodomain.freeyourgadget.gadgetbridge.activities.debug.preferences;
public record DebugPreference(String key, String value) {
}
@@ -0,0 +1,253 @@
/* 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.debug.preferences;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.backup.JsonBackupPreferences;
public class PreferenceManagerActivity extends AbstractGBActivity implements MenuProvider {
private static final Logger LOG = LoggerFactory.getLogger(PreferenceManagerActivity.class);
public static final String EXTRA_NAME = "name";
public static final String EXTRA_TITLE = "title";
private SearchView searchView;
private String sharedPreferencesName;
private final List<DebugPreference> allPrefs = new ArrayList<>();
private PreferenceManagerAdapter listAdapter;
private final ActivityResultLauncher<String> exportFileChooser = registerForActivityResult(
new ActivityResultContracts.CreateDocument("application/json"),
uri -> {
LOG.info("Got target export file: {}", uri);
if (uri != null) {
exportPreferences(uri);
}
}
);
private final ActivityResultLauncher<String[]> importFileChooser = registerForActivityResult(
new ActivityResultContracts.OpenDocument(),
uri -> {
LOG.info("Got import file: {}", uri);
if (uri != null) {
importPreferences(uri);
}
}
);
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_debug_preferences);
addMenuProvider(this);
final RecyclerView preferenceListView = findViewById(R.id.preferenceListView);
preferenceListView.setLayoutManager(new LinearLayoutManager(this));
sharedPreferencesName = getIntent().getStringExtra(EXTRA_NAME);
if (sharedPreferencesName == null) {
GB.toast("No preferences name!", Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
final ActionBar actionBar = getSupportActionBar();
final String title = getIntent().getStringExtra(EXTRA_TITLE);
if (actionBar != null) {
actionBar.setTitle(title != null ? title : sharedPreferencesName);
}
listAdapter = new PreferenceManagerAdapter(this, allPrefs);
preferenceListView.setAdapter(listAdapter);
searchView = findViewById(R.id.preferenceListSearchView);
searchView.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
searchView.setIconifiedByDefault(false);
searchView.setVisibility(View.GONE);
searchView.setIconified(false);
searchView.setQuery("", false);
searchView.clearFocus();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(final String query) {
return false;
}
@Override
public boolean onQueryTextChange(final String newText) {
listAdapter.getFilter().filter(newText);
return true;
}
});
reloadPreferences();
}
@SuppressLint("NotifyDataSetChanged") // the entire list will be reloaded
private void reloadPreferences() {
allPrefs.clear();
final SharedPreferences sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
for (Map.Entry<String, ?> e : sharedPreferences.getAll().entrySet()) {
allPrefs.add(new DebugPreference(
e.getKey(),
e.getValue().toString()
));
}
LOG.debug("Got {} preferences", allPrefs.size());
allPrefs.sort((a, b) -> a.key().compareToIgnoreCase(b.key()));
listAdapter.notifyDataSetChanged();
}
@Override
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater menuInflater) {
menuInflater.inflate(R.menu.menu_debug_preferences, menu);
if (!sharedPreferencesName.startsWith("devicesettings_")) {
menu.findItem(R.id.debug_preferences_reset).setVisible(false);
}
}
@SuppressLint("ApplySharedPref")
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem menuItem) {
final SharedPreferences sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
final int itemId = menuItem.getItemId();
if (itemId == R.id.debug_preferences_search) {
searchView.setVisibility(View.VISIBLE);
searchView.requestFocus();
searchView.setIconified(true);
searchView.setIconified(false);
return true;
} else if (itemId == R.id.debug_preferences_export) {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
final String defaultFilename = String.format(Locale.ROOT, "%s_%s.json", sharedPreferencesName, sdf.format(new Date()))
.replace(":", "_")
.toLowerCase(Locale.ROOT);
exportFileChooser.launch(defaultFilename);
return true;
} else if (itemId == R.id.debug_preferences_import) {
importFileChooser.launch(new String[]{"application/json"});
return true;
} else if (itemId == R.id.debug_preferences_reset) {
if (!sharedPreferencesName.startsWith("devicesettings_")) {
LOG.warn("Preventing non device settings clear");
return true;
}
new MaterialAlertDialogBuilder(this)
.setCancelable(true)
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.debugactivity_confirm_remove_device_preferences_title)
.setMessage(R.string.debugactivity_confirm_remove_device_preferences)
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
dialog.dismiss();
})
.setPositiveButton(R.string.ok, (dialog, which) -> {
sharedPreferences.edit().clear().commit();
reloadPreferences();
}).show();
return true;
}
return false;
}
private void exportPreferences(final Uri uri) {
try {
final SharedPreferences sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
final JsonBackupPreferences jsonBackupPreferences = JsonBackupPreferences.exportFrom(sharedPreferences);
final String preferencesJson = jsonBackupPreferences.toJson();
try (OutputStream outputStream = getContentResolver().openOutputStream(uri)) {
if (outputStream == null) {
GB.toast(this, "Failed to open output file", Toast.LENGTH_LONG, GB.ERROR);
return;
}
outputStream.write(preferencesJson.getBytes(StandardCharsets.UTF_8));
GB.toast(this, getString(R.string.export_success), Toast.LENGTH_SHORT, GB.INFO);
}
} catch (IOException e) {
LOG.error("Error exporting preferences", e);
GB.toast(this, "Error exporting preferences: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
private void importPreferences(final Uri uri) {
try {
final JsonBackupPreferences jsonBackupPreferences;
try (InputStream is = getContentResolver().openInputStream(uri)) {
jsonBackupPreferences = JsonBackupPreferences.fromJson(is);
}
final SharedPreferences sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
jsonBackupPreferences.importInto(sharedPreferences);
reloadPreferences();
GB.toast(this, getString(R.string.import_success), Toast.LENGTH_SHORT, GB.INFO);
} catch (IOException | IllegalArgumentException e) {
LOG.error("Error importing preferences", e);
GB.toast(this, "Error importing preferences: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,163 @@
/* Copyright (C) 2023-2024 akasaka / Genjitsu Labs
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.debug.preferences;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
public class PreferenceManagerAdapter extends RecyclerView.Adapter<PreferenceManagerAdapter.DebugPreferenceViewHolder> {
protected static final Logger LOG = LoggerFactory.getLogger(PreferenceManagerAdapter.class);
private final List<DebugPreference> preferenceList;
private final Context mContext;
private final PreferenceFilter preferenceFilter;
public PreferenceManagerAdapter(final Context context, final List<DebugPreference> preferenceList) {
mContext = context;
this.preferenceList = preferenceList;
this.preferenceFilter = new PreferenceFilter(this, preferenceList);
}
@NonNull
@Override
public DebugPreferenceViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final View view = LayoutInflater.from(mContext).inflate(R.layout.item_debug_preference, parent, false);
return new DebugPreferenceViewHolder(view);
}
@Override
public void onBindViewHolder(final DebugPreferenceViewHolder holder, final int position) {
final DebugPreference preference = preferenceList.get(position);
//holder.icon.setVisibility(View.GONE);
holder.name.setText(preference.key());
holder.description.setText(preference.value());
holder.description.setMaxLines(10);
holder.menu.setVisibility(View.INVISIBLE);
//holder.menu.setOnClickListener(view -> {
// final PopupMenu menu = new PopupMenu(mContext, holder.menu);
// menu.inflate(R.menu.file_manager_file);
// menu.getMenu().findItem(R.id.file_manager_preference_menu_view).setVisible(file.getPath().toLowerCase(Locale.ROOT).endsWith(".fit"));
// menu.setOnMenuItemClickListener(item -> {
// final int itemId = item.getItemId();
// if (itemId == R.id.file_manager_preference_menu_share) {
// try {
// AndroidUtils.shareFile(mContext, file, "*/*");
// } catch (final IOException e) {
// GB.toast("Failed to share file", Toast.LENGTH_LONG, GB.ERROR, e);
// }
// return true;
// }
// if (itemId == R.id.file_manager_preference_menu_view) {
// final Intent inspectFileIntent = new Intent(mContext, FitViewerActivity.class);
// inspectFileIntent.putExtra(FitViewerActivity.EXTRA_PATH, file.getPath());
// mContext.startActivity(inspectFileIntent);
// return true;
// }
// return false;
// });
// menu.show();
//});
}
@Override
public int getItemCount() {
return preferenceList.size();
}
public Filter getFilter() {
return preferenceFilter;
}
public static class DebugPreferenceViewHolder extends RecyclerView.ViewHolder {
final TextView name;
final TextView description;
final ImageView menu;
DebugPreferenceViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.preference_key);
description = itemView.findViewById(R.id.preference_description);
menu = itemView.findViewById(R.id.preference_menu);
}
}
private static class PreferenceFilter extends Filter {
private final PreferenceManagerAdapter adapter;
private final List<DebugPreference> originalList;
private final List<DebugPreference> filteredList;
private PreferenceFilter(final PreferenceManagerAdapter adapter, final List<DebugPreference> originalList) {
super();
this.originalList = new ArrayList<>(originalList);
this.filteredList = new ArrayList<>();
this.adapter = adapter;
}
@Override
protected FilterResults performFiltering(final CharSequence filter) {
if (originalList.isEmpty()) {
originalList.addAll(adapter.preferenceList);
}
filteredList.clear();
final FilterResults results = new FilterResults();
if (filter == null || filter.length() == 0)
filteredList.addAll(originalList);
else {
final String filterPattern = filter.toString().toLowerCase().trim();
for (DebugPreference p : originalList) {
if (p.key().toLowerCase().contains(filterPattern) || p.value().toLowerCase().contains(filterPattern)) {
filteredList.add(p);
}
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults filterResults) {
adapter.preferenceList.clear();
adapter.preferenceList.addAll((List<DebugPreference>) filterResults.values);
adapter.notifyDataSetChanged();
}
}
}
@@ -44,7 +44,6 @@ import android.os.ParcelUuid;
import android.os.Parcelable;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
@@ -52,10 +51,8 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
@@ -76,7 +73,6 @@ import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -84,12 +80,9 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AuthKeyActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.DebugActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SimpleIconListAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
@@ -102,6 +95,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.TestDeviceDialog;
public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterView.OnItemClickListener,
@@ -131,8 +125,6 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
private ActivityResultLauncher<Intent> authKeyLauncher;
private long selectedUnsupportedDeviceKey = DebugActivity.SELECT_DEVICE;
private final Runnable stopRunnable = () -> {
stopDiscovery();
LOG.info("Discovery stopped by thread timeout.");
@@ -783,56 +775,13 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
private void showUnsupportedDeviceDialog(final GBDeviceCandidate deviceCandidate) {
LOG.info("Unsupported device candidate selected: {}", deviceCandidate);
final Map<String, Pair<Long, Integer>> allDevices = DebugActivity.getAllSupportedDevices(getApplicationContext());
final LinearLayout linearLayout = new LinearLayout(DiscoveryActivityV2.this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
final ArrayList<SpinnerWithIconItem> deviceListArray = new ArrayList<>();
for (Map.Entry<String, Pair<Long, Integer>> item : allDevices.entrySet()) {
deviceListArray.add(new SpinnerWithIconItem(item.getKey(), item.getValue().first, item.getValue().second));
}
final SpinnerWithIconAdapter deviceListAdapter = new SpinnerWithIconAdapter(
DiscoveryActivityV2.this,
R.layout.spinner_with_image_layout,
R.id.spinner_item_text,
deviceListArray
);
final Spinner deviceListSpinner = new Spinner(DiscoveryActivityV2.this);
deviceListSpinner.setAdapter(deviceListAdapter);
deviceListSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, final long id) {
final SpinnerWithIconItem selectedItem = (SpinnerWithIconItem) parent.getItemAtPosition(pos);
selectedUnsupportedDeviceKey = selectedItem.getId();
}
@Override
public void onNothingSelected(final AdapterView<?> arg0) {
}
});
linearLayout.addView(deviceListSpinner);
final LinearLayout macLayout = new LinearLayout(DiscoveryActivityV2.this);
macLayout.setOrientation(LinearLayout.HORIZONTAL);
macLayout.setPadding(20, 0, 20, 0);
linearLayout.addView(macLayout);
new MaterialAlertDialogBuilder(DiscoveryActivityV2.this)
.setCancelable(true)
.setTitle(R.string.add_test_device)
.setView(linearLayout)
.setPositiveButton(R.string.ok, (dialog, which) -> {
if (selectedUnsupportedDeviceKey != DebugActivity.SELECT_DEVICE) {
final DeviceType deviceType = DeviceType.values()[(int) selectedUnsupportedDeviceKey];
DeviceHelper.getInstance().setForcedDeviceType(deviceCandidate.getMacAddress().toLowerCase(), deviceType);
preparePair(deviceCandidate);
}
})
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
})
.show();
new TestDeviceDialog(this, deviceCandidate.getMacAddress())
.show((macAddress, deviceType) -> {
LOG.debug("Force-pairing {} as {}", deviceCandidate, deviceType);
DeviceHelper.getInstance().setForcedDeviceType(deviceCandidate.getMacAddress().toLowerCase(), deviceType);
preparePair(deviceCandidate);
return kotlin.Unit.INSTANCE;
});
}
@Override
@@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.activities.files;
import android.os.Bundle;
import android.text.InputType;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -87,6 +88,7 @@ public class FileManagerActivity extends AbstractGBActivity implements MenuProvi
fileListView.setAdapter(appListAdapter);
searchView = findViewById(R.id.fileListSearchView);
searchView.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
searchView.setIconifiedByDefault(false);
searchView.setVisibility(View.GONE);
searchView.setIconified(false);
@@ -99,6 +99,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutListActivity;
@@ -916,6 +917,9 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
final boolean detailsShown = expandedDeviceAddress.equals(device.getAddress());
boolean showInfoIcon = device.hasDeviceInfos() && !device.isBusy();
if (BuildConfig.DEBUG) {
menu.getMenu().findItem(R.id.controlcenter_device_submenu_test_new_function).setVisible(deviceConnected);
}
menu.getMenu().findItem(R.id.controlcenter_device_submenu_connect).setVisible(!deviceConnected);
menu.getMenu().findItem(R.id.controlcenter_device_submenu_disconnect).setVisible(deviceConnected);
menu.getMenu().findItem(R.id.controlcenter_device_submenu_show_details).setEnabled(showInfoIcon);
@@ -939,6 +943,12 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
}
removeFromLastDeviceAddressesPref(device);
return true;
} else if (itemId == R.id.controlcenter_device_submenu_test_new_function) {
if (device.isInitialized()) {
GBApplication.deviceService(device).onTestNewFunction();
showTransientSnackbar(R.string.controlcenter_test_new_function);
}
return true;
} else if (itemId == R.id.controlcenter_device_submenu_set_alias) {
showSetAliasDialog(device);
return true;
@@ -42,7 +42,7 @@ public class SpinnerWithIconItem {
@Override
public String toString() {
return text + " " + id + " " + imageId;
return text;
}
}
@@ -444,9 +444,8 @@ public class NotificationListener extends NotificationListenerService {
}
}
NotificationSpec notificationSpec = new NotificationSpec();
NotificationSpec notificationSpec = new NotificationSpec(-1, notification.when);
notificationSpec.key = sbn.getKey();
notificationSpec.when = notification.when;
// determinate Source App Name ("Label")
String name = NotificationUtils.getApplicationLabel(this, source);
@@ -17,7 +17,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.util.GBToStringBuilder;
public class CallSpec {
// TODO: Migrate all usages to the enum..
public static final int CALL_UNDEFINED = 0;
public static final int CALL_ACCEPT = 1;
public static final int CALL_INCOMING = 2;
@@ -44,4 +49,35 @@ public class CallSpec {
public int command;
public int dndSuppressed;
public enum Command {
UNDEFINED,
ACCEPT,
INCOMING,
OUTGOING,
REJECT,
START,
END,
}
@NonNull
@Override
public String toString() {
final GBToStringBuilder tsb = new GBToStringBuilder(this);
tsb.append("command", Command.values()[command]);
tsb.append("number", number);
tsb.append("name", name);
tsb.append("sourceName", sourceName);
tsb.append("sourceAppId", sourceAppId);
tsb.append("key", key);
tsb.append("channelId", channelId);
tsb.append("category", category);
if (isVoip) {
tsb.append("isVoip", isVoip);
}
if (dndSuppressed != 0) {
tsb.append("dndSuppressed", dndSuppressed);
}
return tsb.toString();
}
}
@@ -24,9 +24,9 @@ import java.util.concurrent.atomic.AtomicInteger;
public class NotificationSpec {
public int flags;
private static final AtomicInteger c = new AtomicInteger((int) (System.currentTimeMillis()/1000));
private int id;
private final int id;
public String key;
public long when;
public final long when;
public String sender;
public String phoneNumber;
public String title;
@@ -59,11 +59,15 @@ public class NotificationSpec {
}
public NotificationSpec(int id) {
this(id, System.currentTimeMillis());
}
public NotificationSpec(int id, long when) {
if (id != -1)
this.id = id;
else
this.id = c.incrementAndGet();
this.when = System.currentTimeMillis();
this.when = when;
}
public int getId() {
@@ -57,10 +57,10 @@ class WeatherSpec() : Parcelable {
// Forecasts from the next day onward, in chronological order, one entry per day.
// It should not include the current or previous days
var forecasts: ArrayList<Daily?> = ArrayList()
var forecasts: ArrayList<Daily> = ArrayList()
// Hourly forecasts
var hourly: ArrayList<Hourly?> = ArrayList()
var hourly: ArrayList<Hourly> = ArrayList()
constructor(parcel: Parcel) : this() {
val version = parcel.readInt()
@@ -643,5 +643,58 @@ class WeatherSpec() : Parcelable {
}
return level
}
fun createTestWeather(): WeatherSpec {
val weather = WeatherSpec()
weather.location = "Green Hill"
weather.timestamp = 1764364324
weather.currentTemp = 15 + 273
weather.todayMinTemp = 10 + 273
weather.todayMaxTemp = 25 + 273
weather.currentConditionCode = 601 // snow
weather.currentCondition = "Snowy"
weather.windDirection = 12
weather.precipProbability = 99
weather.windSpeed = 10f
weather.feelsLikeTemp = 13 + 273
weather.currentHumidity = 70
weather.latitude = 38.250137f
weather.longitude = -122.410805f
weather.dewPoint = 10 + 273
val airQuality = AirQuality()
airQuality.aqi = 50
weather.airQuality = airQuality
weather.currentHumidity = 30
weather.hourly = ArrayList()
for (i in 0..23) {
val gbForecast = Hourly()
gbForecast.temp = 10 + i + 273
gbForecast.conditionCode = 800 // clear
gbForecast.precipProbability = 50 + i
gbForecast.windDirection = 30 + i
gbForecast.windSpeed = 20f + i
gbForecast.humidity = 10 + i
gbForecast.uvIndex = 2f + i
weather.hourly.add(gbForecast)
}
weather.forecasts = ArrayList()
for (i in 0..4) {
val gbForecast = Daily()
gbForecast.minTemp = 10 + i + 273
gbForecast.maxTemp = 25 + i + 273
gbForecast.conditionCode = 800 // clear
gbForecast.precipProbability = 50 + i
val airQualityDaily = AirQuality()
airQualityDaily.aqi = 120 + i
gbForecast.airQuality = airQualityDaily
weather.forecasts.add(gbForecast)
}
return weather
}
}
}
@@ -785,12 +785,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
return START_STICKY;
}
LOG.debug("Service startcommand: " + action);
// when we get past this, we should have valid mDeviceSupport and mGBDevice instances
GBDevice targetDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("Service startcommand: {}{}", action, targetDevice != null ? " (" + targetDevice.getAddress() + ")" : "");
Prefs prefs = getPrefs();
switch (action) {
case ACTION_CONNECT:
@@ -20,6 +20,8 @@ package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.Context;
import android.text.format.DateUtils;
import androidx.annotation.NonNull;
import com.github.pfichtner.durationformatter.DurationFormatter;
import java.text.FieldPosition;
@@ -39,8 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class DateTimeUtils {
private static SimpleDateFormat DAY_STORAGE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
private static SimpleDateFormat HOURS_MINUTES_FORMAT = new SimpleDateFormat("HH:mm", Locale.US);
private static final SimpleDateFormat DAY_STORAGE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
private static final SimpleDateFormat HOURS_MINUTES_FORMAT = new SimpleDateFormat("HH:mm", Locale.US);
public static SimpleDateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US){
//see https://github.com/Freeyourgadget/Gadgetbridge/issues/1076#issuecomment-383834116 and https://stackoverflow.com/a/30221245
@@ -53,6 +55,7 @@ public class DateTimeUtils {
}
@NonNull
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
StringBuffer rfcFormat = super.format(date, toAppendTo, pos);
@@ -118,8 +121,7 @@ public class DateTimeUtils {
Calendar cal = GregorianCalendar.getInstance();
cal.setTime(date);
cal.add(GregorianCalendar.DAY_OF_YEAR, offset);
Date newDate = cal.getTime();
return newDate;
return cal.getTime();
}
public static Date dayStart(final Date date) {
@@ -132,6 +134,16 @@ public class DateTimeUtils {
return calendar.getTime();
}
public static Date dayStart(final int year, final int monthOfYear, final int dayOfMonth) {
final Calendar calendar = Calendar.getInstance();
calendar.set(year, monthOfYear, dayOfMonth);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
public static Calendar dayStart(final Calendar calendar) {
final Calendar ret = (Calendar) calendar.clone();
ret.set(Calendar.HOUR_OF_DAY, 0);
@@ -189,10 +201,6 @@ public class DateTimeUtils {
return cal.getTime();
}
public static String dayToString(Date date) {
return DAY_STORAGE_FORMAT.format(date);
}
public static Date dayFromString(String day) throws ParseException {
return DAY_STORAGE_FORMAT.parse(day);
}
@@ -225,9 +233,6 @@ public class DateTimeUtils {
/**
* Calculates new timestamp with a month offset (positive to add or negative to remove)
* from a given time
*
* @param time
* @param month
*/
public static int shiftMonths(int time, int month) {
Calendar day = Calendar.getInstance();
@@ -239,9 +244,6 @@ public class DateTimeUtils {
/**
* Calculates new timestamp with a day offset (positive to add or negative to remove)
* from a given time
*
* @param time
* @param days
*/
public static int shiftDays(int time, int days) {
Calendar day = Calendar.getInstance();
@@ -250,16 +252,6 @@ public class DateTimeUtils {
return (int) (day.getTimeInMillis() / 1000);
}
/**
* Calculates difference in days between two timestamps
*
* @param time1
* @param time2
*/
public static int getDaysBetweenTimes(int time1, int time2) {
return (int) TimeUnit.MILLISECONDS.toDays((time2 - time1) * 1000L);
}
/**
* Determine whether two Calendar instances are on the same day
*
@@ -322,10 +314,11 @@ public class DateTimeUtils {
}
public static String formatDaysUntil(int days, int endTs) {
Date to = new Date((long) endTs * 1000);
Date from = org.apache.commons.lang3.time.DateUtils.addDays(to, - (days - 1));
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("E, MMM dd", Locale.getDefault());
final Date to = new Date((long) endTs * 1000);
final Date from = org.apache.commons.lang3.time.DateUtils.addDays(to, - (days - 1));
final String toFormattedDate = simpleDateFormat.format(to);
final String fromFormattedDate = simpleDateFormat.format(from);
return fromFormattedDate + " - " + toFormattedDate;
}
@@ -49,10 +49,12 @@ public class MediaManager {
this.context = context;
}
@Nullable
public MusicSpec getBufferMusicSpec() {
return bufferMusicSpec;
}
@Nullable
public MusicStateSpec getBufferMusicStateSpec() {
return bufferMusicStateSpec;
}
@@ -0,0 +1,299 @@
package nodomain.freeyourgadget.gadgetbridge.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.Spanned
import android.text.TextWatcher
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Toast
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import nodomain.freeyourgadget.gadgetbridge.BuildConfig
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
import nodomain.freeyourgadget.gadgetbridge.databinding.DialogTestDeviceBinding
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Random
import java.util.TreeMap
class TestDeviceDialog(
private val activity: Activity,
private val macAddress: String?
) {
var selectedTestDeviceMAC = macAddress ?: randomMac()
var selectedTestDeviceKey = -1L
private lateinit var updateOkButtonState: (() -> Unit)
fun show(onSelection: (String, DeviceType) -> Unit) {
val binding = DialogTestDeviceBinding.inflate(activity.layoutInflater)
val allDevicesByName: MutableMap<String, DeviceTypeWithIcon> = getAllSupportedDevices(activity)
// Get unique manufacturers
val manufacturerMap = mutableMapOf<String, MutableList<Map.Entry<String, DeviceTypeWithIcon>>>()
val manufacturers = mutableSetOf<String>()
for (entry in allDevicesByName.entries) {
val manufacturer = entry.value.deviceType.getDeviceCoordinator().getManufacturer()
manufacturers.add(manufacturer)
manufacturerMap.getOrPut(manufacturer) { mutableListOf() }.add(entry)
}
val manufacturerPriority = listOf(
"Gadgetbridge",
activity.getString(R.string.pref_header_generic),
activity.getString(R.string.unknown)
)
val sortedManufacturers = manufacturers.toList().sortedWith(
compareBy<String> { item ->
val index = manufacturerPriority.indexOfFirst { it.equals(item, ignoreCase = true) }
if (index >= 0) index else Int.MAX_VALUE
}.thenBy(String.CASE_INSENSITIVE_ORDER) { it }
)
val manufacturerListArray = ArrayList<String>()
manufacturerListArray.add(activity.getString(R.string.all_manufacturers))
manufacturerListArray.addAll(sortedManufacturers)
// Create full device list
val allDeviceListArray = allDevicesByName.entries
.map { SpinnerWithIconItem(it.key, it.value.deviceType.ordinal.toLong(), it.value.icon) }
.toCollection(ArrayList())
val currentDeviceList = ArrayList(allDeviceListArray)
val deviceListAdapter = SpinnerWithIconAdapter(
activity,
R.layout.spinner_with_image_layout,
R.id.spinner_item_text,
currentDeviceList
)
binding.deviceTypeDropdown.setAdapter(deviceListAdapter)
binding.deviceTypeDropdown.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
val selectedItem = parent.getItemAtPosition(position) as SpinnerWithIconItem
selectedTestDeviceKey = selectedItem.getId()
updateOkButtonState.invoke()
}
// Set up manufacturer dropdown
binding.deviceManufacturerDropdown.setAdapter(
ArrayAdapter(
activity,
android.R.layout.simple_dropdown_item_1line,
manufacturerListArray
)
)
binding.deviceManufacturerDropdown.setText(activity.getString(R.string.all_manufacturers), false)
binding.deviceManufacturerDropdown.onItemClickListener =
AdapterView.OnItemClickListener { parent, _, position, _ ->
val selectedManufacturer = parent.getItemAtPosition(position) as String
// Filter device list based on selected manufacturer
currentDeviceList.clear()
if (position == 0) {
// Show all devices
currentDeviceList.addAll(allDeviceListArray)
} else {
// Show only devices from selected manufacturer
val filteredDevices = manufacturerMap[selectedManufacturer]?.map {
SpinnerWithIconItem(
it.key.replace("^${selectedManufacturer} ".toRegex(), "")
.replace(" \\(${selectedManufacturer}\\)$".toRegex(), ""),
it.value.deviceType.ordinal.toLong(),
it.value.icon
)
} ?: emptyList()
currentDeviceList.addAll(filteredDevices)
}
deviceListAdapter.notifyDataSetChanged()
// Reset device selection
binding.deviceTypeDropdown.setText("", false)
selectedTestDeviceKey = -1L
updateOkButtonState.invoke()
}
val dialog = MaterialAlertDialogBuilder(activity)
.setCancelable(true)
.setTitle(R.string.add_test_device)
.setView(binding.root)
.setPositiveButton(R.string.ok) { _, _ ->
onSelection.invoke(
binding.macEditText.text.toString(),
DeviceType.entries[selectedTestDeviceKey.toInt()]
)
}
.setNegativeButton(R.string.Cancel) { _, _ -> }
.create()
// Validate and update OK button state
updateOkButtonState = {
val positiveButton = dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE)
val isMacValid = isValidMacAddress(binding.macEditText.text.toString())
val isDeviceSelected = selectedTestDeviceKey != -1L
positiveButton?.isEnabled = isMacValid && isDeviceSelected
}
dialog.setOnShowListener {
updateOkButtonState.invoke()
}
setupMacAddressInput(binding.macEditText)
dialog.show()
}
private fun getAllSupportedDevices(context: Context): MutableMap<String, DeviceTypeWithIcon> {
var newMap = LinkedHashMap<String, DeviceTypeWithIcon>(1)
for (deviceType in DeviceType.entries) {
val coordinator = deviceType.getDeviceCoordinator()
val icon = coordinator.getDefaultIconResource()
var name = context.getString(coordinator.getDeviceNameResource())
val manufacturer = when (coordinator.getManufacturer()) {
"Unknown" -> context.getString(R.string.unknown)
"Generic" -> context.getString(R.string.pref_header_generic)
else -> coordinator.getManufacturer()
}
if (!name.startsWith(manufacturer)) {
name += " (${manufacturer})"
}
newMap[name] = DeviceTypeWithIcon(deviceType, icon)
}
val sortedMap = TreeMap<String, DeviceTypeWithIcon>(String.CASE_INSENSITIVE_ORDER)
sortedMap.putAll(newMap)
newMap = LinkedHashMap(sortedMap.size + 3)
// Ensure some devices are first
//newMap[context.getString(R.string.devicetype_scannable)] =
// DeviceTypeWithIcon(DeviceType.SCANNABLE, R.drawable.ic_device_scannable)
//newMap[context.getString(R.string.devicetype_ble_gatt_client)] =
// DeviceTypeWithIcon(DeviceType.BLE_GATT_CLIENT, R.drawable.ic_device_scannable)
newMap.putAll(sortedMap)
return newMap
}
private fun setupMacAddressInput(editText: EditText) {
editText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
if (macAddress != null) {
editText.isEnabled = false
}
// help the user to input a properly formated MAC - see BluetoothAdapter.checkBluetoothAddress
// for Bangle.js builds: also support pebble emulator addresses
editText.filters = arrayOf(object : InputFilter {
@Suppress("KotlinConstantConditions")
override fun filter(
source: CharSequence,
start: Int, end: Int,
dest: Spanned,
dstart: Int, dend: Int
): CharSequence {
val builder = StringBuilder()
for (i in start..<end) {
val c = source[i]
if ((c in '0'..'9') || (c in 'A'..'F') || c == ':') {
builder.append(c)
} else if (c in 'a'..'f') {
builder.append((c.code - 'a'.code + 'A'.code).toChar())
} else if (BuildConfig.INTERNET_ACCESS) {
builder.append(c)
} else if (c == '-') {
builder.append(':')
}
}
return builder
}
})
editText.setText(selectedTestDeviceMAC)
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
selectedTestDeviceMAC = editable.toString()
updateOkButtonState.invoke()
}
})
}
private data class DeviceTypeWithIcon(val deviceType: DeviceType, val icon: Int)
companion object {
private val LOG: Logger = LoggerFactory.getLogger(TestDeviceDialog::class.java)
private fun isValidMacAddress(mac: String): Boolean {
if (BuildConfig.INTERNET_ACCESS) {
// For builds with internet access (Bangle.js), allow more flexible formats
return mac.isNotEmpty()
}
// Standard MAC address validation: XX:XX:XX:XX:XX:XX
val macRegex = "^([0-9A-F]{2}:){5}[0-9A-F]{2}$".toRegex()
return macRegex.matches(mac)
}
private fun randomMac(): String {
val random = Random()
return String.format(
"%02X:%02X:%02X:%02X:%02X:%02X",
random.nextInt(0xff),
random.nextInt(0xff),
random.nextInt(0xff),
random.nextInt(0xff),
random.nextInt(0xff),
random.nextInt(0xff)
)
}
fun createTestDevice(
context: Context,
deviceType: DeviceType,
deviceMac: String,
inputDeviceName: String?
) {
val deviceNameResource = deviceType.getDeviceCoordinator().getDeviceNameResource()
val deviceName: String =
inputDeviceName ?: if (deviceNameResource == 0) deviceType.name else context.getString(
deviceNameResource
)
try {
GBApplication.acquireDB().use { db ->
val daoSession = db.getDaoSession()
val gbDevice = GBDevice(deviceMac, deviceName, "", null, deviceType)
gbDevice.firmwareVersion = "N/A"
gbDevice.firmwareVersion2 = "N/A"
//this causes the attributes (fw version) to be stored as well. Not much useful, but still...
gbDevice.setState(GBDevice.State.INITIALIZED)
DBHelper.getDevice(gbDevice, daoSession) //the addition happens here
val refreshIntent = Intent(DeviceManager.ACTION_REFRESH_DEVICELIST)
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent)
GB.toast(context, "Added test device: $deviceName", Toast.LENGTH_SHORT, GB.INFO)
}
} catch (e: Exception) {
LOG.error("Error accessing database", e)
}
}
}
}
@@ -142,17 +142,17 @@ public class WidgetPreferenceStorage {
editor.apply();
}
public void showAppWidgetsPrefs(Context context) {
public String getAppWidgetsPrefs(Context context) {
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
String savedWidgetsPreferencesData = sharedPrefs.getString(PREFS_WIDGET_SETTINGS, "");
JSONArray savedWidgetsPreferencesDataArray = null;
if (savedWidgetsPreferencesData.isBlank()) {
return "";
}
try {
savedWidgetsPreferencesDataArray = new JSONArray(savedWidgetsPreferencesData);
} catch (
JSONException e) {
LOG.error(e.getMessage());
return new JSONArray(savedWidgetsPreferencesData).toString();
} catch (JSONException e) {
return e.getMessage();
}
GB.toast("Saved app widget preferences: " + savedWidgetsPreferencesDataArray, Toast.LENGTH_SHORT, GB.INFO);
}
@Nullable
+10
View File
@@ -0,0 +1,10 @@
<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="M200,840q-51,0 -72.5,-45.5T138,710l222,-270v-240h-40q-17,0 -28.5,-11.5T280,160q0,-17 11.5,-28.5T320,120h320q17,0 28.5,11.5T680,160q0,17 -11.5,28.5T640,200h-40v240l222,270q32,39 10.5,84.5T760,840L200,840ZM200,760h560L520,468v-268h-80v268L200,760ZM480,480Z" />
</vector>
+11
View File
@@ -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="M120,800v-640l760,320 -760,320ZM200,680 L674,480 200,280v140l240,60 -240,60v140ZM200,680v-400,400Z" />
</vector>
@@ -125,74 +125,6 @@
android:layout_weight="1"
android:text="@string/activity_DB_ShowContentButton" />
<TextView
android:id="@+id/cleanExportDirectory_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/activity_db_management_clean_export_directory_label"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/accent" />
<TextView
android:id="@+id/cleanExportDirectory_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/activity_db_management_clean_export_directory_text" />
<TextView
android:id="@+id/activity_data_management_path2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textStyle="bold" />
<Button
android:id="@+id/cleanExportDirectoryButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/activity_db_management_clean_export_directory_label" />
<TextView
android:id="@+id/mergeOldActivityDataTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/activity_db_management_merge_old_title"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/accent" />
<Button
android:id="@+id/deleteOldActivityDB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/activity_DB_delete_legacy_button" />
<TextView
android:id="@+id/emptyActivityDataTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/activity_db_management_empty_DB"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/accent" />
<TextView
android:id="@+id/emptyDBText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/activity_db_management_empty_db_warning"
android:textAppearance="?android:attr/textAppearanceSmall" />
<Button
android:id="@+id/emptyDBButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/activity_DB_empty_button" />
</LinearLayout>
</LinearLayout>
-332
View File
@@ -1,332 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:grid="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true">
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
grid:alignmentMode="alignMargins"
grid:columnCount="2">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Message / Caller"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/editContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:ems="10"
android:inputType="textMultiLine"
android:text="Test" />
<Spinner
android:id="@+id/sendTypeSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_gravity="fill_horizontal"
grid:layout_columnSpan="2"
android:text="send as SMS" />
<Button
android:id="@+id/sendButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Send" />
<Button
android:id="@+id/incomingCallButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="incoming call" />
<Button
android:id="@+id/outgoingCallButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
grid:layout_gravity="fill_horizontal"
android:text="outgoing call" />
<Button
android:id="@+id/startCallButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="start call" />
<Button
android:id="@+id/endCallButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
grid:layout_gravity="fill_horizontal"
android:text="end call" />
<Button
android:id="@+id/setTimeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="set time" />
<Button
android:id="@+id/setMusicInfoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
grid:layout_gravity="fill_horizontal"
android:text="set music info" />
<Button
android:id="@+id/HeartRateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="Heart Rate Test" />
<Button
android:id="@+id/rebootButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
grid:layout_gravity="fill_horizontal"
android:text="reboot" />
<Button
android:id="@+id/SetFetchTimeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Set Activity Fetch Time" />
<Button
android:id="@+id/setWeatherButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="Set Weather" />
<Button
android:id="@+id/showCachedWeatherButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
grid:layout_gravity="fill_horizontal"
android:text="Show Cached Weather" />
<Button
android:id="@+id/factoryResetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="factory reset" />
<Button
android:id="@+id/testNotificationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="create test notification" />
<Button
android:id="@+id/testPebbleKitNotificationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="create PebbleKit test notification" />
<Button
android:id="@+id/fetchDebugLogsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Fetch device Debug Logs" />
<Button
android:id="@+id/testNewFunctionality"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Test New Functionality" />
<Button
android:id="@+id/shareLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/share_log" />
<Button
android:id="@+id/showWidgetsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Show all registered app widgets IDs" />
<Button
android:id="@+id/deleteWidgets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Delete all registered app widgets IDs" />
<Button
android:id="@+id/showWidgetsPrefs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Show app Widgets Preferences" />
<Button
android:id="@+id/deleteWidgetsPrefs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Delete app Widgets Preferences" />
<CheckBox
android:id="@+id/activity_list_debug_extra_time_range"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_marginTop="7dp"
android:padding="4dp"
android:paddingLeft="10dp"
android:text="Activity summary extra date range (takes long to generate when used)"
android:textStyle="bold"
grid:layout_columnSpan="2" />
<Button
android:id="@+id/addDeviceButtonDebug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="Add test device manually" />
<Button
android:id="@+id/removeDevicePreferences"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Remove device preferences"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/runDebugFunction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Run Debug Function"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/startFitnessAppTracking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_device_action_fitness_app_control_start"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/stopFitnessAppTracking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_device_action_fitness_app_control_stop"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/showStatusFitnessAppTracking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show Fit.App.Track. Status"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/stopPhoneGpsLocationListener"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_device_action_phone_gps_location_listener_stop"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal" />
<Button
android:id="@+id/showCompanionDevices"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/debug_companion_show_associated" />
<Button
android:id="@+id/pairAsCompanion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/debug_companion_pair_current" />
<Button
android:id="@+id/cameraOpen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/open_camera" />
<Button
android:id="@+id/startWelcomeActivity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="Welcome Activity" />
<Button
android:id="@+id/startPermissionsActivity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
grid:layout_gravity="fill_horizontal"
android:text="Permissions Screen" />
</androidx.gridlayout.widget.GridLayout>
</ScrollView>
</RelativeLayout>
@@ -0,0 +1,28 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.debug.preferences.PreferenceManagerActivity">
<androidx.appcompat.widget.SearchView
android:id="@+id/preferenceListSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:inputType="textNoSuggestions"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/preferenceListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/preferenceListSearchView"
android:layout_centerHorizontal="true"
android:divider="@null"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scrollbarSize="5dp"
android:scrollbars="vertical" />
</RelativeLayout>
@@ -0,0 +1,59 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/device_manufacturer_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/device_manufacturer"
android:labelFor="@+id/device_manufacturer_dropdown">
<AutoCompleteTextView
android:id="@+id/device_manufacturer_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:maxLines="1"
app:simpleItems="@array/empty_array" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/device_type_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/widget_settings_select_device_title"
android:labelFor="@+id/device_type_dropdown">
<AutoCompleteTextView
android:id="@+id/device_type_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:maxLines="1"
app:simpleItems="@array/empty_array" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/mac_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/mac_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/device_mac_address"
android:inputType="textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:minHeight="50dp">
<LinearLayout
android:layout_width="0dp"
@@ -20,12 +20,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="Placeholder: Permission name" />
tools:text="Placeholder: Permission name" />
<TextView
android:id="@+id/permission_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Placeholder: Permission summary" />
tools:text="Placeholder: Permission summary" />
</LinearLayout>
<Button
android:id="@+id/permission_request"
@@ -41,7 +41,7 @@
android:layout_marginEnd="20dp"
android:src="@drawable/cpv_preset_checked"
android:visibility="gone"
app:tint="@android:color/holo_green_dark"
app:tint="?attr/checkmarkColor"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="60dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/preference_menu"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/preference_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollHorizontally="false"
android:text="-"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/preference_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text=""
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
<ImageView
android:id="@+id/preference_menu"
android:layout_width="32dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:srcCompat="@drawable/ic_more_vert" />
</RelativeLayout>
@@ -6,6 +6,10 @@
<item
android:id="@+id/controlcenter_device_submenu_disconnect"
android:title="@string/controlcenter_disconnect" />
<item
android:id="@+id/controlcenter_device_submenu_test_new_function"
android:title="@string/controlcenter_test_new_function"
android:visible="false" />
<item
android:id="@+id/controlcenter_device_submenu_set_parent_folder"
android:title="@string/controlcenter_set_parent_folder" />
@@ -0,0 +1,29 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.debug.preferences.PreferenceManagerActivity">
<item
android:id="@+id/debug_preferences_search"
android:icon="@drawable/ic_search"
android:title="@string/search"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="always" />
<item
android:id="@+id/debug_preferences_export"
android:icon="@drawable/ic_file_upload"
android:title="@string/file_export"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
<item
android:id="@+id/debug_preferences_import"
android:icon="@drawable/ic_download"
android:title="@string/file_import"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
<item
android:id="@+id/debug_preferences_reset"
android:icon="@drawable/ic_file_upload"
android:title="@string/reset"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
</menu>
+1
View File
@@ -19,6 +19,7 @@
<attr name="training_acute_load" format="color" />
<attr name="alternate_row_background" format="color" />
<attr name="checkmarkColor" format="color" />
<attr name="deviceIconPrimary" format="color" />
<attr name="deviceIconOnPrimary" format="color" />
<attr name="deviceIconLight" format="color" />
+19 -10
View File
@@ -102,7 +102,7 @@
<string name="debugactivity_really_factoryreset_title">Really factory reset?</string>
<string name="debugactivity_really_factoryreset">Doing a factory reset will delete all data from the connected device (if supported). Xiaomi/Huami devices also change Bluetooth MAC address, so they appear as a new devices to Gadgetbridge.</string>
<string name="debugactivity_confirm_remove_device_preferences_title">Remove device preferences?</string>
<string name="debugactivity_confirm_remove_device_preferences">This will reset the device preferences for all connected devices. Are you sure?</string>
<string name="debugactivity_confirm_remove_device_preferences">This will reset the device preferences for this device. Are you sure?</string>
<!-- Strings related to AppManager -->
<string name="title_activity_appmanager">App Manager</string>
<string name="appmanager_cached_watchapps_watchfaces">Apps in cache</string>
@@ -1345,7 +1345,6 @@
<string name="action_db_management">Data management</string>
<string name="title_activity_db_management">Data management</string>
<string name="activity_db_management_import_export_explanation">The export/import operations use the following path (see below) to a directory on your device. This directory is accessible to other Android apps and your computer. Do note, that this directory and all containing files are deleted if you uninstall Gadgetbridge. The data includes:\n Export_preference - global settings\n Export_preference_MAC - device specific settings\n Gadgetbridge - device and activity database\n Gadgetbridge_date - database exported on a date\n *.gpx - GPS recordings\n *.log - log files\nExpect to find your exported files (or place the files you want to import) there:</string>
<string name="activity_db_management_merge_old_title">Legacy database delete</string>
<string name="dbmanagementactivvity_cannot_access_export_path">Cannot access export path. Please contact the developers.</string>
<string name="dbmanagementactivity_exported_to">Exported to: %1$s</string>
<string name="dbmanagementactivity_error_exporting_db">Error exporting DB: %1$s</string>
@@ -1392,10 +1391,6 @@
<string name="set">Set</string>
<string name="activity_data_management_directory_content_title">Export/Import directory content</string>
<string name="activity_DB_ShowContentButton">Show Export/Import directory content</string>
<string name="activity_db_management_clean_export_directory_label">Delete files in Export/Import directory</string>
<string name="activity_DB_clean_export_directory_warning_title">Delete files in the Export/Import directory?</string>
<string name="activity_DB_clean_export_directory_warning_message">Really delete files in the Export/Import directory?</string>
<string name="activity_db_management_clean_export_directory_text">Exported files in the Export/Import directory are accessible by any app on your device. You might like to remove these files after synchronisation or backup. Make sure to have a backup before deleting them. GPX files, sub-directories and auto-exported database file (if exist) will not be deleted. The path to the Export/Import directory is:</string>
<string name="dbmanagementactivity_export_finished">Deletion finished</string>
<!-- Strings related to Vibration Activity -->
<string name="title_activity_vibration">Vibration</string>
@@ -1943,7 +1938,7 @@
<string name="devicetype_garmin_quatix_8" translatable="false">Garmin Quatix 8</string>
<string name="devicetype_garmin_descent_mk3" translatable="false">Garmin Descent Mk3</string>
<string name="devicetype_garmin_descent_g2" translatable="false">Garmin Descent G2</string>
<string name="devicetype_garmin_inReach_mini_2" translatable="false">inReach Mini 2</string>
<string name="devicetype_garmin_inReach_mini_2" translatable="false">Garmin inReach Mini 2</string>
<string name="devicetype_garmin_instinct" translatable="false">Garmin Instinct</string>
<string name="devicetype_garmin_instinct_solar" translatable="false">Garmin Instinct Solar</string>
<string name="devicetype_garmin_instinct_2" translatable="false">Garmin Instinct 2</string>
@@ -2033,7 +2028,7 @@
<string name="devicetype_polarh9" translatable="false">Polar H9</string>
<string name="devicetype_polarh10" translatable="false">Polar H10</string>
<string name="devicetype_sonyswr12" translatable="false">Sony SWR12</string>
<string name="devicetype_waspos" translatable="false">Wasp-os</string>
<string name="devicetype_waspos" translatable="false">Wasp-OS</string>
<string name="devicetype_smaq2oss" translatable="false">SMA-Q2 OSS</string>
<string name="devicetype_fitpro" translatable="false">FitPro</string>
<string name="devicetype_colacao21" translatable="false">ColaCao 2021</string>
@@ -2325,7 +2320,6 @@
<string name="preferences_rtl_settings">Right To Left Support</string>
<string name="share_log">Share log</string>
<string name="share_log_warning">Please keep in mind Gadgetbridge logs files that may contain lots of personal info, including but not limited to health data, unique identifiers (such as a device\'s MAC address), music preferences, etc. Consider editing the file and removing this info before sending the file to a public issue report.</string>
<string name="share_log_not_enabled_message">You must first enable logging in the Settings - Write log files</string>
<string name="warning">Warning!</string>
<string name="note">Note</string>
<string name="no_data">No data</string>
@@ -3490,6 +3484,7 @@
<string name="info_no_devices_connected">No devices connected</string>
<string name="info_connected_count">%d devices connected</string>
<string name="controlcenter_test_new_function">Test new function</string>
<string name="controlcenter_set_parent_folder">Set parent folder</string>
<string name="controlcenter_set_preferences">Set preferences</string>
<string name="controlcenter_toggle_details">Toggle details</string>
@@ -3529,7 +3524,7 @@
<string name="weekly_total">Weekly total</string>
<string name="prefs_hourly_chime">Hourly chime</string>
<string name="prefs_hourly_chime_summary">The watch will beep once an hour</string>
<string name="devicetype_flipper_zero" translatable="false">Flipper zero</string>
<string name="devicetype_flipper_zero" translatable="false">Flipper Zero</string>
<string name="activity_prefs_allow_bluetooth_intent_api">Bluetooth Intent API</string>
<string name="activity_prefs_summary_allow_bluetooth_intent_api">Allow controlling Bluetooth connection via Intent API</string>
<string name="intent_api_allow_activity_sync_title">Allow activity sync trigger</string>
@@ -4488,4 +4483,18 @@
<string name="solar_panel2_peak_w">Solar panel 2 peak W</string>
<string name="solar_panel3_peak_w">Solar panel 3 peak W</string>
<string name="solar_panel4_peak_w">Solar panel 4 peak W</string>
<string name="all_manufacturers">All manufacturers</string>
<string name="device_manufacturer">Manufacturer</string>
<string name="debug_selected_device">Debug device</string>
<string name="device_mac_address">MAC address</string>
<string name="database">Database</string>
<string name="file_export">Export</string>
<string name="file_import">Import</string>
<string name="global_preferences">Global preferences</string>
<string name="export_success">Export successful</string>
<string name="import_success">Import successful</string>
<string name="reset_fields">Reset fields</string>
<string name="are_you_sure">Are you sure?</string>
<string name="dangerous_actions">Dangerous actions</string>
<string name="debug_other_actions">Other actions</string>
</resources>
+4
View File
@@ -35,6 +35,7 @@
<item name="alternate_row_background">@color/alternate_row_background_light</item>
<item name="row_separator">@color/row_separator_light</item>
<item name="checkmarkColor">#669900</item>
<item name="deviceIconPrimary">#2196f3</item>
<item name="deviceIconOnPrimary">#ffffff</item>
<item name="deviceIconLight">#1f7fdb</item>
@@ -81,6 +82,7 @@
<item name="alternate_row_background">@color/alternate_row_background_dark</item>
<item name="row_separator">@color/row_separator_dark</item>
<item name="checkmarkColor">#669900</item>
<item name="deviceIconPrimary">#2196f3</item>
<item name="deviceIconOnPrimary">#ffffff</item>
<item name="deviceIconLight">#1f7fdb</item>
@@ -115,6 +117,7 @@
<item name="android:navigationBarColor">?attr/colorSurface</item>
<!-- FIXME: SDK 21 to 27 has light navbar on light background -->
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
<item name="checkmarkColor">?attr/colorPrimary</item>
<item name="deviceIconPrimary">?attr/colorPrimary</item>
<item name="deviceIconOnPrimary">?attr/colorOnPrimary</item>
<item name="deviceIconLight">?attr/colorPrimaryVariant</item>
@@ -137,6 +140,7 @@
<item name="preferenceTheme">@style/AppPreferenceThemeOverlay</item>
<item name="android:navigationBarColor">?attr/colorSurface</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
<item name="checkmarkColor">?attr/colorPrimary</item>
<item name="deviceIconPrimary">?attr/colorPrimary</item>
<item name="deviceIconOnPrimary">?attr/colorOnPrimary</item>
<item name="deviceIconLight">?attr/colorPrimaryVariant</item>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/phone_calls">
<Preference
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_call_send"
android:persistent="false"
android:title="Send CallSpec" />
<Preference
android:icon="@drawable/ic_refresh"
android:key="pref_debug_call_reset"
android:persistent="false"
android:title="@string/reset_fields" />
<PreferenceCategory
android:key="pref_debug_header_callspec"
android:title="CallSpec"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="INCOMING"
android:entries="@array/empty_array"
android:entryValues="@array/empty_array"
android:key="pref_debug_call_command"
android:title="command"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="6365553226"
android:key="pref_debug_call_number"
android:title="number"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="Mr. Plow"
android:key="pref_debug_call_name"
android:title="name"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_call_source_name"
android:title="sourceName"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_call_source_app_id"
android:title="sourceAppId"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_call_key"
android:title="key"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_call_channel_id"
android:title="channelId"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_call_category"
android:title="category"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_call_is_voip"
android:layout="@layout/preference_checkbox"
android:title="isVoip"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_call_dnd_suppressed_bool"
android:layout="@layout/preference_checkbox"
android:title="dndSuppressed"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/companion_pairing_request_title">
<PreferenceCategory
android:key="pref_header_companion_devices"
android:title="@string/companion_pairing_request_title">
<!-- to be filled up dynamically -->
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_other_devices"
android:title="@string/Other">
<!-- to be filled up dynamically -->
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/database">
<Preference
android:icon="@drawable/ic_database"
android:key="pref_debug_database_version"
android:persistent="false"
android:selectable="false"
android:title="Database version" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_warning_gray"
android:key="dangerous_actions"
android:layout="@layout/preference_checkbox"
android:persistent="false"
android:title="@string/dangerous_actions" />
<Preference
android:dependency="dangerous_actions"
android:icon="@drawable/ic_delete_forever"
android:key="pref_debug_delete_old_database"
android:persistent="false"
android:title="@string/activity_DB_delete_legacy_button" />
<Preference
android:dependency="dangerous_actions"
android:icon="@drawable/ic_delete_forever"
android:key="pref_debug_empty_database"
android:persistent="false"
android:summary="@string/activity_db_management_empty_db_warning"
android:title="@string/activity_db_management_empty_DB" />
<PreferenceCategory
android:key="pref_header_database_tables"
android:title="Tables"
app:iconSpaceReserved="false">
<!-- to be dynamically filled -->
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- details will be added programmatically -->
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/pref_header_location">
<PreferenceCategory
android:key="pref_header_location_opentracks"
android:title="OpenTracks">
<Preference
android:icon="@drawable/ic_gps_location"
android:key="pref_debug_opentracks_status"
android:persistent="false"
android:title="@string/status" />
<Preference
android:icon="@drawable/ic_play"
android:key="pref_debug_opentracks_start"
android:persistent="false"
android:title="@string/start" />
<Preference
android:icon="@drawable/ic_stop"
android:key="pref_debug_opentracks_stop"
android:persistent="false"
android:title="@string/stop" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_location_gps_listener"
android:title="GPS Listener">
<Preference
android:icon="@drawable/ic_stop"
android:key="pref_debug_gps_listener_stop"
android:persistent="false"
android:title="@string/stop" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<Preference
android:icon="@drawable/ic_add"
android:key="pref_debug_add_test_device"
android:persistent="false"
android:title="@string/add_test_device" />
<PreferenceCategory
android:key="pref_header_logs"
android:title="@string/pref_app_logs_title">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_settings_applications"
android:key="log_to_file"
android:layout="@layout/preference_checkbox"
android:title="@string/pref_write_logfiles" />
<Preference
android:dependency="log_to_file"
android:icon="@drawable/ic_share"
android:key="pref_debug_share_logs"
android:persistent="false"
android:title="@string/share_log" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_debug"
android:title="@string/action_debug">
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.NotificationsDebugFragment"
android:icon="@drawable/ic_notifications"
android:key="pref_debug_notifications"
android:persistent="false"
android:title="@string/menuitem_notifications" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.CallsDebugFragment"
android:icon="@drawable/ic_phone"
android:key="pref_debug_calls"
android:persistent="false"
android:title="@string/phone_calls" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.MusicDebugFragment"
android:icon="@drawable/ic_music_note"
android:key="pref_debug_music"
android:persistent="false"
android:title="@string/menuitem_music" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.WeatherDebugFragment"
android:icon="@drawable/ic_wb_sunny"
android:key="pref_debug_weather"
android:persistent="false"
android:title="@string/menuitem_weather" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.DatabaseDebugFragment"
android:icon="@drawable/ic_database"
android:key="pref_debug_database"
android:persistent="false"
android:title="@string/database" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.PreferencesDebugFragment"
android:icon="@drawable/ic_settings_applications"
android:key="pref_debug_preferences"
android:persistent="false"
android:title="@string/menuitem_settings" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.CompanionDebugFragment"
android:icon="@drawable/ic_devices_other"
android:key="pref_debug_companion_devices"
android:persistent="false"
android:title="@string/companion_pairing_request_title"
tools:ignore="NewApi" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.WidgetsDebugFragment"
android:icon="@drawable/ic_widgets"
android:key="pref_debug_widgets"
android:persistent="false"
android:title="@string/menuitem_widgets" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.LocationDebugFragment"
android:icon="@drawable/ic_gps_location"
android:key="pref_debug_location"
android:persistent="false"
android:title="@string/pref_header_location" />
<Preference
android:fragment="nodomain.freeyourgadget.gadgetbridge.activities.debug.OtherDebugFragment"
android:icon="@drawable/ic_developer_mode"
android:key="pref_debug_other"
android:persistent="false"
android:title="@string/debug_other_actions" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_activities"
android:title="Activities">
<Preference
android:icon="@drawable/ic_camera_remote"
android:key="pref_debug_activity_camera"
android:persistent="false"
android:title="@string/pref_camera_remote_title" />
<Preference
android:icon="@drawable/ic_open_in_browser"
android:key="pref_debug_activity_welcome"
android:persistent="false"
android:title="@string/first_start_welcome_title" />
<Preference
android:icon="@drawable/ic_shield"
android:key="pref_debug_activity_permissions"
android:persistent="false"
android:title="@string/first_start_permissions_title" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/menuitem_music">
<Preference
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_music_send_musicspec"
android:persistent="false"
android:title="Send MusicSpec" />
<Preference
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_music_send_musicstatespec"
android:persistent="false"
android:title="Send MusicStateSpec" />
<Preference
android:icon="@drawable/ic_download"
android:key="pref_debug_music_pull"
android:persistent="false"
android:title="Pull fields from phone" />
<Preference
android:icon="@drawable/ic_refresh"
android:key="pref_debug_music_reset"
android:persistent="false"
android:title="@string/reset_fields" />
<PreferenceCategory
android:key="pref_header_musicspec"
android:title="MusicSpec"
app:iconSpaceReserved="false">
<EditTextPreference
android:defaultValue="The Artist"
android:key="pref_debug_music_artist"
android:title="artist"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="The Album"
android:key="pref_debug_music_album"
android:title="album"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="The Track Name"
android:key="pref_debug_music_track"
android:title="track"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="120"
android:inputType="number"
android:key="pref_debug_music_duration"
android:title="duration"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="5"
android:inputType="number"
android:key="pref_debug_music_trackCount"
android:title="trackCount"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="2"
android:inputType="number"
android:key="pref_debug_music_trackNr"
android:title="trackNr"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_musicstatespec"
android:title="MusicStateSpec"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="STATE_PLAYING"
android:entries="@array/empty_array"
android:entryValues="@array/empty_array"
android:key="pref_debug_music_state"
android:title="state"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="30"
android:inputType="number"
android:key="pref_debug_music_position"
android:title="position"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="100"
android:inputType="number"
android:key="pref_debug_music_playRate"
android:title="playRate"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_music_shuffle_bool"
android:layout="@layout/preference_checkbox"
android:title="shuffle"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_music_repeat_bool"
android:layout="@layout/preference_checkbox"
android:title="repeat"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_notifications_send"
android:persistent="false"
android:title="Send NotificationSpec" />
<Preference
android:icon="@drawable/ic_arrow_upload_progress"
android:key="pref_debug_pebblekit_notification"
android:persistent="false"
android:title="Send PebbleKit notification" />
<Preference
android:icon="@drawable/ic_notifications"
android:key="pref_debug_create_test_notification"
android:persistent="false"
android:title="Create test notification" />
<Preference
android:icon="@drawable/ic_refresh"
android:key="pref_debug_notifications_reset"
android:persistent="false"
android:title="@string/reset_fields" />
<PreferenceCategory
android:key="pref_debug_header_notificationspec"
android:title="NotificationSpec"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="UNKNOWN"
android:entries="@array/empty_array"
android:entryValues="@array/empty_array"
android:key="pref_debug_notifications_type"
android:title="type"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_notifications_sender"
android:title="sender"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="Title"
android:key="pref_debug_notifications_title"
android:title="title"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="Subject"
android:key="pref_debug_notifications_subject"
android:title="subject"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="Notification body"
android:key="pref_debug_notifications_body"
android:title="body"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_notifications_attachedActions_reply"
android:layout="@layout/preference_checkbox"
android:title="attachedActions - REPLY"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_notifications_picturePath_bool"
android:layout="@layout/preference_checkbox"
android:title="picturePath - Include picture"
app:iconSpaceReserved="false" />
<EditTextPreference
android:key="pref_debug_notifications_phoneNumber"
android:title="phoneNumber"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="Gadgetbridge"
android:key="pref_debug_notifications_sourceName"
android:title="sourceName"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="@string/applicationId"
android:key="pref_debug_notifications_sourceAppId"
android:title="sourceAppId"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="0"
android:key="pref_debug_notifications_flags"
android:title="flags"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="0|nodomain.freeyourgadget.gadgetbridge|-493519667|null|10679"
android:key="pref_debug_notifications_key"
android:title="key"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="the_channel"
android:key="pref_debug_notifications_channelId"
android:title="channelId"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:key="pref_debug_notifications_category"
android:title="category"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:defaultValue="0"
android:key="pref_debug_notifications_iconId"
android:title="iconId"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_debug_notifications_dndSuppressed"
android:layout="@layout/preference_checkbox"
android:title="dndSuppressed"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/debug_other_actions">
<Preference
android:icon="@drawable/ic_access_time"
android:key="pref_debug_set_time"
android:persistent="false"
android:title="Set time" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_show_chart"
android:key="activity_list_debug_extra_time_range"
android:layout="@layout/preference_checkbox"
android:summary="Takes long to generate when used"
android:title="Activity summary extra date range" />
<PreferenceCategory
android:key="pref_debug_header_fetch_recorded_data"
android:title="Data fetch"
app:iconSpaceReserved="false">
<Preference
android:icon="@drawable/ic_timer"
android:key="pref_debug_set_activity_fetch_time"
android:persistent="false"
android:title="Set activity fetch time" />
<Preference
android:icon="@drawable/ic_download"
android:key="pref_debug_fetch_debug_logs"
android:persistent="false"
android:title="Fetch debug logs" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_debug_header_reset"
android:title="@string/reset"
app:iconSpaceReserved="false">
<Preference
android:icon="@drawable/ic_refresh"
android:key="pref_debug_reboot"
android:persistent="false"
android:title="Reboot" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_warning_gray"
android:key="dangerous_actions"
android:layout="@layout/preference_checkbox"
android:persistent="false"
android:title="@string/dangerous_actions" />
<Preference
android:dependency="dangerous_actions"
android:icon="@drawable/ic_delete"
android:key="pref_debug_factory_reset"
android:persistent="false"
android:title="@string/factory_reset" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/menuitem_settings">
<Preference
android:icon="@drawable/ic_settings_applications"
android:key="pref_debug_preferences_version"
android:persistent="false"
android:selectable="false"
android:title="Preferences version" />
<Preference
android:icon="@drawable/ic_settings"
android:key="pref_debug_global_preferences"
android:persistent="false"
android:title="@string/global_preferences" />
<PreferenceCategory
android:key="pref_header_device_preferences"
android:title="@string/bottom_nav_devices"
app:iconSpaceReserved="false">
<!-- to be dynamically filled -->
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/menuitem_weather">
<PreferenceCategory
android:key="pref_header_weather"
android:title="@string/menuitem_weather">
<Preference
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_weather_send"
android:persistent="false"
android:title="Send weather to devices" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:icon="@drawable/ic_database"
android:key="cache_weather"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather" />
<Preference
android:icon="@drawable/ic_add_gray"
android:key="pref_debug_weather_add_test"
android:persistent="false"
android:title="Add test weather to cache" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_header_cached_weather"
android:title="Cache">
<!-- to be filled up dynamically -->
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/menuitem_widgets">
<Preference
android:icon="@drawable/ic_settings"
android:key="pref_debug_widgets_prefs_show"
android:persistent="false"
android:title="Show widgets preferences" />
<Preference
android:icon="@drawable/ic_delete_forever"
android:key="pref_debug_widgets_prefs_delete"
android:persistent="false"
android:title="Delete widgets preferences" />
<PreferenceCategory
android:key="pref_header_widgets"
android:title="Widgets">
<!-- to be filled up dynamically -->
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
-7
View File
@@ -370,13 +370,6 @@
android:summary="@string/pref_crash_notification_summary"
android:title="@string/pref_crash_notification_title"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="cache_weather"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:key="pref_screen_intent_api"
+1
View File
@@ -6,4 +6,5 @@
<cache-path name="raw" path="raw/" />
<cache-path name="gpx" path="gpx/" />
<cache-path name="audio" path="audio/" />
<cache-path name="images" path="images/" />
</paths>