ble: introduce CompanionDeviceService to elevate process priority

This commit is contained in:
Thomas Kuehne
2025-06-22 12:48:01 +02:00
committed by José Rebelo
parent 485df6a021
commit fcd4ac30be
5 changed files with 223 additions and 4 deletions
+12
View File
@@ -69,6 +69,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
<!-- Used for reverse find device -->
<uses-permission android:name="android.permission.VIBRATE" />
@@ -535,6 +536,17 @@
android:exported="false"
android:foregroundServiceType="connectedDevice" />
<service
android:name=".service.GBCompanionDeviceService"
android:exported="true"
android:label="GB CompanionDevice Service"
android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE"
tools:targetApi="31">
<intent-filter>
<action android:name="android.companion.CompanionDeviceService" />
</intent-filter>
</service>
<receiver
android:name=".externalevents.GenericWeatherReceiver"
android:enabled="true"
@@ -85,6 +85,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@@ -168,6 +169,7 @@ public class GBApplication extends Application {
public static void quit() {
GB.log("Quitting Gadgetbridge...", GB.INFO, null);
BondingUtil.StopObservingAll(getContext());
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
@@ -176,6 +178,7 @@ public class GBApplication extends Application {
public static void restart() {
GB.log("Restarting Gadgetbridge...", GB.INFO, null);
BondingUtil.StopObservingAll(getContext());
final Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
@@ -288,6 +291,8 @@ public class GBApplication extends Application {
.build(), context);
}
}
BondingUtil.StartObservingAll(getBaseContext());
}
@Override
@@ -28,7 +28,6 @@ import android.content.IntentFilter;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.MotionEvent;
@@ -0,0 +1,110 @@
/* Copyright (C) 2025 Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceService;
import android.companion.DevicePresenceEvent;
import android.os.Build;
import androidx.annotation.DeprecatedSinceApi;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.AutoConnectIntervalReceiver;
/**
* For now this service only ensures that GB is less likely to get killed.
* See {@link #maybeConnect}. Android API documentation:
* <blockquote>
* The system binding CompanionDeviceService elevates the priority of the process that the service
* is running in, and thus may prevent the Low-memory killer from killing the process at expense of
* other processes with lower priority.
* </blockquote>
*/
@RequiresApi(Build.VERSION_CODES.S)
public class GBCompanionDeviceService extends CompanionDeviceService {
private static final Logger LOG = LoggerFactory.getLogger(GBCompanionDeviceService.class);
@DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
public void onDeviceAppeared(@NonNull String address) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
LOG.debug("onDeviceAppeared address:{}", address);
maybeConnect();
}
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@DeprecatedSinceApi(api = Build.VERSION_CODES.BAKLAVA)
@Override
public void onDeviceAppeared(@NonNull AssociationInfo associationInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) {
LOG.debug("onDeviceAppeared association:{}", associationInfo.getDeviceMacAddress());
maybeConnect();
}
}
@RequiresApi(api = Build.VERSION_CODES.BAKLAVA)
@Override
public void onDevicePresenceEvent(@NonNull DevicePresenceEvent event) {
final int code = event.getEvent();
final String type = switch (code) {
case DevicePresenceEvent.EVENT_BLE_APPEARED -> "EVENT_BLE_APPEARED";
case DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> "EVENT_BLE_DISAPPEARED";
case DevicePresenceEvent.EVENT_BT_CONNECTED -> "EVENT_BT_CONNECTED";
case DevicePresenceEvent.EVENT_BT_DISCONNECTED -> "EVENT_BT_DISCONNECTED";
case DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED -> "EVENT_SELF_MANAGED_APPEARED";
case DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED ->
"EVENT_SELF_MANAGED_DISAPPEARED";
default -> Integer.toString(code);
};
LOG.debug("onDevicePresenceEvent {} association:{}", type, event.getAssociationId());
switch (code) {
case DevicePresenceEvent.EVENT_BLE_APPEARED:
case DevicePresenceEvent.EVENT_BT_CONNECTED:
case DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED:
maybeConnect();
break;
}
}
/**
* FIXME {@link DeviceCommunicationService#connectToDevice} has to play nicer with
* concurrent reconnect triggers before code can be added here
* <ol>
* <li>{@link AutoConnectIntervalReceiver}</li>
* <li>{@link BluetoothConnectReceiver}</li>
* <li>{@link DeviceService#ACTION_CONNECT}</li>
* <li>device specific - e.g. {@link BtLEQueue#maybeReconnect}'s {@code mBluetoothGatt.connect}</li>
* </ol>
*/
private void maybeConnect() {
// TODO check DEVICE_CONNECT_BACK, WAITING_FOR_RECONNECT and WAITING_FOR_SCAN
// and conditionally try to establish a connection
}
}
@@ -1,5 +1,5 @@
/* Copyright (C) 2020-2024 Andreas Böhler, Arjan Schrijver, Daniel Dakhno,
José Rebelo, Taavi Eomäe
/* Copyright (C) 2020-2025 Andreas Böhler, Arjan Schrijver, Daniel Dakhno,
José Rebelo, Taavi Eomäe, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -310,18 +310,20 @@ public class BondingUtil {
} else if (extra instanceof final ScanResult scanResult) {
deviceToPair = scanResult.getDevice();
} else {
LOG.error(" handleActivityResult unexpected EXTRA_DEVICE {}", extra);
LOG.error("handleActivityResult unexpected EXTRA_DEVICE {}", extra);
deviceToPair = null;
}
if (deviceToPair != null) {
if (deviceToPair.getAddress().equals(bondingInterface.getMacAddress())) {
StartObserving(bondingInterface.getContext(), deviceToPair.getAddress());
if (deviceToPair.getBondState() != BluetoothDevice.BOND_BONDED) {
BondingUtil.bluetoothBond(bondingInterface, bondingInterface.getCurrentTarget().getDevice());
} else {
bondingInterface.onBondingComplete(true);
}
} else {
LOG.debug("handleActivityResult unexpected device {}", deviceToPair);
bondingInterface.onBondingComplete(false);
}
}
@@ -383,6 +385,7 @@ public class BondingUtil {
for (String association : manager.getAssociations()) {
LOG.debug(String.format("Already associated with: %s", association));
if (association.equals(macAddress)) {
StartObserving(bondingInterface.getContext(), macAddress);
LOG.info("The device has already been bonded through CompanionDeviceManager, using regular");
// If it's already "associated", we should immediately pair
// because the callback is never called (AFAIK?)
@@ -515,6 +518,93 @@ public class BondingUtil {
};
}
public static boolean StartObservingAll(Context context) {
return StartObserving(context, null);
}
public static boolean StartObserving(Context context, String mac) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
LOG.info("StartObserving - API {} < {} is too old",
Build.VERSION.SDK_INT, Build.VERSION_CODES.S);
return false;
}
if (mac != null && !BluetoothAdapter.checkBluetoothAddress(mac)) {
LOG.warn("StartObserving - mac '{}' is invalid", mac);
return false;
}
final CompanionDeviceManager manager = getCompanionDeviceManager(context);
if (manager == null) {
LOG.warn("StartObserving - CompanionDeviceManager is null");
return false;
}
boolean success = false;
final List<String> addresses;
if (mac == null) {
addresses = manager.getAssociations();
LOG.debug("StartObserving - {} associations", addresses.size());
} else {
addresses = Collections.singletonList(mac);
}
for (final String address : addresses) {
try {
LOG.debug("StartObserving - {}", address);
manager.startObservingDevicePresence(address);
success = true;
} catch (Exception e) {
LOG.warn("StartObserving - exception", e);
}
}
return success;
}
public static boolean StopObservingAll(Context context) {
return StopObserving(context, null);
}
public static boolean StopObserving(final Context context, final String mac) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
LOG.info("StopObserving - API {} < {} is too old",
Build.VERSION.SDK_INT, Build.VERSION_CODES.S);
return false;
}
if (mac != null && !BluetoothAdapter.checkBluetoothAddress(mac)) {
LOG.warn("StopObserving - mac '{}' is invalid", mac);
return false;
}
final CompanionDeviceManager manager = getCompanionDeviceManager(context);
if (manager == null) {
LOG.warn("StopObserving - CompanionDeviceManager is null");
return false;
}
boolean success = false;
final List<String> addresses;
if (mac == null) {
addresses = manager.getAssociations();
LOG.debug("StopObserving - {} associations", addresses.size());
} else {
addresses = Collections.singletonList(mac);
}
for (final String address : addresses) {
try {
LOG.debug("StopObserving - {}", address);
manager.stopObservingDevicePresence(address);
success = true;
} catch (Exception e) {
LOG.warn("StopObserving - exception {}", e.getMessage());
}
}
return success;
}
public static boolean Disassociate(Context context, String mac) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
LOG.info("Disassociate - API {} < {} is too old",
@@ -532,6 +622,7 @@ public class BondingUtil {
LOG.warn("Disassociate - CompanionDeviceManager is null");
} else {
try {
StopObserving(context, mac);
LOG.debug("Disassociate - {}", mac);
manager.disassociate(mac);
return true;
@@ -549,6 +640,8 @@ public class BondingUtil {
return false;
}
StopObserving(context, mac);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) {
LOG.debug("Unpair - API {} < {} is too old for modern bond removal",
Build.VERSION.SDK_INT, Build.VERSION_CODES.BAKLAVA);