2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 01:51:47 +00:00

Migrate BackgroundService to Kotlin

This commit is contained in:
TPJ Schikhof 2024-10-02 03:51:59 +00:00 committed by Philip Cohn-Cort
parent 65a71696d5
commit 7ee33de646
2 changed files with 291 additions and 304 deletions

View File

@ -1,304 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity;
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity;
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin;
import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
/*
* This class (still) does 3 things:
* - Keeps the app running by creating a foreground notification.
* - Holds references to the active LinkProviders, but doesn't handle the DeviceLink those create (the KdeConnect class does that).
* - Listens for network connectivity changes and tells the LinkProviders to re-check for devices.
* It can be started by the KdeConnectBroadcastReceiver on some events or when the MainActivity is launched.
*/
public class BackgroundService extends Service {
private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static BackgroundService instance;
private KdeConnect applicationInstance;
private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>();
public static BackgroundService getInstance() {
return instance;
}
private static boolean initialized = false;
// This indicates when connected over wifi/usb/bluetooth/(anything other than cellular)
private final MutableLiveData<Boolean> connectedToNonCellularNetwork = new MutableLiveData<>();
public LiveData<Boolean> isConnectedToNonCellularNetwork() {
return connectedToNonCellularNetwork;
}
public void updateForegroundNotification() {
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
//Update the foreground notification with the currently connected device list
NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class);
nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
}
private void registerLinkProviders() {
linkProviders.add(new LanLinkProvider(this));
// linkProviders.add(new LoopbackLinkProvider(this));
linkProviders.add(new BluetoothLinkProvider(this));
}
public void onNetworkChange(@Nullable Network network) {
if (!initialized) {
Log.d("KDE/BackgroundService", "ignoring onNetworkChange called before the service is initialized");
return;
}
Log.d("KDE/BackgroundService", "onNetworkChange");
for (BaseLinkProvider a : linkProviders) {
a.onNetworkChange(network);
}
}
public void addConnectionListener(BaseLinkProvider.ConnectionReceiver cr) {
for (BaseLinkProvider a : linkProviders) {
a.addConnectionReceiver(cr);
}
}
public void removeConnectionListener(BaseLinkProvider.ConnectionReceiver cr) {
for (BaseLinkProvider a : linkProviders) {
a.removeConnectionReceiver(cr);
}
}
//This will called only once, even if we launch the service intent several times
@Override
public void onCreate() {
super.onCreate();
Log.d("KdeConnect/BgService", "onCreate");
instance = this;
KdeConnect.getInstance().addDeviceListChangedCallback("BackgroundService", this::updateForegroundNotification);
// Register screen on listener
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
// See: https://developer.android.com/reference/android/net/ConnectivityManager.html#CONNECTIVITY_ACTION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
}
registerReceiver(new KdeConnectBroadcastReceiver(), filter);
// Watch for changes on all network connections except cellular networks
NetworkRequest.Builder networkRequestBuilder = getNonCellularNetworkRequestBuilder();
ConnectivityManager cm = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
cm.registerNetworkCallback(networkRequestBuilder.build(), new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
Log.i("BackgroundService", "Valid network available");
connectedToNonCellularNetwork.postValue(true);
onNetworkChange(network);
}
@Override
public void onLost(Network network) {
Log.i("BackgroundService", "Valid network lost");
connectedToNonCellularNetwork.postValue(false);
}
});
applicationInstance = KdeConnect.getInstance();
registerLinkProviders();
addConnectionListener(applicationInstance.getConnectionListener()); // Link Providers need to be already registered
for (BaseLinkProvider a : linkProviders) {
a.onStart();
}
initialized = true;
}
private static NetworkRequest.Builder getNonCellularNetworkRequestBuilder() {
NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_BLUETOOTH);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE);
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_USB)
.addTransportType(NetworkCapabilities.TRANSPORT_LOWPAN);
}
return networkRequestBuilder;
}
public void changePersistentNotificationVisibility(boolean visible) {
if (visible) {
updateForegroundNotification();
} else {
Stop();
Start(this);
}
}
private Notification createForegroundNotification() {
//Why is this needed: https://developer.android.com/guide/components/services#Foreground
ArrayList<String> connectedDevices = new ArrayList<>();
ArrayList<String> connectedDeviceIds = new ArrayList<>();
for (Device device : applicationInstance.getDevices().values()) {
if (device.isReachable() && device.isPaired()) {
connectedDeviceIds.add(device.getDeviceId());
connectedDevices.add(device.getName());
}
}
Intent intent = new Intent(this, MainActivity.class);
if (connectedDeviceIds.size() == 1) {
// Force open screen of the only connected device
intent.putExtra(MainActivity.EXTRA_DEVICE_ID, connectedDeviceIds.get(0));
}
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, NotificationHelper.Channels.PERSISTENT);
notification
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.setContentIntent(pi)
.setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW
.setShowWhen(false)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setAutoCancel(false);
notification.setGroup("BackgroundService");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
//Pre-oreo, the notification will have an empty title line without this
notification.setContentTitle(getString(R.string.kde_connect));
}
if (connectedDevices.isEmpty()) {
notification.setContentText(getString(R.string.foreground_notification_no_devices));
} else {
notification.setContentText(getString(R.string.foreground_notification_devices, TextUtils.join(", ", connectedDevices)));
// Adding an action button to send clipboard manually in Android 10 and later.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P &&
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_LOGS) == PackageManager.PERMISSION_DENIED) {
Intent sendClipboard = ClipboardFloatingActivity.getIntent(this, true);
PendingIntent sendPendingClipboard = PendingIntent.getActivity(this, 3, sendClipboard, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
notification.addAction(0, getString(R.string.foreground_notification_send_clipboard), sendPendingClipboard);
}
if (connectedDeviceIds.size() == 1) {
String deviceId = connectedDeviceIds.get(0);
Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device != null) {
// Adding two action buttons only when there is a single device connected.
// Setting up Send File Intent.
Intent sendFile = new Intent(this, SendFileActivity.class);
sendFile.putExtra("deviceId", deviceId);
PendingIntent sendPendingFile = PendingIntent.getActivity(this, 1, sendFile, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
notification.addAction(0, getString(R.string.send_files), sendPendingFile);
// Checking if there are registered commands and adding the button.
RunCommandPlugin plugin = (RunCommandPlugin) device.getPlugin("RunCommandPlugin");
if (plugin != null && !plugin.getCommandList().isEmpty()) {
Intent runCommand = new Intent(this, RunCommandActivity.class);
runCommand.putExtra("deviceId", connectedDeviceIds.get(0));
PendingIntent runPendingCommand = PendingIntent.getActivity(this, 2, runCommand, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
notification.addAction(0, getString(R.string.pref_plugin_runcommand), runPendingCommand);
}
}
}
}
return notification.build();
}
@Override
public void onDestroy() {
Log.d("KdeConnect/BgService", "onDestroy");
initialized = false;
for (BaseLinkProvider a : linkProviders) {
a.onStop();
}
KdeConnect.getInstance().removeDeviceListChangedCallback("BackgroundService");
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("KDE/BackgroundService", "onStartCommand");
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
} else {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
}
if (intent != null && intent.getBooleanExtra("refresh", false)) {
onNetworkChange(null);
}
return Service.START_STICKY;
}
public static void Start(Context context) {
Log.d("KDE/BackgroundService", "Start");
Intent intent = new Intent(context, BackgroundService.class);
ContextCompat.startForegroundService(context, intent);
}
public static void ForceRefreshConnections(Context context) {
Log.d("KDE/BackgroundService", "ForceRefreshConnections");
Intent intent = new Intent(context, BackgroundService.class);
intent.putExtra("refresh", true);
ContextCompat.startForegroundService(context, intent);
}
public void Stop() {
stopForeground(true);
}
}

