/* * Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kde.kdeconnect; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Binder; import android.os.IBinder; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BackgroundService extends Service { public interface DeviceListChangedCallback { void onDeviceListChanged(); } private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>(); private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>(); private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>(); private final HashSet<Object> discoveryModeAcquisitions = new HashSet<>(); public boolean acquireDiscoveryMode(Object key) { boolean wasEmpty = discoveryModeAcquisitions.isEmpty(); discoveryModeAcquisitions.add(key); if (wasEmpty) { onNetworkChange(); } //Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); return wasEmpty; } public void releaseDiscoveryMode(Object key) { boolean removed = discoveryModeAcquisitions.remove(key); //Log.e("releaseDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); if (removed && discoveryModeAcquisitions.isEmpty()) { cleanDevices(); } } public static void addGuiInUseCounter(Context activity) { addGuiInUseCounter(activity, false); } public static void addGuiInUseCounter(final Context activity, final boolean forceNetworkRefresh) { BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { boolean refreshed = service.acquireDiscoveryMode(activity); if (!refreshed && forceNetworkRefresh) { service.onNetworkChange(); } } }); } public static void removeGuiInUseCounter(final Context activity) { BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { //If no user interface is open, close the connections open to other devices service.releaseDiscoveryMode(activity); } }); } private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() { @Override public void incomingRequest() { onDeviceListChanged(); } @Override public void pairingSuccessful() { onDeviceListChanged(); } @Override public void pairingFailed(String error) { onDeviceListChanged(); } @Override public void unpaired() { onDeviceListChanged(); } }; private void onDeviceListChanged() { for(DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) { callback.onDeviceListChanged(); } } private void loadRememberedDevicesFromSettings() { //Log.e("BackgroundService", "Loading remembered trusted devices"); SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); Set<String> trustedDevices = preferences.getAll().keySet(); for(String deviceId : trustedDevices) { //Log.e("BackgroundService", "Loading device "+deviceId); if (preferences.getBoolean(deviceId, false)) { Device device = new Device(this, deviceId); devices.put(deviceId,device); device.addPairingCallback(devicePairingCallback); } } } private void registerLinkProviders() { //linkProviders.add(new LoopbackLinkProvider(this)); linkProviders.add(new LanLinkProvider(this)); } public ArrayList<BaseLinkProvider> getLinkProviders() { return linkProviders; } public Device getDevice(String id) { return devices.get(id); } private void cleanDevices() { new Thread(new Runnable() { @Override public void run() { for(Device d : devices.values()) { if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByPeer() && !d.deviceShouldBeKeptAlive()) { d.disconnect(); } } } }).start(); } private final BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() { @Override public void onConnectionReceived(final NetworkPackage identityPackage, final BaseLink link) { String deviceId = identityPackage.getString("deviceId"); Device device = devices.get(deviceId); if (device != null) { Log.i("KDE/BackgroundService", "addLink, known device: " + deviceId); device.addLink(identityPackage, link); } else { Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId); device = new Device(BackgroundService.this, identityPackage, link); if (device.isPaired() || device.isPairRequested() || device.isPairRequestedByPeer() || link.linkShouldBeKeptAlive() ||!discoveryModeAcquisitions.isEmpty() ) { devices.put(deviceId, device); device.addPairingCallback(devicePairingCallback); } else { device.disconnect(); } } onDeviceListChanged(); } @Override public void onConnectionLost(BaseLink link) { Device d = devices.get(link.getDeviceId()); Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId()); if (d != null) { d.removeLink(link); if (!d.isReachable() && !d.isPaired()) { //Log.e("onConnectionLost","Removing connection device because it was not paired"); devices.remove(link.getDeviceId()); d.removePairingCallback(devicePairingCallback); } } else { //Log.d("KDE/onConnectionLost","Removing connection to unknown device"); } onDeviceListChanged(); } }; public ConcurrentHashMap<String, Device> getDevices() { return devices; } public void onNetworkChange() { for (BaseLinkProvider a : linkProviders) { a.onNetworkChange(); } } 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); } } public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) { deviceListChangedCallbacks.put(key, callback); } public void removeDeviceListChangedCallback(String key) { deviceListChangedCallbacks.remove(key); } //This will called only once, even if we launch the service intent several times @Override public void onCreate() { super.onCreate(); // Register screen on listener IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); registerReceiver(new KdeConnectBroadcastReceiver(), filter); Log.i("KDE/BackgroundService", "Service not started yet, initializing..."); initializeSecurityParameters(); loadRememberedDevicesFromSettings(); registerLinkProviders(); //Link Providers need to be already registered addConnectionListener(deviceListener); for (BaseLinkProvider a : linkProviders) { a.onStart(); } } void initializeSecurityParameters() { RsaHelper.initialiseRsaKeys(this); SslHelper.initialiseCertificate(this); } @Override public void onDestroy() { for (BaseLinkProvider a : linkProviders) { a.onStop(); } super.onDestroy(); } @Override public IBinder onBind (Intent intent) { return new Binder(); } //To use the service from the gui public interface InstanceCallback { void onServiceStart(BackgroundService service); } private final static ArrayList<InstanceCallback> callbacks = new ArrayList<>(); private final static Lock mutex = new ReentrantLock(true); @Override public int onStartCommand(Intent intent, int flags, int startId) { //This will be called for each intent launch, even if the service is already started and it is reused mutex.lock(); try { for (InstanceCallback c : callbacks) { c.onServiceStart(this); } callbacks.clear(); } finally { mutex.unlock(); } return Service.START_STICKY; } public static void Start(Context c) { RunCommand(c, null); } public static void RunCommand(final Context c, final InstanceCallback callback) { new Thread(new Runnable() { @Override public void run() { if (callback != null) { mutex.lock(); try { callbacks.add(callback); } finally { mutex.unlock(); } } Intent serviceIntent = new Intent(c, BackgroundService.class); c.startService(serviceIntent); } }).start(); } }