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 }