View File

@ -0,0 +1,291 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.kde.kdeconnect.Backends.BaseLinkProvider
import org.kde.kdeconnect.Backends.BaseLinkProvider.ConnectionReceiver
import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider
import org.kde.kdeconnect.Helpers.NotificationHelper
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin
import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity
import org.kde.kdeconnect.UserInterface.MainActivity
import org.kde.kdeconnect_tp.R
/**
* This class (still) does 3 things:
* - Keeps the app running by creating a foreground notification.
* - Holds references to the active LinkProviders, but doesn't handle the DeviceLink those create (the KdeConnect class does that).
* - Listens for network connectivity changes and tells the LinkProviders to re-check for devices.
* It can be started by the KdeConnectBroadcastReceiver on some events or when the MainActivity is launched.
*/
class BackgroundService : Service() {
private lateinit var applicationInstance: KdeConnect
private val linkProviders = ArrayList<BaseLinkProvider>()
private val connectedToNonCellularNetwork = MutableLiveData<Boolean>()
/** Indicates whether device is connected over wifi / usb / bluetooth / (anything other than cellular) */
val isConnectedToNonCellularNetwork: LiveData<Boolean>
get() = connectedToNonCellularNetwork
fun updateForegroundNotification() {
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
// Update the foreground notification with the currently connected device list
val notificationManager = getSystemService<NotificationManager>()
notificationManager?.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification())
}
}
private fun registerLinkProviders() {
linkProviders.add(LanLinkProvider(this))
// linkProviders.add(new LoopbackLinkProvider(this));
linkProviders.add(BluetoothLinkProvider(this))
}
fun onNetworkChange(network: Network?) {
if (!initialized) {
Log.d(LOG_TAG, "ignoring onNetworkChange called before the service is initialized")
return
}
Log.d(LOG_TAG, "onNetworkChange")
for (linkProvider in linkProviders) {
linkProvider.onNetworkChange(network)
}
}
fun addConnectionListener(connectionReceiver: ConnectionReceiver) {
for (linkProvider in linkProviders) {
linkProvider.addConnectionReceiver(connectionReceiver)
}
}
fun removeConnectionListener(connectionReceiver: ConnectionReceiver) {
for (linkProvider in linkProviders) {
linkProvider.removeConnectionReceiver(connectionReceiver)
}
}
/** This will called only once, even if we launch the service intent several times */
override fun onCreate() {
super.onCreate()
Log.d("KdeConnect/BgService", "onCreate")
this.applicationInstance = KdeConnect.getInstance()
instance = this
KdeConnect.getInstance().addDeviceListChangedCallback("BackgroundService", this::updateForegroundNotification)
// Register screen on listener
val filter = IntentFilter(Intent.ACTION_SCREEN_ON)
// See: https://developer.android.com/reference/android/net/ConnectivityManager.html#CONNECTIVITY_ACTION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
}
registerReceiver(KdeConnectBroadcastReceiver(), filter)
// Watch for changes on all network connections except cellular networks
val networkRequestBuilder = createNonCellularNetworkRequestBuilder()
val connectivityManager = this.getSystemService<ConnectivityManager>()
connectivityManager?.registerNetworkCallback(networkRequestBuilder.build(), object : NetworkCallback() {
override fun onAvailable(network: Network) {
Log.i("BackgroundService", "Valid network available")
connectedToNonCellularNetwork.postValue(true)
onNetworkChange(network)
}
override fun onLost(network: Network) {
Log.i("BackgroundService", "Valid network lost")
connectedToNonCellularNetwork.postValue(false)
}
})
registerLinkProviders()
addConnectionListener(applicationInstance.connectionListener) // Link Providers need to be already registered
for (linkProvider in linkProviders) {
linkProvider.onStart()
}
initialized = true
}
fun changePersistentNotificationVisibility(visible: Boolean) {
if (visible) {
updateForegroundNotification()
}
else {
stopForeground(true)
Start(this)
}
}
private fun createForegroundNotification(): Notification {
// Why is this needed: https://developer.android.com/guide/components/services#Foreground
val connectedDevices = ArrayList<String>()
val connectedDeviceIds = ArrayList<String>()
for (device in applicationInstance.devices.values) {
if (device.isReachable && device.isPaired) {
connectedDeviceIds.add(device.deviceId)
connectedDevices.add(device.name)
}
}
val intent = Intent(this, MainActivity::class.java)
if (connectedDeviceIds.size == 1) {
// Force open screen of the only connected device
intent.putExtra(MainActivity.EXTRA_DEVICE_ID, connectedDeviceIds[0])
}
val pi = PendingIntent.getActivity(this, 0, intent, UPDATE_MUTABLE_FLAGS)
val notification = NotificationCompat.Builder(this, NotificationHelper.Channels.PERSISTENT).apply {
setSmallIcon(R.drawable.ic_notification)
setOngoing(true)
setContentIntent(pi)
setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW
setShowWhen(false)
setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
setAutoCancel(false)
setGroup("BackgroundService")
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Pre-oreo, the notification will have an empty title line without this
notification.setContentTitle(getString(R.string.kde_connect))
}
if (connectedDevices.isEmpty()) {
notification.setContentText(getString(R.string.foreground_notification_no_devices))
}
else {
notification.setContentText(getString(R.string.foreground_notification_devices, connectedDevices.joinToString(", ")))
// Adding an action button to send clipboard manually in Android 10 and later.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_LOGS) == PackageManager.PERMISSION_DENIED) {
val sendClipboard = ClipboardFloatingActivity.getIntent(this, true)
val sendPendingClipboard = PendingIntent.getActivity(this, 3, sendClipboard, UPDATE_MUTABLE_FLAGS)
notification.addAction(0, getString(R.string.foreground_notification_send_clipboard), sendPendingClipboard)
}
if (connectedDeviceIds.size == 1) {
val deviceId = connectedDeviceIds[0]
val device = KdeConnect.getInstance().getDevice(deviceId)
if (device != null) {
// Adding two action buttons only when there is a single device connected.
// Setting up Send File Intent.
val sendFile = Intent(this, SendFileActivity::class.java)
sendFile.putExtra("deviceId", deviceId)
val sendPendingFile = PendingIntent.getActivity(this, 1, sendFile, UPDATE_MUTABLE_FLAGS)
notification.addAction(0, getString(R.string.send_files), sendPendingFile)
// Checking if there are registered commands and adding the button.
val plugin = device.getPlugin("RunCommandPlugin") as RunCommandPlugin?
if (plugin != null && plugin.commandList.isNotEmpty()) {
val runCommand = Intent(this, RunCommandActivity::class.java)
runCommand.putExtra("deviceId", connectedDeviceIds[0])
val runPendingCommand = PendingIntent.getActivity(this, 2, runCommand, UPDATE_MUTABLE_FLAGS)
notification.addAction(0, getString(R.string.pref_plugin_runcommand), runPendingCommand)
}
}
}
}
return notification.build()
}
override fun onDestroy() {
Log.d("KdeConnect/BgService", "onDestroy")
initialized = false
for (linkProvider in linkProviders) {
linkProvider.onStop()
}
KdeConnect.getInstance().removeDeviceListChangedCallback("BackgroundService")
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(LOG_TAG, "onStartCommand")
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
}
else {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification())
}
}
if (intent != null && intent.getBooleanExtra("refresh", false)) {
onNetworkChange(null)
}
return START_STICKY
}
companion object {
const val LOG_TAG = "KDE/BackgroundService"
const val UPDATE_MUTABLE_FLAGS = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
private const val FOREGROUND_NOTIFICATION_ID = 1
@JvmStatic
var instance: BackgroundService? = null
private set
private var initialized = false
private fun createNonCellularNetworkRequestBuilder(): NetworkRequest.Builder {
return NetworkRequest.Builder().apply {
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
addTransportType(NetworkCapabilities.TRANSPORT_VPN)
addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
addTransportType(NetworkCapabilities.TRANSPORT_BLUETOOTH)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
addTransportType(NetworkCapabilities.TRANSPORT_USB)
addTransportType(NetworkCapabilities.TRANSPORT_LOWPAN)
}
}
}
fun Start(context: Context) {
Log.d(LOG_TAG, "Start")
val intent = Intent(context, BackgroundService::class.java)
ContextCompat.startForegroundService(context, intent)
}
@JvmStatic
fun ForceRefreshConnections(context: Context) {
Log.d(LOG_TAG, "ForceRefreshConnections")
val intent = Intent(context, BackgroundService::class.java)
intent.putExtra("refresh", true)
ContextCompat.startForegroundService(context, intent)
}
}
}