/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Frank Slezak, ivanovlev, Kasha, Lem Dulfo, Pavel Elagin, Steffen Liebergeld, vanous 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; import static android.content.Intent.EXTRA_SUBJECT; import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID; import android.app.Activity; import android.app.AlertDialog; 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.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.preference.PreferenceManager; import android.text.Editable; 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.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.app.NavUtils; import androidx.core.app.NotificationCompat; import androidx.core.app.RemoteInput; import androidx.core.content.FileProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; 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.adapter.SpinnerWithIconAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; 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.GBLocationManager; 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; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; 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 = 999L; 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); registerReceiver(mReceiver, filter); // 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.pebbleColor = notificationSpec.type.color; notificationSpec.attachedActions = new ArrayList<>(); if (notificationSpec.type == NotificationType.GENERIC_SMS) { // REPLY action NotificationSpec.Action replyAction = new NotificationSpec.Action(); replyAction.title = "Reply"; replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR; 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(); 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 AlertDialog.Builder(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.getInstance().getWeatherSpec() == null) { final WeatherSpec weatherSpec = new WeatherSpec(); weatherSpec.forecasts = new ArrayList<>(); weatherSpec.location = "Green Hill"; weatherSpec.currentConditionCode = 601; // snow weatherSpec.currentCondition = Weather.getConditionString(weatherSpec.currentConditionCode); weatherSpec.currentTemp = 15 + 273; weatherSpec.currentHumidity = 30; weatherSpec.windSpeed = 10; weatherSpec.windDirection = 12; weatherSpec.timestamp = (int) (System.currentTimeMillis() / 1000); weatherSpec.todayMinTemp = 10 + 273; weatherSpec.todayMaxTemp = 25 + 273; for (int i = 0; i < 5; i++) { final WeatherSpec.Forecast gbForecast = new WeatherSpec.Forecast(); gbForecast.minTemp = 10 + i + 273; gbForecast.maxTemp = 25 + i + 273; gbForecast.conditionCode = 800; // clear weatherSpec.forecasts.add(gbForecast); } Weather.getInstance().setWeatherSpec(weatherSpec); } GBApplication.deviceService().onSendWeather(Weather.getInstance().getWeatherSpec()); } }); 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) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(DebugActivity.this); Prefs prefs = new Prefs(sharedPrefs); boolean logging_enabled = prefs.getBoolean("log_to_file", false); if (logging_enabled) { 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 AlertDialog.Builder(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) { LinkedHashMap> allDevices; 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); 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 AlertDialog.Builder(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); } }) .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) { GBLocationManager.stopAll(getBaseContext()); } }); 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); String companionDevicesList = String.format(Locale.ROOT, "%d companion devices", associations.size()); if (!associations.isEmpty()) { companionDevicesList += "\n\n" + StringUtils.join("\n", associations.toArray(new String[0])); } new AlertDialog.Builder(DebugActivity.this) .setCancelable(false) .setTitle("Companion Devices") .setMessage(companionDevicesList) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int 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 AlertDialog.Builder fitnesStatusBuilder = new AlertDialog.Builder(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); } }); } @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) GBApplication.getContext().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(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 AlertDialog.Builder(this) .setTitle(R.string.note) .setPositiveButton(R.string.ok, null) .setMessage(R.string.share_log_not_enabled_message) .show(); } private void showLogSharingWarning() { new AlertDialog.Builder(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(); } private void shareLog() { String fileName = GBApplication.getLogPath(); if (fileName != null && fileName.length() > 0) { 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.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(getApplicationContext(), 0, notificationIntent, 0, false); RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_REPLY) .build(); Intent replyIntent = new Intent(ACTION_REPLY); PendingIntent replyPendingIntent = PendingIntentUtils.getBroadcast(this, 0, replyIntent, 0, true); 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(MenuItem item) { switch (item.getItemId()) { case 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()); } protected static void createTestDevice(Context context, long deviceKey, String deviceMac) { if (deviceKey == SELECT_DEVICE) { return; } DeviceType deviceType = DeviceType.fromKey((int) deviceKey); try ( DBHandler db = GBApplication.acquireDB()) { DaoSession daoSession = db.getDaoSession(); GBDevice gbDevice = new GBDevice(deviceMac, deviceType.name(), "", 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: " + deviceType.name(), Toast.LENGTH_SHORT, GB.INFO); } catch ( Exception e) { GB.log("Error accessing database", GB.ERROR, 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); } public static LinkedHashMap getAllSupportedDevices(Context appContext) { LinkedHashMap> newMap = new LinkedHashMap<>(1); GBApplication app = (GBApplication) appContext; for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) { DeviceType deviceType = coordinator.getDeviceType(); int icon = deviceType.getIcon(); String name = app.getString(deviceType.getName()) + " (" + coordinator.getManufacturer() + ")"; long deviceId = deviceType.getKey(); newMap.put(name, new Pair(deviceId, icon)); } TreeMap > sortedMap = new TreeMap<>(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.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 } } }