From ad1e0c55899be3b6b9df64b74443ad5ece57388c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 26 Dec 2025 20:02:37 +0000 Subject: [PATCH] Revamp Debug activity --- .idea/dictionaries/t.xml | 3 + app/src/main/AndroidManifest.xml | 8 +- .../gadgetbridge/GBApplication.java | 2 +- .../activities/ControlCenterv2.java | 3 +- .../activities/DataManagementActivity.java | 162 -- .../activities/DebugActivity.java | 1310 ----------------- .../activities/SettingsActivity.java | 13 - .../activities/debug/AbstractDebugFragment.kt | 119 ++ .../activities/debug/CallsDebugFragment.kt | 82 ++ .../debug/CompanionDebugFragment.kt | 191 +++ .../activities/debug/DatabaseDebugFragment.kt | 149 ++ .../debug/DatabaseTableDebugFragment.kt | 109 ++ .../activities/debug/DebugActivityV2.kt | 10 + .../activities/debug/LocationDebugFragment.kt | 67 + .../activities/debug/MainDebugFragment.kt | 155 ++ .../activities/debug/MusicDebugFragment.kt | 154 ++ .../debug/NotificationsDebugFragment.kt | 274 ++++ .../activities/debug/OtherDebugFragment.kt | 124 ++ .../debug/PreferencesDebugFragment.kt | 54 + .../activities/debug/WeatherDebugFragment.kt | 168 +++ .../debug/WeatherSpecDebugFragment.kt | 178 +++ .../activities/debug/WidgetsDebugFragment.kt | 98 ++ .../debug/preferences/DebugPreference.java | 4 + .../PreferenceManagerActivity.java | 253 ++++ .../preferences/PreferenceManagerAdapter.java | 163 ++ .../discovery/DiscoveryActivityV2.java | 67 +- .../activities/files/FileManagerActivity.java | 2 + .../adapter/GBDeviceAdapterv2.java | 10 + .../adapter/SpinnerWithIconItem.java | 2 +- .../externalevents/NotificationListener.java | 3 +- .../gadgetbridge/model/CallSpec.java | 36 + .../gadgetbridge/model/NotificationSpec.java | 10 +- .../gadgetbridge/model/WeatherSpec.kt | 57 +- .../service/DeviceCommunicationService.java | 4 +- .../gadgetbridge/util/DateTimeUtils.java | 49 +- .../gadgetbridge/util/MediaManager.java | 2 + .../gadgetbridge/util/TestDeviceDialog.kt | 299 ++++ .../util/WidgetPreferenceStorage.java | 14 +- app/src/main/res/drawable/ic_science.xml | 10 + app/src/main/res/drawable/ic_send.xml | 11 + .../res/layout/activity_data_management.xml | 68 - app/src/main/res/layout/activity_debug.xml | 332 ----- .../res/layout/activity_debug_preferences.xml | 28 + .../main/res/layout/dialog_test_device.xml | 59 + .../fragment_welcome_permission_row.xml | 10 +- .../main/res/layout/item_debug_preference.xml | 53 + .../menu/fragment_devices_device_submenu.xml | 4 + .../main/res/menu/menu_debug_preferences.xml | 29 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 29 +- app/src/main/res/values/styles.xml | 4 + .../main/res/xml/debug_preferences_calls.xml | 91 ++ .../res/xml/debug_preferences_companion.xml | 21 + .../res/xml/debug_preferences_database.xml | 45 + .../main/res/xml/debug_preferences_empty.xml | 4 + .../res/xml/debug_preferences_location.xml | 40 + .../main/res/xml/debug_preferences_main.xml | 131 ++ .../main/res/xml/debug_preferences_music.xml | 126 ++ .../xml/debug_preferences_notifications.xml | 146 ++ .../xml/debug_preferences_other_actions.xml | 66 + .../res/xml/debug_preferences_preferences.xml | 28 + .../res/xml/debug_preferences_weather.xml | 39 + .../res/xml/debug_preferences_widgets.xml | 25 + app/src/main/res/xml/preferences.xml | 7 - app/src/main/res/xml/shared_paths.xml | 1 + 65 files changed, 3801 insertions(+), 2015 deletions(-) delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/AbstractDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CallsDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CompanionDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseTableDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DebugActivityV2.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/LocationDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MusicDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/NotificationsDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/OtherDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/PreferencesDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherSpecDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WidgetsDebugFragment.kt create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/DebugPreference.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerAdapter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/TestDeviceDialog.kt create mode 100644 app/src/main/res/drawable/ic_science.xml create mode 100644 app/src/main/res/drawable/ic_send.xml delete mode 100644 app/src/main/res/layout/activity_debug.xml create mode 100644 app/src/main/res/layout/activity_debug_preferences.xml create mode 100644 app/src/main/res/layout/dialog_test_device.xml create mode 100644 app/src/main/res/layout/item_debug_preference.xml create mode 100644 app/src/main/res/menu/menu_debug_preferences.xml create mode 100644 app/src/main/res/xml/debug_preferences_calls.xml create mode 100644 app/src/main/res/xml/debug_preferences_companion.xml create mode 100644 app/src/main/res/xml/debug_preferences_database.xml create mode 100644 app/src/main/res/xml/debug_preferences_empty.xml create mode 100644 app/src/main/res/xml/debug_preferences_location.xml create mode 100644 app/src/main/res/xml/debug_preferences_main.xml create mode 100644 app/src/main/res/xml/debug_preferences_music.xml create mode 100644 app/src/main/res/xml/debug_preferences_notifications.xml create mode 100644 app/src/main/res/xml/debug_preferences_other_actions.xml create mode 100644 app/src/main/res/xml/debug_preferences_preferences.xml create mode 100644 app/src/main/res/xml/debug_preferences_weather.xml create mode 100644 app/src/main/res/xml/debug_preferences_widgets.xml diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml index 55684f7f5d..b585333578 100644 --- a/.idea/dictionaries/t.xml +++ b/.idea/dictionaries/t.xml @@ -116,12 +116,15 @@ morpheuz mosenkovs multisport + musicspec + musicstatespec nack nephiel nodomain nordhøy normano novotny + opentracks oraclejdk oukitel paddleboarding diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6487a9f8c4..2756cbe179 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -664,10 +664,14 @@ dialog when changing orientation --> + . */ -package nodomain.freeyourgadget.gadgetbridge.activities; - -import static android.content.Intent.EXTRA_SUBJECT; -import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID; - -import android.app.Activity; -import android.app.DatePickerDialog; -import android.app.PendingIntent; -import android.appwidget.AppWidgetHost; -import android.appwidget.AppWidgetManager; -import android.bluetooth.BluetoothDevice; -import android.companion.AssociationRequest; -import android.companion.BluetoothDeviceFilter; -import android.companion.CompanionDeviceManager; -import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.IntentSender; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.Editable; -import android.text.InputFilter; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Pair; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.DatePicker; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; -import androidx.core.app.NavUtils; -import androidx.core.app.NotificationCompat; -import androidx.core.app.RemoteInput; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.Serializable; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Random; -import java.util.TreeMap; - -import nodomain.freeyourgadget.gadgetbridge.BuildConfig; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.Widget; -import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity; -import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter; -import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; -import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; -import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver; -import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; -import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; -import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; -import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; -import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; -import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather; -import nodomain.freeyourgadget.gadgetbridge.model.weather.WeatherMapper; -import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; -import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; -import nodomain.freeyourgadget.gadgetbridge.util.GB; -import nodomain.freeyourgadget.gadgetbridge.util.WidgetPreferenceStorage; - -public class DebugActivity extends AbstractGBActivity { - private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class); - - private static Bundle dataLossSave; - - private static final String EXTRA_REPLY = "reply"; - private static final String ACTION_REPLY - = "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply"; - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - switch (Objects.requireNonNull(intent.getAction())) { - case ACTION_REPLY: { - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY); - LOG.info("got wearable reply: " + reply); - GB.toast(context, "got wearable reply: " + reply, Toast.LENGTH_SHORT, GB.INFO); - break; - } - case DeviceService.ACTION_REALTIME_SAMPLES: - handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE)); - break; - default: - LOG.info("ignoring intent action " + intent.getAction()); - break; - } - } - }; - private Spinner sendTypeSpinner; - private EditText editContent; - public static final long SELECT_DEVICE = -1; - private long selectedTestDeviceKey = SELECT_DEVICE; - private String selectedTestDeviceMAC; - - private static final int SELECT_DEVICE_REQUEST_CODE = 1; - - private void handleRealtimeSample(Serializable extra) { - if (extra instanceof ActivitySample) { - ActivitySample sample = (ActivitySample) extra; - GB.toast(this, "Heart Rate measured: " + sample.getHeartRate(), Toast.LENGTH_LONG, GB.INFO); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_debug); - - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_REPLY); - filter.addAction(DeviceService.ACTION_REALTIME_SAMPLES); - LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter); - ContextCompat.registerReceiver(this, mReceiver, filter, ContextCompat.RECEIVER_EXPORTED); // for ACTION_REPLY - - editContent = findViewById(R.id.editContent); - - final ArrayList spinnerArray = new ArrayList<>(); - for (NotificationType notificationType : NotificationType.sortedValues()) { - spinnerArray.add(notificationType.name()); - } - ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, spinnerArray); - sendTypeSpinner = findViewById(R.id.sendTypeSpinner); - sendTypeSpinner.setAdapter(spinnerArrayAdapter); - - Button sendButton = findViewById(R.id.sendButton); - sendButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - NotificationSpec notificationSpec = new NotificationSpec(); - String testString = editContent.getText().toString(); - notificationSpec.phoneNumber = testString; - notificationSpec.body = testString; - notificationSpec.sender = testString; - notificationSpec.subject = testString; - 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 = BuildConfig.APPLICATION_ID; - } - notificationSpec.sourceName = getApplicationContext().getApplicationInfo() - .loadLabel(getApplicationContext().getPackageManager()) - .toString(); - notificationSpec.type = NotificationType.sortedValues()[sendTypeSpinner.getSelectedItemPosition()]; - notificationSpec.attachedActions = new ArrayList<>(); - - // DISMISS action - NotificationSpec.Action dismissAction = new NotificationSpec.Action(); - dismissAction.title = getString(R.string.dismiss); - dismissAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS; - notificationSpec.attachedActions.add(dismissAction); - - if (notificationSpec.type == NotificationType.GENERIC_SMS) { - // REPLY action - NotificationSpec.Action replyAction = new NotificationSpec.Action(); - replyAction.title = getString(R.string._pebble_watch_reply); - replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR; - notificationSpec.attachedActions.add(replyAction); - } else if ("generic_chat".equals(notificationSpec.type.getGenericType())) { - // REPLY action - NotificationSpec.Action replyAction = new NotificationSpec.Action(); - replyAction.title = getString(R.string._pebble_watch_reply); - replyAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY; - notificationSpec.attachedActions.add(replyAction); - } - - GBApplication.deviceService().onNotification(notificationSpec); - } - }); - - Button incomingCallButton = findViewById(R.id.incomingCallButton); - incomingCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CallSpec callSpec = new CallSpec(); - callSpec.command = CallSpec.CALL_INCOMING; - callSpec.number = editContent.getText().toString(); - callSpec.sourceAppId = BuildConfig.APPLICATION_ID; - GBApplication.deviceService().onSetCallState(callSpec); - } - }); - Button outgoingCallButton = findViewById(R.id.outgoingCallButton); - outgoingCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CallSpec callSpec = new CallSpec(); - callSpec.command = CallSpec.CALL_OUTGOING; - callSpec.number = editContent.getText().toString(); - GBApplication.deviceService().onSetCallState(callSpec); - } - }); - - Button startCallButton = findViewById(R.id.startCallButton); - startCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CallSpec callSpec = new CallSpec(); - callSpec.command = CallSpec.CALL_START; - GBApplication.deviceService().onSetCallState(callSpec); - } - }); - Button endCallButton = findViewById(R.id.endCallButton); - endCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CallSpec callSpec = new CallSpec(); - callSpec.command = CallSpec.CALL_END; - GBApplication.deviceService().onSetCallState(callSpec); - } - }); - - Button rebootButton = findViewById(R.id.rebootButton); - rebootButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GBApplication.deviceService().onReset(GBDeviceProtocol.RESET_FLAGS_REBOOT); - } - }); - - Button factoryResetButton = findViewById(R.id.factoryResetButton); - factoryResetButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(true) - .setTitle(R.string.debugactivity_really_factoryreset_title) - .setMessage(R.string.debugactivity_really_factoryreset) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - GBApplication.deviceService().onReset(GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET); - } - }) - .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .show(); - } - }); - - Button heartRateButton = findViewById(R.id.HeartRateButton); - heartRateButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GB.toast("Measuring heart rate, please wait...", Toast.LENGTH_LONG, GB.INFO); - GBApplication.deviceService().onHeartRateTest(); - } - }); - - Button setFetchTimeButton = findViewById(R.id.SetFetchTimeButton); - setFetchTimeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - final Calendar currentDate = Calendar.getInstance(); - Context context = getApplicationContext(); - - if (context instanceof GBApplication) { - GBApplication gbApp = (GBApplication) context; - final List devices = gbApp.getDeviceManager().getSelectedDevices(); - if(devices.size() == 0){ - GB.toast("Device not selected/connected", Toast.LENGTH_LONG, GB.INFO); - return; - } - new DatePickerDialog(DebugActivity.this, new DatePickerDialog.OnDateSetListener() { - @Override - public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { - Calendar date = Calendar.getInstance(); - date.set(year, monthOfYear, dayOfMonth); - - long timestamp = date.getTimeInMillis() - 1000; - GB.toast("Setting lastSyncTimeMillis: " + timestamp, Toast.LENGTH_LONG, GB.INFO); - - for(GBDevice device : devices){ - SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).edit(); - editor.remove("lastSyncTimeMillis"); //FIXME: key reconstruction is BAD - editor.putLong("lastSyncTimeMillis", timestamp); - editor.apply(); - } - } - }, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show(); - } - - - } - }); - - Button setWeatherButton = findViewById(R.id.setWeatherButton); - setWeatherButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (Weather.getWeatherSpec() == null) { - final WeatherSpec weatherSpec = new WeatherSpec(); - weatherSpec.setForecasts(new ArrayList<>()); - - weatherSpec.setLocation("Green Hill"); - weatherSpec.setCurrentConditionCode(601); // snow - weatherSpec.setCurrentCondition(WeatherMapper.getConditionString(DebugActivity.this, weatherSpec.getCurrentConditionCode())); - - weatherSpec.setCurrentTemp(15 + 273); - weatherSpec.setCurrentHumidity(30); - - weatherSpec.setWindSpeed(10); - weatherSpec.setWindDirection(12); - weatherSpec.setTimestamp((int) (System.currentTimeMillis() / 1000)); - weatherSpec.setTodayMinTemp(10 + 273); - weatherSpec.setTodayMaxTemp(25 + 273); - - for (int i = 0; i < 5; i++) { - final WeatherSpec.Daily gbForecast = new WeatherSpec.Daily(); - gbForecast.setMinTemp(10 + i + 273); - gbForecast.setMaxTemp(25 + i + 273); - - gbForecast.setConditionCode(800); // clear - weatherSpec.getForecasts().add(gbForecast); - } - - Weather.setWeatherSpec(new ArrayList<>(Collections.singletonList(weatherSpec))); - } - - GBApplication.deviceService().onSendWeather(); - } - }); - - Button showCachedWeatherButton = findViewById(R.id.showCachedWeatherButton); - showCachedWeatherButton.setOnClickListener(new View.OnClickListener(){ - @Override - public void onClick(View v) { - final List weatherSpecs = Weather.getWeatherSpecs(); - - if (weatherSpecs == null || weatherSpecs.isEmpty()) { - displayWeatherInfo(null); - return; - } else if (weatherSpecs.size() == 1) { - displayWeatherInfo(weatherSpecs.get(0)); - return; - } - - final String[] weatherLocations = new String[weatherSpecs.size()]; - - for (int i = 0; i < weatherSpecs.size(); i++) { - weatherLocations[i] = weatherSpecs.get(i).getLocation(); - } - - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(true) - .setTitle("Choose Location") - .setItems(weatherLocations, (dialog, which) -> displayWeatherInfo(weatherSpecs.get(which))) - .setNegativeButton("Cancel", (dialog, which) -> { - }) - .show(); - } - }); - - Button setMusicInfoButton = findViewById(R.id.setMusicInfoButton); - setMusicInfoButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - MusicSpec musicSpec = new MusicSpec(); - String testString = editContent.getText().toString(); - musicSpec.artist = testString + "(artist)"; - musicSpec.album = testString + "(album)"; - musicSpec.track = testString + "(track)"; - musicSpec.duration = 10; - musicSpec.trackCount = 5; - musicSpec.trackNr = 2; - - GBApplication.deviceService().onSetMusicInfo(musicSpec); - - MusicStateSpec stateSpec = new MusicStateSpec(); - stateSpec.position = 0; - stateSpec.state = 0x01; // playing - stateSpec.playRate = 100; - stateSpec.repeat = 1; - stateSpec.shuffle = 1; - - GBApplication.deviceService().onSetMusicState(stateSpec); - } - }); - - Button setTimeButton = findViewById(R.id.setTimeButton); - setTimeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GBApplication.deviceService().onSetTime(); - } - }); - - Button testNotificationButton = findViewById(R.id.testNotificationButton); - testNotificationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - testNotification(); - } - }); - - Button testPebbleKitNotificationButton = findViewById(R.id.testPebbleKitNotificationButton); - testPebbleKitNotificationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - testPebbleKitNotification(); - } - }); - - Button fetchDebugLogsButton = findViewById(R.id.fetchDebugLogsButton); - fetchDebugLogsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GBApplication.deviceService().onFetchRecordedData(RecordedDataTypes.TYPE_DEBUGLOGS); - } - }); - - Button testNewFunctionalityButton = findViewById(R.id.testNewFunctionality); - testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - testNewFunctionality(); - } - }); - - Button shareLogButton = findViewById(R.id.shareLog); - shareLogButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean hasLog = false; - String filePath = "???"; - try { - filePath = GBApplication.getLogPath(); - if (filePath != null && filePath.length() > 0) { - File log = new File(filePath); - hasLog = log.exists(); - } - } catch (Exception e) { - LOG.warn("failed to check log existence: {}", filePath, e); - } - - if (hasLog) { - showLogSharingWarning(); - } else { - showLogSharingNotEnabledAlert(); - } - } - }); - - Button showWidgetsButton = findViewById(R.id.showWidgetsButton); - showWidgetsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showAllRegisteredAppWidgets(); - } - }); - - Button unregisterWidgetsButton = findViewById(R.id.deleteWidgets); - unregisterWidgetsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - unregisterAllRegisteredAppWidgets(); - } - }); - - Button showWidgetsPrefsButton = findViewById(R.id.showWidgetsPrefs); - showWidgetsPrefsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showAppWidgetsPrefs(); - } - }); - - Button deleteWidgetsPrefsButton = findViewById(R.id.deleteWidgetsPrefs); - deleteWidgetsPrefsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - deleteWidgetsPrefs(); - } - }); - - Button removeDevicePreferencesButton = findViewById(R.id.removeDevicePreferences); - removeDevicePreferencesButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(true) - .setTitle(R.string.debugactivity_confirm_remove_device_preferences_title) - .setMessage(R.string.debugactivity_confirm_remove_device_preferences) - .setPositiveButton(R.string.ok, (dialog, which) -> { - final GBApplication gbApp = (GBApplication) getApplicationContext(); - final List devices = gbApp.getDeviceManager().getSelectedDevices(); - for(final GBDevice device : devices){ - GBApplication.deleteDeviceSpecificSharedPrefs(device.getAddress()); - } - }) - .setNegativeButton(R.string.Cancel, (dialog, which) -> {}) - .show(); - } - }); - - Button runDebugFunction = findViewById(R.id.runDebugFunction); - runDebugFunction.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - //SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); - //editor.remove("notification_list_is_blacklist").apply(); - } - }); - - Button addDeviceButtonDebug = findViewById(R.id.addDeviceButtonDebug); - addDeviceButtonDebug.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Map> allDevices = getAllSupportedDevices(getApplicationContext()); - - final LinearLayout linearLayout = new LinearLayout(DebugActivity.this); - linearLayout.setOrientation(LinearLayout.VERTICAL); - - final LinearLayout macLayout = new LinearLayout(DebugActivity.this); - macLayout.setOrientation(LinearLayout.HORIZONTAL); - macLayout.setPadding(20, 0, 20, 0); - - final TextView textView = new TextView(DebugActivity.this); - textView.setText("MAC Address: "); - final EditText editText = new EditText(DebugActivity.this); - // help the user to input a properly formated MAC - see BluetoothAdapter.checkBluetoothAddress - // for banglejs builds: also support pebble emulator addresses - editText.setFilters(new InputFilter[]{(source, start, end, dest, dstart, dend) -> { - StringBuilder builder = new StringBuilder(); - - for (int i = start; i < end; i++) { - char c = source.charAt(i); - if (('0' <= c && c <= '9') || ('A' <= c && c <= 'F') || c == ':') { - builder.append(c); - } else if ('a' <= c && c <= 'f') { - builder.append((char) (c - 'a' + 'A')); - } else if (BuildConfig.INTERNET_ACCESS) { - builder.append(c); - } else if (c == '-') { - builder.append(':'); - } - } - return builder; - }}); - selectedTestDeviceMAC = randomMac(); - editText.setText(selectedTestDeviceMAC); - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - selectedTestDeviceMAC = editable.toString(); - - } - }); - - macLayout.addView(textView); - macLayout.addView(editText); - - final Spinner deviceListSpinner = new Spinner(DebugActivity.this); - ArrayList deviceListArray = new ArrayList<>(); - for (Map.Entry> item : allDevices.entrySet()) { - deviceListArray.add(new SpinnerWithIconItem(item.getKey(), item.getValue().first, item.getValue().second)); - } - final SpinnerWithIconAdapter deviceListAdapter = new SpinnerWithIconAdapter(DebugActivity.this, - R.layout.spinner_with_image_layout, R.id.spinner_item_text, deviceListArray); - deviceListSpinner.setAdapter(deviceListAdapter); - addListenerOnSpinnerDeviceSelection(deviceListSpinner); - - linearLayout.addView(deviceListSpinner); - linearLayout.addView(macLayout); - - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(true) - .setTitle(R.string.add_test_device) - .setView(linearLayout) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - createTestDevice(DebugActivity.this, selectedTestDeviceKey, selectedTestDeviceMAC, null); - } - }) - .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .show(); - } - }); - - CheckBox activity_list_debug_extra_time_range = findViewById(R.id.activity_list_debug_extra_time_range); - activity_list_debug_extra_time_range.setAllCaps(true); - boolean activity_list_debug_extra_time_range_value = GBApplication.getPrefs().getPreferences().getBoolean("activity_list_debug_extra_time_range", false); - activity_list_debug_extra_time_range.setChecked(activity_list_debug_extra_time_range_value); - - activity_list_debug_extra_time_range.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - GBApplication.getPrefs().getPreferences().getBoolean("activity_list_debug_extra_time_range", false); - SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); - editor.putBoolean("activity_list_debug_extra_time_range", b).apply(); - } - }); - - Button startFitnessAppTracking = findViewById(R.id.startFitnessAppTracking); - startFitnessAppTracking.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - OpenTracksController.startRecording(DebugActivity.this); - } - }); - - Button stopFitnessAppTracking = findViewById(R.id.stopFitnessAppTracking); - stopFitnessAppTracking.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - OpenTracksController.stopRecording(DebugActivity.this); - } - }); - - Button stopPhoneGpsLocationListener = findViewById(R.id.stopPhoneGpsLocationListener); - stopPhoneGpsLocationListener.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - GBLocationService.stop(DebugActivity.this, null); - } - }); - - Button showCompanionDevices = findViewById(R.id.showCompanionDevices); - showCompanionDevices.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE); - showCompanionDevices.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - LOG.warn("Android version < O, companion devices not supported"); - return; - } - - final CompanionDeviceManager manager = (CompanionDeviceManager) GBApplication.getContext().getSystemService(Context.COMPANION_DEVICE_SERVICE); - final List associations = new ArrayList<>(manager.getAssociations()); - Collections.sort(associations); - final StringBuilder sb = new StringBuilder(String.format(Locale.ROOT, "%d companion devices", associations.size())); - if (!associations.isEmpty()) { - sb.append("\n"); - for (final String association : associations) { - sb.append("\n").append(association); - final GBDevice device = GBApplication.app() - .getDeviceManager() - .getDeviceByAddress(association); - if (device == null) { - sb.append(" (Unknown)"); - } else { - sb.append(" (").append(device.getAliasOrName()).append(")"); - } - } - } - - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(false) - .setTitle("Companion Devices") - .setMessage(sb.toString()) - .setPositiveButton(R.string.ok, (dialog, which) -> { - }) - .show(); - } - }); - - final Button pairAsCompanion = findViewById(R.id.pairAsCompanion); - pairAsCompanion.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE); - pairAsCompanion.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - pairCurrentAsCompanion(); - } - }); - - Button showStatusFitnessAppTracking = findViewById(R.id.showStatusFitnessAppTracking); - final int delay = 2 * 1000; - - showStatusFitnessAppTracking.setOnClickListener(new View.OnClickListener() { - final Handler handler = new Handler(); - Runnable runnable; - - @Override - public void onClick(View v) { - final MaterialAlertDialogBuilder fitnesStatusBuilder = new MaterialAlertDialogBuilder(DebugActivity.this); - fitnesStatusBuilder - .setCancelable(false) - .setTitle("openTracksObserver Status") - .setMessage("Starting openTracksObserver watcher, waiting for an update, refreshing every: " + delay + "ms") - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - handler.removeCallbacks(runnable); - } - }); - final AlertDialog alert = fitnesStatusBuilder.show(); - - - runnable = new Runnable() { - @Override - public void run() { - LOG.debug("openTracksObserver debug watch dialog running"); - handler.postDelayed(this, delay); //schedule next execution - - OpenTracksContentObserver openTracksObserver = GBApplication.app().getOpenTracksObserver(); - if (openTracksObserver == null) { - LOG.debug("openTracksObserver is null"); - alert.cancel(); - alert.setMessage("openTracksObserver not running"); - alert.show(); - return; - } - LOG.debug("openTracksObserver is not null, updating debug view"); - long timeSecs = openTracksObserver.getTimeMillisChange() / 1000; - float distanceCM = openTracksObserver.getDistanceMeterChange() * 100; - - LOG.debug("Time: " + timeSecs + " distanceCM " + distanceCM); - alert.cancel(); - alert.setMessage("TimeSec: " + timeSecs + " distanceCM " + distanceCM); - alert.show(); - } - }; - handler.postDelayed(runnable, delay); - } - }); - - Button cameraOpenButton = findViewById(R.id.cameraOpen); - cameraOpenButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - - Intent cameraIntent = new Intent(getApplicationContext(), CameraActivity.class); - cameraIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - cameraIntent.putExtra(CameraActivity.intentExtraEvent, GBDeviceEventCameraRemote.eventToInt(GBDeviceEventCameraRemote.Event.OPEN_CAMERA)); - getApplicationContext().startActivity(cameraIntent); - } - }); - - Button startWelcomeActivity = findViewById(R.id.startWelcomeActivity); - startWelcomeActivity.setOnClickListener(view -> { - startActivity(new Intent(this, WelcomeActivity.class)); - }); - - Button startPermissionsActivity = findViewById(R.id.startPermissionsActivity); - startPermissionsActivity.setOnClickListener(view -> { - startActivity(new Intent(this, PermissionsActivity.class)); - }); - } - - @RequiresApi(Build.VERSION_CODES.O) - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && - requestCode == SELECT_DEVICE_REQUEST_CODE && - resultCode == Activity.RESULT_OK) { - - final BluetoothDevice deviceToPair = data.getParcelableExtra(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); - } - } else { - GB.toast("No device to pair", Toast.LENGTH_LONG, GB.ERROR); - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private void pairCurrentAsCompanion() { - final GBApplication gbApp = (GBApplication) getApplicationContext(); - final List devices = gbApp.getDeviceManager().getSelectedDevices(); - if (devices.size() != 1) { - GB.toast("Please connect to a single device that you want to pair as companion", Toast.LENGTH_LONG, GB.WARN); - return; - } - - final GBDevice device = devices.get(0); - - final CompanionDeviceManager manager = (CompanionDeviceManager) getSystemService(Context.COMPANION_DEVICE_SERVICE); - - if (manager.getAssociations().contains(device.getAddress())) { - GB.toast(device.getAliasOrName() + " already paired as companion", Toast.LENGTH_LONG, GB.INFO); - return; - } - - final BluetoothDeviceFilter deviceFilter = new BluetoothDeviceFilter.Builder() - .setAddress(device.getAddress()) - .build(); - - final AssociationRequest pairingRequest = new AssociationRequest.Builder() - .addDeviceFilter(deviceFilter) - .setSingleDevice(true) - .build(); - - CompanionDeviceManager.Callback callback = new CompanionDeviceManager.Callback() { - @Override - public void onFailure(final CharSequence error) { - GB.toast("Companion pairing failed: " + error, Toast.LENGTH_LONG, GB.ERROR); - } - - @Override - public void onDeviceFound(@NonNull final IntentSender chooserLauncher) { - GB.toast("Found device", Toast.LENGTH_SHORT, GB.INFO); - - try { - ActivityCompat.startIntentSenderForResult( - DebugActivity.this, - chooserLauncher, - SELECT_DEVICE_REQUEST_CODE, - null, - 0, - 0, - 0, - null - ); - } catch (final IntentSender.SendIntentException e) { - LOG.error("Failed to send intent", e); - } - } - }; - - manager.associate(pairingRequest, callback, null); - } - - @Override - protected void onPause() { - super.onPause(); - if (dataLossSave != null ) { - dataLossSave.clear(); - dataLossSave = null ; - } - dataLossSave = new Bundle(); - dataLossSave.putString("editContent", editContent.getText().toString()); - } - - @Override - protected void onResume() { - super.onResume(); - if (dataLossSave != null ) { - editContent.setText(dataLossSave.getString("editContent", "")); - }else{ - editContent.setText("Test"); - } - } - - private void deleteWidgetsPrefs() { - WidgetPreferenceStorage widgetPreferenceStorage = new WidgetPreferenceStorage(); - widgetPreferenceStorage.deleteWidgetsPrefs(DebugActivity.this); - widgetPreferenceStorage.showAppWidgetsPrefs(DebugActivity.this); - } - - private void showAppWidgetsPrefs() { - WidgetPreferenceStorage widgetPreferenceStorage = new WidgetPreferenceStorage(); - widgetPreferenceStorage.showAppWidgetsPrefs(DebugActivity.this); - - } - - private void showAllRegisteredAppWidgets() { - //https://stackoverflow.com/questions/17387191/check-if-a-widget-is-exists-on-homescreen-using-appwidgetid - - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(DebugActivity.this); - AppWidgetHost appWidgetHost = new AppWidgetHost(DebugActivity.this, 1); // for removing phantoms - int[] appWidgetIDs = appWidgetManager.getAppWidgetIds(new ComponentName(DebugActivity.this, Widget.class)); - GB.toast("Number of registered app widgets: " + appWidgetIDs.length, Toast.LENGTH_SHORT, GB.INFO); - for (int appWidgetID : appWidgetIDs) { - GB.toast("Widget: " + appWidgetID, Toast.LENGTH_SHORT, GB.INFO); - } - } - - private void unregisterAllRegisteredAppWidgets() { - //https://stackoverflow.com/questions/17387191/check-if-a-widget-is-exists-on-homescreen-using-appwidgetid - - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(DebugActivity.this); - AppWidgetHost appWidgetHost = new AppWidgetHost(DebugActivity.this, 1); // for removing phantoms - int[] appWidgetIDs = appWidgetManager.getAppWidgetIds(new ComponentName(DebugActivity.this, Widget.class)); - GB.toast("Number of registered app widgets: " + appWidgetIDs.length, Toast.LENGTH_SHORT, GB.INFO); - for (int appWidgetID : appWidgetIDs) { - appWidgetHost.deleteAppWidgetId(appWidgetID); - GB.toast("Removing widget: " + appWidgetID, Toast.LENGTH_SHORT, GB.INFO); - } - } - - private void showLogSharingNotEnabledAlert() { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.note) - .setPositiveButton(R.string.ok, null) - .setMessage(R.string.share_log_not_enabled_message) - .show(); - } - - private void showLogSharingWarning() { - new MaterialAlertDialogBuilder(this) - .setCancelable(true) - .setTitle(R.string.warning) - .setMessage(R.string.share_log_warning) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - shareLog(); - } - }) - .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // do nothing - } - }) - .show(); - } - - private void testNewFunctionality() { - GBApplication.deviceService().onTestNewFunction(); - - //try (DBHandler db = GBApplication.acquireDB()) { - // db.getDatabase().execSQL("DROP TABLE IF EXISTS TABLE_NAME_TO_DROP_HERE"); - //} catch (final Exception e) { - // GB.log("Error accessing database", GB.ERROR, e); - //} - } - - private void shareLog() { - String fileName = GBApplication.getLogPath(); - if (fileName != null && fileName.length() > 0) { - if(GBApplication.isFileLoggingEnabled()) { - // 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); - } - - File logFile = new File(fileName); - if (!logFile.exists()) { - GB.toast("File does not exist", Toast.LENGTH_LONG, GB.INFO); - return; - } - - final Uri providerUri = FileProvider.getUriForFile( - this, - getApplicationContext().getPackageName() + ".screenshot_provider", - logFile - ); - - Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - emailIntent.setType("*/*"); - emailIntent.putExtra(EXTRA_SUBJECT, "Gadgetbridge log file"); - emailIntent.putExtra(Intent.EXTRA_STREAM, providerUri); - startActivity(Intent.createChooser(emailIntent, "Share File")); - } - } - - private void testNotification() { - Intent notificationIntent = new Intent(getApplicationContext(), DebugActivity.class); - notificationIntent.setPackage(BuildConfig.APPLICATION_ID); - notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TASK); - Context context = getApplicationContext(); - int flags1 = 0; - flags1 |= PendingIntent.FLAG_IMMUTABLE; - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, flags1); - - RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_REPLY) - .build(); - - Intent replyIntent = new Intent(ACTION_REPLY); - replyIntent.setPackage(BuildConfig.APPLICATION_ID); - - PendingIntent replyPendingIntent = PendingIntent.getBroadcast(this, 0, replyIntent, PendingIntent.FLAG_MUTABLE); - - NotificationCompat.Action action = - new NotificationCompat.Action.Builder(android.R.drawable.ic_input_add, "Reply", replyPendingIntent) - .addRemoteInput(remoteInput) - .build(); - - NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender().addAction(action); - - NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this, 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); - - GB.notify((int) System.currentTimeMillis(), ncomp.build(), this); - } - - private void testPebbleKitNotification() { - Intent pebbleKitIntent = new Intent("com.getpebble.action.SEND_NOTIFICATION"); - pebbleKitIntent.putExtra("messageType", "PEBBLE_ALERT"); - pebbleKitIntent.putExtra("notificationData", "[{\"title\":\"PebbleKitTest\",\"body\":\"sent from Gadgetbridge\"}]"); - getApplicationContext().sendBroadcast(pebbleKitIntent); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); - unregisterReceiver(mReceiver); - } - - private void addListenerOnSpinnerDeviceSelection(Spinner spinner) { - spinner.setOnItemSelectedListener(new CustomOnDeviceSelectedListener()); - } - - public static void createTestDevice(Context context, long deviceKey, String deviceMac, String deviceName) { - if (deviceKey == SELECT_DEVICE) { - return; - } - DeviceType deviceType = DeviceType.values()[(int) deviceKey]; - if(deviceName == null) { - int deviceNameResource = deviceType.getDeviceCoordinator().getDeviceNameResource(); - if(deviceNameResource == 0){ - deviceName = deviceType.name(); - }else { - deviceName = context.getString(deviceNameResource); - } - }; - try ( - DBHandler db = GBApplication.acquireDB()) { - DaoSession daoSession = db.getDaoSession(); - GBDevice gbDevice = new GBDevice(deviceMac, deviceName, "", null, deviceType); - gbDevice.setFirmwareVersion("N/A"); - gbDevice.setFirmwareVersion2("N/A"); - - //this causes the attributes (fw version) to be stored as well. Not much useful, but still... - gbDevice.setState(GBDevice.State.INITIALIZED); - - Device device = DBHelper.getDevice(gbDevice, daoSession); //the addition happens here - Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST); - LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent); - GB.toast(context, "Added test device: " + deviceName, Toast.LENGTH_SHORT, GB.INFO); - - } catch ( - Exception e) { - LOG.error("Error accessing database", e); - } - } - - private String randomMac() { - Random random = new Random(); - String separator = ":"; - String[] mac = { - String.format("%02x", random.nextInt(0xff)), - String.format("%02x", random.nextInt(0xff)), - String.format("%02x", random.nextInt(0xff)), - String.format("%02x", random.nextInt(0xff)), - String.format("%02x", random.nextInt(0xff)), - String.format("%02x", random.nextInt(0xff)) - }; - return TextUtils.join(separator, mac).toUpperCase(Locale.ROOT); - } - - private void displayWeatherInfo(final WeatherSpec weatherSpec) { - final String weatherInfo = getWeatherInfo(weatherSpec); - - new MaterialAlertDialogBuilder(DebugActivity.this) - .setCancelable(true) - .setTitle("Cached Weather Data") - .setMessage(weatherInfo) - .setPositiveButton(R.string.ok, (dialog, which) -> { - }) - .setNeutralButton(android.R.string.copy, (dialog, which) -> { - final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Weather Info", weatherInfo); - clipboard.setPrimaryClip(clip); - }) - .show(); - } - - private String getWeatherInfo(final WeatherSpec weatherSpec) { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ROOT); - - final StringBuilder builder = new StringBuilder(); - - if (weatherSpec == null) - return "Weather cache is empty."; - - builder.append("Location: ").append(weatherSpec.getLocation()).append("\n"); - builder.append("Timestamp: ").append(weatherSpec.getTimestamp()).append("\n"); - builder.append("Current Temp: ").append(weatherSpec.getCurrentTemp()).append(" K\n"); - builder.append("Max Temp: ").append(weatherSpec.getTodayMaxTemp()).append(" K\n"); - builder.append("Min Temp: ").append(weatherSpec.getTodayMinTemp()).append(" K\n"); - builder.append("Condition: ").append(weatherSpec.getCurrentCondition()).append("\n"); - builder.append("Condition Code: ").append(weatherSpec.getCurrentConditionCode()).append("\n"); - builder.append("Humidity: ").append(weatherSpec.getCurrentHumidity()).append("\n"); - builder.append("Wind Speed: ").append(weatherSpec.getWindSpeed()).append(" kmph\n"); - builder.append("Wind Direction: ").append(weatherSpec.getWindDirection()).append(" deg\n"); - builder.append("UV Index: ").append(weatherSpec.getUvIndex()).append("\n"); - builder.append("Precip Probability: ").append(weatherSpec.getPrecipProbability()).append(" %\n"); - builder.append("Dew Point: ").append(weatherSpec.getDewPoint()).append(" K\n"); - builder.append("Pressure: ").append(weatherSpec.getPressure()).append(" mb\n"); - builder.append("Cloud Cover: ").append(weatherSpec.getCloudCover()).append(" %\n"); - builder.append("Visibility: ").append(weatherSpec.getVisibility()).append(" m\n"); - builder.append("Sun Rise: ").append(sdf.format(new Date(weatherSpec.getSunRise() * 1000L))).append("\n"); - builder.append("Sun Set: ").append(sdf.format(new Date(weatherSpec.getSunSet() * 1000L))).append("\n"); - builder.append("Moon Rise: ").append(sdf.format(new Date(weatherSpec.getMoonRise() * 1000L))).append("\n"); - builder.append("Moon Set: ").append(sdf.format(new Date(weatherSpec.getMoonSet() * 1000L))).append("\n"); - builder.append("Moon Phase: ").append(weatherSpec.getMoonPhase()).append(" deg\n"); - builder.append("Latitude: ").append(weatherSpec.getLatitude()).append("\n"); - builder.append("Longitude: ").append(weatherSpec.getLongitude()).append("\n"); - builder.append("Feels Like Temp: ").append(weatherSpec.getFeelsLikeTemp()).append(" K\n"); - builder.append("Is Current Location: ").append(weatherSpec.getIsCurrentLocation()).append("\n"); - - if (weatherSpec.getAirQuality() != null) { - builder.append("Air Quality aqi: ").append(weatherSpec.getAirQuality().getAqi()).append("\n"); - builder.append("Air Quality co: ").append(weatherSpec.getAirQuality().getCo()).append("\n"); - builder.append("Air Quality no2: ").append(weatherSpec.getAirQuality().getNo2()).append("\n"); - builder.append("Air Quality o3: ").append(weatherSpec.getAirQuality().getO3()).append("\n"); - builder.append("Air Quality pm10: ").append(weatherSpec.getAirQuality().getPm10()).append("\n"); - builder.append("Air Quality pm25: ").append(weatherSpec.getAirQuality().getPm25()).append("\n"); - builder.append("Air Quality so2: ").append(weatherSpec.getAirQuality().getSo2()).append("\n"); - builder.append("Air Quality coAqi: ").append(weatherSpec.getAirQuality().getCoAqi()).append("\n"); - builder.append("Air Quality no2Aqi: ").append(weatherSpec.getAirQuality().getNo2Aqi()).append("\n"); - builder.append("Air Quality o3Aqi: ").append(weatherSpec.getAirQuality().getO3Aqi()).append("\n"); - builder.append("Air Quality pm10Aqi: ").append(weatherSpec.getAirQuality().getPm10Aqi()).append("\n"); - builder.append("Air Quality pm25Aqi: ").append(weatherSpec.getAirQuality().getPm25Aqi()).append("\n"); - builder.append("Air Quality so2Aqi: ").append(weatherSpec.getAirQuality().getSo2Aqi()).append("\n"); - } else { - builder.append("Air Quality: null\n"); - } - - int i = 0; - for (final WeatherSpec.Daily daily : weatherSpec.getForecasts()) { - builder.append("-------------\n"); - builder.append("-->Day ").append(i++).append("\n"); - builder.append("Max Temp: ").append(daily.getMaxTemp()).append(" K\n"); - builder.append("Min Temp: ").append(daily.getMinTemp()).append(" K\n"); - builder.append("Condition Code: ").append(daily.getConditionCode()).append("\n"); - builder.append("Humidity: ").append(daily.getHumidity()).append("\n"); - builder.append("Wind Speed: ").append(daily.getWindSpeed()).append(" kmph\n"); - builder.append("Wind Direction: ").append(daily.getWindDirection()).append(" deg\n"); - builder.append("UV Index: ").append(daily.getUvIndex()).append("\n"); - builder.append("Precip Probability: ").append(daily.getPrecipProbability()).append(" %\n"); - builder.append("Sun Rise: ").append(sdf.format(new Date(daily.getSunRise() * 1000L))).append("\n"); - builder.append("Sun Set: ").append(sdf.format(new Date(daily.getSunSet() * 1000L))).append("\n"); - builder.append("Moon Rise: ").append(sdf.format(new Date(daily.getMoonRise() * 1000L))).append("\n"); - builder.append("Moon Set: ").append(sdf.format(new Date(daily.getMoonSet() * 1000L))).append("\n"); - builder.append("Moon Phase: ").append(daily.getMoonPhase()).append(" deg\n"); - - if (daily.getAirQuality() != null) { - builder.append("Air Quality aqi: ").append(daily.getAirQuality().getAqi()).append("\n"); - builder.append("Air Quality co: ").append(daily.getAirQuality().getCo()).append("\n"); - builder.append("Air Quality no2: ").append(daily.getAirQuality().getNo2()).append("\n"); - builder.append("Air Quality o3: ").append(daily.getAirQuality().getO3()).append("\n"); - builder.append("Air Quality pm10: ").append(daily.getAirQuality().getPm10()).append("\n"); - builder.append("Air Quality pm25: ").append(daily.getAirQuality().getPm25()).append("\n"); - builder.append("Air Quality so2: ").append(daily.getAirQuality().getSo2()).append("\n"); - builder.append("Air Quality coAqi: ").append(daily.getAirQuality().getCoAqi()).append("\n"); - builder.append("Air Quality no2Aqi: ").append(daily.getAirQuality().getNo2Aqi()).append("\n"); - builder.append("Air Quality o3Aqi: ").append(daily.getAirQuality().getO3Aqi()).append("\n"); - builder.append("Air Quality pm10Aqi: ").append(daily.getAirQuality().getPm10Aqi()).append("\n"); - builder.append("Air Quality pm25Aqi: ").append(daily.getAirQuality().getPm25Aqi()).append("\n"); - builder.append("Air Quality so2Aqi: ").append(daily.getAirQuality().getSo2Aqi()).append("\n"); - } else { - builder.append("Air Quality: null\n"); - } - } - - builder.append("=============\n"); - - for (final WeatherSpec.Hourly hourly : weatherSpec.getHourly()) { - builder.append("-------------\n"); - builder.append("-->Hour: ").append(sdf.format(new Date(hourly.getTimestamp() * 1000L))).append("\n"); - builder.append("Max Temp: ").append(hourly.getTemp()).append(" K\n"); - builder.append("Condition Code: ").append(hourly.getConditionCode()).append("\n"); - builder.append("Humidity: ").append(hourly.getHumidity()).append("\n"); - builder.append("Wind Speed: ").append(hourly.getWindSpeed()).append(" kmph\n"); - builder.append("Wind Direction: ").append(hourly.getWindDirection()).append(" deg\n"); - builder.append("UV Index: ").append(hourly.getUvIndex()).append("\n"); - builder.append("Precip Probability: ").append(hourly.getPrecipProbability()).append(" %\n"); - } - - return builder.toString(); - } - - public static Map> getAllSupportedDevices(Context appContext) { - LinkedHashMap> newMap = new LinkedHashMap<>(1); - GBApplication app = (GBApplication) appContext; - for (DeviceType deviceType : DeviceType.values()) { - DeviceCoordinator coordinator = deviceType.getDeviceCoordinator(); - int icon = coordinator.getDefaultIconResource(); - String name = app.getString(coordinator.getDeviceNameResource()); - if (!name.startsWith(coordinator.getManufacturer())) { - name += " (" + coordinator.getManufacturer() + ")"; - } - long deviceId = deviceType.ordinal(); - newMap.put(name, new Pair<>(deviceId, icon)); - } - TreeMap > sortedMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - sortedMap.putAll(newMap); - newMap = new LinkedHashMap<>(1); - newMap.put(app.getString(R.string.widget_settings_select_device_title), new Pair<>(SELECT_DEVICE, R.drawable.ic_device_unknown)); - newMap.put(app.getString(R.string.devicetype_scannable), new Pair<>((long) DeviceType.SCANNABLE.ordinal(), R.drawable.ic_device_scannable)); - newMap.put(app.getString(R.string.devicetype_ble_gatt_client), new Pair<>((long) DeviceType.BLE_GATT_CLIENT.ordinal(), R.drawable.ic_device_scannable)); - - newMap.putAll(sortedMap); - - return newMap; - } - - public class CustomOnDeviceSelectedListener implements AdapterView.OnItemSelectedListener { - - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - SpinnerWithIconItem selectedItem = (SpinnerWithIconItem) parent.getItemAtPosition(pos); - selectedTestDeviceKey = selectedItem.getId(); - } - - @Override - public void onNothingSelected(AdapterView arg0) { - // TODO Auto-generated method stub - } - - } - -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 9e2b75b267..0bf82c78c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -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) -> { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/AbstractDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/AbstractDebugFragment.kt new file mode 100644 index 0000000000..fb014f3f05 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/AbstractDebugFragment.kt @@ -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(prefKey)!!.setOnPreferenceClickListener { + function.invoke() + return@setOnPreferenceClickListener true + } + } + + protected fun setInputTypeNumber(prefKey: String) { + findPreference(prefKey)!!.setOnBindEditTextListener { + it.setInputType(InputType.TYPE_CLASS_NUMBER) + } + } + + protected fun setListPreferenceEntries(prefKey: String, entries: Array) { + val statePref = findPreference(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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CallsDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CallsDebugFragment.kt new file mode 100644 index 0000000000..b12e9852e9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CallsDebugFragment.kt @@ -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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CompanionDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CompanionDebugFragment.kt new file mode 100644 index 0000000000..02b5e4f733 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/CompanionDebugFragment.kt @@ -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 = 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(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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseDebugFragment.kt new file mode 100644 index 0000000000..50d559ef3d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseDebugFragment.kt @@ -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(PREF_DEBUG_DELETE_OLD_DATABASE)!!.isVisible = false + } + + addTables() + } + + private fun addTables() { + val tablesHeader: PreferenceCategory = findPreference(PREF_HEADER_DATABASE_TABLES)!! + removeDynamicPrefs(tablesHeader) + + val tableNames = mutableListOf() + + try { + GBApplication.acquireDB().use { db -> + findPreference(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(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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseTableDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseTableDebugFragment.kt new file mode 100644 index 0000000000..7b2a98838b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DatabaseTableDebugFragment.kt @@ -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() + 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() + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DebugActivityV2.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DebugActivityV2.kt new file mode 100644 index 0000000000..8c7f58680e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DebugActivityV2.kt @@ -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() + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/LocationDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/LocationDebugFragment.kt new file mode 100644 index 0000000000..f0f8d19c52 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/LocationDebugFragment.kt @@ -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(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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt new file mode 100644 index 0000000000..9c44fd6585 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt @@ -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(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(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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MusicDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MusicDebugFragment.kt new file mode 100644 index 0000000000..00d6c33fef --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MusicDebugFragment.kt @@ -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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/NotificationsDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/NotificationsDebugFragment.kt new file mode 100644 index 0000000000..2cfd24e373 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/NotificationsDebugFragment.kt @@ -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(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() + + // 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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/OtherDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/OtherDebugFragment.kt new file mode 100644 index 0000000000..fad6c87b04 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/OtherDebugFragment.kt @@ -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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/PreferencesDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/PreferencesDebugFragment.kt new file mode 100644 index 0000000000..8d983bab7d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/PreferencesDebugFragment.kt @@ -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(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(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"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherDebugFragment.kt new file mode 100644 index 0000000000..b5572c7c41 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherDebugFragment.kt @@ -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(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 = 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 List.moveTo(from: Int, to: Int): List { + val target = to.coerceIn(indices) + if (from == target) return this + + return toMutableList().apply { + add(target, removeAt(from)) + } + } + + private fun onWeatherClick(weatherSpecs: List, i: Int) { + val weatherSpec = weatherSpecs[i] + + val items: MutableList = 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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherSpecDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherSpecDebugFragment.kt new file mode 100644 index 0000000000..0fed6e0b3b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WeatherSpecDebugFragment.kt @@ -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")!! + } + 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() ?: "", 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} %") + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WidgetsDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WidgetsDebugFragment.kt new file mode 100644 index 0000000000..c2be834613 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/WidgetsDebugFragment.kt @@ -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 = 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" + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/DebugPreference.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/DebugPreference.java new file mode 100644 index 0000000000..e624add808 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/DebugPreference.java @@ -0,0 +1,4 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.debug.preferences; + +public record DebugPreference(String key, String value) { +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerActivity.java new file mode 100644 index 0000000000..0afea786fd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerActivity.java @@ -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 . */ +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 allPrefs = new ArrayList<>(); + private PreferenceManagerAdapter listAdapter; + + private final ActivityResultLauncher exportFileChooser = registerForActivityResult( + new ActivityResultContracts.CreateDocument("application/json"), + uri -> { + LOG.info("Got target export file: {}", uri); + if (uri != null) { + exportPreferences(uri); + } + } + ); + + private final ActivityResultLauncher 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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerAdapter.java new file mode 100644 index 0000000000..7da872fb8c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/preferences/PreferenceManagerAdapter.java @@ -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 . */ +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 { + protected static final Logger LOG = LoggerFactory.getLogger(PreferenceManagerAdapter.class); + + private final List preferenceList; + private final Context mContext; + + private final PreferenceFilter preferenceFilter; + + public PreferenceManagerAdapter(final Context context, final List 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 originalList; + private final List filteredList; + + private PreferenceFilter(final PreferenceManagerAdapter adapter, final List 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) filterResults.values); + adapter.notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java index 2ebc3bf2a5..aa2a6b66f0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java @@ -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 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> allDevices = DebugActivity.getAllSupportedDevices(getApplicationContext()); - - final LinearLayout linearLayout = new LinearLayout(DiscoveryActivityV2.this); - linearLayout.setOrientation(LinearLayout.VERTICAL); - - final ArrayList deviceListArray = new ArrayList<>(); - for (Map.Entry> 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 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java index 517dfd3251..fb29b8dfe1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/files/FileManagerActivity.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index fc73ed24fb..2a27a100d6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -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. */ 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(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java index 1de74ee982..42f12b36d2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java @@ -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() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WeatherSpec.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WeatherSpec.kt index c3a7090ba4..a430248c92 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WeatherSpec.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WeatherSpec.kt @@ -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 = ArrayList() + var forecasts: ArrayList = ArrayList() // Hourly forecasts - var hourly: ArrayList = ArrayList() + var hourly: ArrayList = 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 + } } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index dd3bc117f1..40ef57b6b0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -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: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index 472786e720..979a6c0adf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java index 30e4068269..d76bb3a9d6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java @@ -49,10 +49,12 @@ public class MediaManager { this.context = context; } + @Nullable public MusicSpec getBufferMusicSpec() { return bufferMusicSpec; } + @Nullable public MusicStateSpec getBufferMusicStateSpec() { return bufferMusicStateSpec; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/TestDeviceDialog.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/TestDeviceDialog.kt new file mode 100644 index 0000000000..a469b98647 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/TestDeviceDialog.kt @@ -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 = getAllSupportedDevices(activity) + + // Get unique manufacturers + val manufacturerMap = mutableMapOf>>() + val manufacturers = mutableSetOf() + 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 { 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() + 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 { + var newMap = LinkedHashMap(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.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.. + 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) + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java index 12309a492c..137b8c4f60 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WidgetPreferenceStorage.java @@ -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 diff --git a/app/src/main/res/drawable/ic_science.xml b/app/src/main/res/drawable/ic_science.xml new file mode 100644 index 0000000000..74ccff2f2a --- /dev/null +++ b/app/src/main/res/drawable/ic_science.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml new file mode 100644 index 0000000000..e0cf11cb43 --- /dev/null +++ b/app/src/main/res/drawable/ic_send.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/activity_data_management.xml b/app/src/main/res/layout/activity_data_management.xml index 7dc33d6faf..d35deb8c71 100644 --- a/app/src/main/res/layout/activity_data_management.xml +++ b/app/src/main/res/layout/activity_data_management.xml @@ -125,74 +125,6 @@ android:layout_weight="1" android:text="@string/activity_DB_ShowContentButton" /> - - - - - - - -