mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
ble: introduce CompanionDeviceService to elevate process priority
This commit is contained in:
committed by
José Rebelo
parent
485df6a021
commit
fcd4ac30be
@@ -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;
|
||||
|
||||
+110
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user