File indexing completed on 2024-12-22 04:41:42
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect; 0008 0009 import android.Manifest; 0010 import android.app.Notification; 0011 import android.app.NotificationManager; 0012 import android.app.PendingIntent; 0013 import android.app.Service; 0014 import android.content.Context; 0015 import android.content.Intent; 0016 import android.content.IntentFilter; 0017 import android.content.pm.PackageManager; 0018 import android.content.pm.ServiceInfo; 0019 import android.net.ConnectivityManager; 0020 import android.net.Network; 0021 import android.net.NetworkCapabilities; 0022 import android.net.NetworkRequest; 0023 import android.os.Build; 0024 import android.os.IBinder; 0025 import android.text.TextUtils; 0026 import android.util.Log; 0027 0028 import androidx.annotation.Nullable; 0029 import androidx.core.app.NotificationCompat; 0030 import androidx.core.content.ContextCompat; 0031 import androidx.lifecycle.LiveData; 0032 import androidx.lifecycle.MutableLiveData; 0033 0034 import org.kde.kdeconnect.Backends.BaseLinkProvider; 0035 import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider; 0036 import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; 0037 import org.kde.kdeconnect.Helpers.NotificationHelper; 0038 import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity; 0039 import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity; 0040 import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin; 0041 import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity; 0042 import org.kde.kdeconnect.UserInterface.MainActivity; 0043 import org.kde.kdeconnect_tp.R; 0044 0045 import java.util.ArrayList; 0046 0047 /* 0048 * This class (still) does 3 things: 0049 * - Keeps the app running by creating a foreground notification. 0050 * - Holds references to the active LinkProviders, but doesn't handle the DeviceLink those create (the KdeConnect class does that). 0051 * - Listens for network connectivity changes and tells the LinkProviders to re-check for devices. 0052 * It can be started by the KdeConnectBroadcastReceiver on some events or when the MainActivity is launched. 0053 */ 0054 public class BackgroundService extends Service { 0055 private static final int FOREGROUND_NOTIFICATION_ID = 1; 0056 0057 private static BackgroundService instance; 0058 0059 private KdeConnect applicationInstance; 0060 0061 private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>(); 0062 0063 public static BackgroundService getInstance() { 0064 return instance; 0065 } 0066 0067 private static boolean initialized = false; 0068 0069 // This indicates when connected over wifi/usb/bluetooth/(anything other than cellular) 0070 private final MutableLiveData<Boolean> connectedToNonCellularNetwork = new MutableLiveData<>(); 0071 public LiveData<Boolean> isConnectedToNonCellularNetwork() { 0072 return connectedToNonCellularNetwork; 0073 } 0074 0075 public void updateForegroundNotification() { 0076 if (NotificationHelper.isPersistentNotificationEnabled(this)) { 0077 //Update the foreground notification with the currently connected device list 0078 NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class); 0079 nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); 0080 } 0081 } 0082 0083 private void registerLinkProviders() { 0084 linkProviders.add(new LanLinkProvider(this)); 0085 // linkProviders.add(new LoopbackLinkProvider(this)); 0086 linkProviders.add(new BluetoothLinkProvider(this)); 0087 } 0088 0089 public void onNetworkChange(@Nullable Network network) { 0090 if (!initialized) { 0091 Log.d("KDE/BackgroundService", "ignoring onNetworkChange called before the service is initialized"); 0092 return; 0093 } 0094 Log.d("KDE/BackgroundService", "onNetworkChange"); 0095 for (BaseLinkProvider a : linkProviders) { 0096 a.onNetworkChange(network); 0097 } 0098 } 0099 0100 public void addConnectionListener(BaseLinkProvider.ConnectionReceiver cr) { 0101 for (BaseLinkProvider a : linkProviders) { 0102 a.addConnectionReceiver(cr); 0103 } 0104 } 0105 0106 public void removeConnectionListener(BaseLinkProvider.ConnectionReceiver cr) { 0107 for (BaseLinkProvider a : linkProviders) { 0108 a.removeConnectionReceiver(cr); 0109 } 0110 } 0111 0112 //This will called only once, even if we launch the service intent several times 0113 @Override 0114 public void onCreate() { 0115 super.onCreate(); 0116 Log.d("KdeConnect/BgService", "onCreate"); 0117 instance = this; 0118 0119 KdeConnect.getInstance().addDeviceListChangedCallback("BackgroundService", this::updateForegroundNotification); 0120 0121 // Register screen on listener 0122 IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); 0123 // See: https://developer.android.com/reference/android/net/ConnectivityManager.html#CONNECTIVITY_ACTION 0124 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 0125 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 0126 } 0127 registerReceiver(new KdeConnectBroadcastReceiver(), filter); 0128 0129 // Watch for changes on all network connections except cellular networks 0130 NetworkRequest.Builder networkRequestBuilder = getNonCellularNetworkRequestBuilder(); 0131 ConnectivityManager cm = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); 0132 cm.registerNetworkCallback(networkRequestBuilder.build(), new ConnectivityManager.NetworkCallback() { 0133 @Override 0134 public void onAvailable(Network network) { 0135 Log.i("BackgroundService", "Valid network available"); 0136 connectedToNonCellularNetwork.postValue(true); 0137 onNetworkChange(network); 0138 } 0139 @Override 0140 public void onLost(Network network) { 0141 Log.i("BackgroundService", "Valid network lost"); 0142 connectedToNonCellularNetwork.postValue(false); 0143 } 0144 }); 0145 0146 applicationInstance = KdeConnect.getInstance(); 0147 0148 registerLinkProviders(); 0149 addConnectionListener(applicationInstance.getConnectionListener()); // Link Providers need to be already registered 0150 for (BaseLinkProvider a : linkProviders) { 0151 a.onStart(); 0152 } 0153 initialized = true; 0154 } 0155 0156 private static NetworkRequest.Builder getNonCellularNetworkRequestBuilder() { 0157 NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder() 0158 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 0159 .addTransportType(NetworkCapabilities.TRANSPORT_VPN) 0160 .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) 0161 .addTransportType(NetworkCapabilities.TRANSPORT_BLUETOOTH); 0162 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { 0163 networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE); 0164 } 0165 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) { 0166 networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_USB) 0167 .addTransportType(NetworkCapabilities.TRANSPORT_LOWPAN); 0168 } 0169 return networkRequestBuilder; 0170 } 0171 0172 public void changePersistentNotificationVisibility(boolean visible) { 0173 if (visible) { 0174 updateForegroundNotification(); 0175 } else { 0176 Stop(); 0177 Start(this); 0178 } 0179 } 0180 0181 private Notification createForegroundNotification() { 0182 0183 //Why is this needed: https://developer.android.com/guide/components/services#Foreground 0184 0185 ArrayList<String> connectedDevices = new ArrayList<>(); 0186 ArrayList<String> connectedDeviceIds = new ArrayList<>(); 0187 for (Device device : applicationInstance.getDevices().values()) { 0188 if (device.isReachable() && device.isPaired()) { 0189 connectedDeviceIds.add(device.getDeviceId()); 0190 connectedDevices.add(device.getName()); 0191 } 0192 } 0193 0194 Intent intent = new Intent(this, MainActivity.class); 0195 if (connectedDeviceIds.size() == 1) { 0196 // Force open screen of the only connected device 0197 intent.putExtra(MainActivity.EXTRA_DEVICE_ID, connectedDeviceIds.get(0)); 0198 } 0199 0200 PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); 0201 NotificationCompat.Builder notification = new NotificationCompat.Builder(this, NotificationHelper.Channels.PERSISTENT); 0202 notification 0203 .setSmallIcon(R.drawable.ic_notification) 0204 .setOngoing(true) 0205 .setContentIntent(pi) 0206 .setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW 0207 .setShowWhen(false) 0208 .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) 0209 .setAutoCancel(false); 0210 notification.setGroup("BackgroundService"); 0211 0212 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 0213 //Pre-oreo, the notification will have an empty title line without this 0214 notification.setContentTitle(getString(R.string.kde_connect)); 0215 } 0216 0217 if (connectedDevices.isEmpty()) { 0218 notification.setContentText(getString(R.string.foreground_notification_no_devices)); 0219 } else { 0220 notification.setContentText(getString(R.string.foreground_notification_devices, TextUtils.join(", ", connectedDevices))); 0221 0222 // Adding an action button to send clipboard manually in Android 10 and later. 0223 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && 0224 ContextCompat.checkSelfPermission(this, Manifest.permission.READ_LOGS) == PackageManager.PERMISSION_DENIED) { 0225 Intent sendClipboard = ClipboardFloatingActivity.getIntent(this, true); 0226 PendingIntent sendPendingClipboard = PendingIntent.getActivity(this, 3, sendClipboard, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); 0227 notification.addAction(0, getString(R.string.foreground_notification_send_clipboard), sendPendingClipboard); 0228 } 0229 0230 if (connectedDeviceIds.size() == 1) { 0231 String deviceId = connectedDeviceIds.get(0); 0232 Device device = KdeConnect.getInstance().getDevice(deviceId); 0233 if (device != null) { 0234 // Adding two action buttons only when there is a single device connected. 0235 // Setting up Send File Intent. 0236 Intent sendFile = new Intent(this, SendFileActivity.class); 0237 sendFile.putExtra("deviceId", deviceId); 0238 PendingIntent sendPendingFile = PendingIntent.getActivity(this, 1, sendFile, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); 0239 notification.addAction(0, getString(R.string.send_files), sendPendingFile); 0240 0241 // Checking if there are registered commands and adding the button. 0242 RunCommandPlugin plugin = (RunCommandPlugin) device.getPlugin("RunCommandPlugin"); 0243 if (plugin != null && !plugin.getCommandList().isEmpty()) { 0244 Intent runCommand = new Intent(this, RunCommandActivity.class); 0245 runCommand.putExtra("deviceId", connectedDeviceIds.get(0)); 0246 PendingIntent runPendingCommand = PendingIntent.getActivity(this, 2, runCommand, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); 0247 notification.addAction(0, getString(R.string.pref_plugin_runcommand), runPendingCommand); 0248 } 0249 } 0250 } 0251 } 0252 return notification.build(); 0253 } 0254 0255 @Override 0256 public void onDestroy() { 0257 Log.d("KdeConnect/BgService", "onDestroy"); 0258 initialized = false; 0259 for (BaseLinkProvider a : linkProviders) { 0260 a.onStop(); 0261 } 0262 KdeConnect.getInstance().removeDeviceListChangedCallback("BackgroundService"); 0263 super.onDestroy(); 0264 } 0265 0266 @Override 0267 public IBinder onBind(Intent intent) { 0268 return null; 0269 } 0270 0271 @Override 0272 public int onStartCommand(Intent intent, int flags, int startId) { 0273 Log.d("KDE/BackgroundService", "onStartCommand"); 0274 if (NotificationHelper.isPersistentNotificationEnabled(this)) { 0275 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 0276 startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE); 0277 } else { 0278 startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); 0279 } 0280 } 0281 if (intent != null && intent.getBooleanExtra("refresh", false)) { 0282 onNetworkChange(null); 0283 } 0284 return Service.START_STICKY; 0285 } 0286 0287 public static void Start(Context context) { 0288 Log.d("KDE/BackgroundService", "Start"); 0289 Intent intent = new Intent(context, BackgroundService.class); 0290 ContextCompat.startForegroundService(context, intent); 0291 } 0292 0293 public static void ForceRefreshConnections(Context context) { 0294 Log.d("KDE/BackgroundService", "ForceRefreshConnections"); 0295 Intent intent = new Intent(context, BackgroundService.class); 0296 intent.putExtra("refresh", true); 0297 ContextCompat.startForegroundService(context, intent); 0298 } 0299 0300 public void Stop() { 0301 stopForeground(true); 0302 } 0303 0304 }