diff --git a/src/org/kde/kdeconnect/BackgroundService.java b/src/org/kde/kdeconnect/BackgroundService.java index 1aff1e7c..4511a1fa 100644 --- a/src/org/kde/kdeconnect/BackgroundService.java +++ b/src/org/kde/kdeconnect/BackgroundService.java @@ -1,440 +1,440 @@ /* * Copyright 2014 Albert Vaca Cintora * * 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 . */ package org.kde.kdeconnect; 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.SharedPreferences; import android.net.ConnectivityManager; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.text.TextUtils; 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.NotificationHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect_tp.R; 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; import androidx.core.app.NotificationCompat; //import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider; public class BackgroundService extends Service { private static final int FOREGROUND_NOTIFICATION_ID = 1; private static BackgroundService instance; public interface DeviceListChangedCallback { void onDeviceListChanged(); } public interface PluginCallback { void run(T plugin); } private final ConcurrentHashMap deviceListChangedCallbacks = new ConcurrentHashMap<>(); private final ArrayList linkProviders = new ArrayList<>(); private final ConcurrentHashMap devices = new ConcurrentHashMap<>(); private final HashSet discoveryModeAcquisitions = new HashSet<>(); public static BackgroundService getInstance() { return instance; } private boolean acquireDiscoveryMode(Object key) { boolean wasEmpty = discoveryModeAcquisitions.isEmpty(); discoveryModeAcquisitions.add(key); if (wasEmpty) { onNetworkChange(); } //Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]"); return wasEmpty; } private 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, service -> { boolean refreshed = service.acquireDiscoveryMode(activity); if (!refreshed && forceNetworkRefresh) { service.onNetworkChange(); } }); } public static void removeGuiInUseCounter(final Context activity) { BackgroundService.RunCommand(activity, 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(); } }; public void onDeviceListChanged() { for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) { callback.onDeviceListChanged(); } if (NotificationHelper.isPersistentNotificationEnabled(this)) { //Update the foreground notification with the currently connected device list NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } } private void loadRememberedDevicesFromSettings() { //Log.e("BackgroundService", "Loading remembered trusted devices"); SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); Set 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 LanLinkProvider(this)); // linkProviders.add(new LoopbackLinkProvider(this)); // linkProviders.add(new BluetoothLinkProvider(this)); } public ArrayList getLinkProviders() { return linkProviders; } public Device getDevice(String id) { return devices.get(id); } private void cleanDevices() { new Thread(() -> { 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 NetworkPacket identityPacket, final BaseLink link) { String deviceId = identityPacket.getString("deviceId"); Device device = devices.get(deviceId); if (device != null) { Log.i("KDE/BackgroundService", "addLink, known device: " + deviceId); device.addLink(identityPacket, link); } else { Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId); device = new Device(BackgroundService.this, identityPacket, 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 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(); instance = this; // 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) { filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } registerReceiver(new KdeConnectBroadcastReceiver(), filter); Log.i("KDE/BackgroundService", "Service not started yet, initializing..."); PluginFactory.initPluginInfo(getBaseContext()); initializeSecurityParameters(); NotificationHelper.initializeChannels(this); loadRememberedDevicesFromSettings(); registerLinkProviders(); //Link Providers need to be already registered addConnectionListener(deviceListener); for (BaseLinkProvider a : linkProviders) { a.onStart(); } } public void changePersistentNotificationVisibility(boolean visible) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (visible) { nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } else { stopForeground(true); Start(this); } } private Notification createForegroundNotification() { //Why is this needed: https://developer.android.com/guide/components/services#Foreground Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 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) .setAutoCancel(false); notification.setGroup("BackgroundService"); ArrayList connectedDevices = new ArrayList<>(); for (Device device : getDevices().values()) { if (device.isReachable() && device.isPaired()) { connectedDevices.add(device.getName()); } } 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))); } return notification.build(); } private void initializeSecurityParameters() { RsaHelper.initialiseRsaKeys(this); SslHelper.initialiseCertificate(this); } @Override public void onDestroy() { stopForeground(true); 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 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(); } if (NotificationHelper.isPersistentNotificationEnabled(this)) { startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } return Service.START_STICKY; } private static void Start(Context c) { RunCommand(c, null); } public static void RunCommand(final Context c, final InstanceCallback callback) { new Thread(() -> { if (callback != null) { mutex.lock(); try { callbacks.add(callback); } finally { mutex.unlock(); } } Intent serviceIntent = new Intent(c, BackgroundService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { c.startForegroundService(serviceIntent); } else { c.startService(serviceIntent); } }).start(); } - public static void runWithPlugin(final Context c, final String deviceId, final Class pluginClass, final PluginCallback cb) { + public static void RunWithPlugin(final Context c, final String deviceId, final Class pluginClass, final PluginCallback cb) { RunCommand(c, service -> { Device device = service.getDevice(deviceId); if (device == null) { Log.e("BackgroundService", "Device " + deviceId + " not found"); return; } final T plugin = device.getPlugin(pluginClass); if (plugin == null) { Log.e("BackgroundService", "Device " + device.getName() + " does not have plugin " + pluginClass.getName()); return; } cb.run(plugin); }); } } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java index a48e834e..866a97c6 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -1,382 +1,382 @@ /* * Copyright 2014 Ahmed I. Khalil * * 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 . */ package org.kde.kdeconnect.Plugins.MousePadPlugin; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import androidx.appcompat.app.AppCompatActivity; public class MousePadActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, MousePadGestureDetector.OnGestureListener { private String deviceId; private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll private final static float MinDistanceToSendGenericScroll = 0.1f; // real mouse scroll wheel event private final static float StandardDpi = 240.0f; // = hdpi private float mPrevX; private float mPrevY; private float mCurrentX; private float mCurrentY; private float mCurrentSensitivity; private float displayDpiMultiplier; private int scrollDirection = 1; private boolean isScrolling = false; private float accumulatedDistanceY = 0; private GestureDetector mDetector; private MousePadGestureDetector mMousePadGestureDetector; private PointerAccelerationProfile mPointerAccelerationProfile; private PointerAccelerationProfile.MouseDelta mouseDelta; // to be reused on every touch move event private KeyListenerView keyListenerView; enum ClickType { RIGHT, MIDDLE, NONE; static ClickType fromString(String s) { switch (s) { case "right": return RIGHT; case "middle": return MIDDLE; default: return NONE; } } } private ClickType doubleTapAction, tripleTapAction; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_mousepad); deviceId = getIntent().getStringExtra("deviceId"); getWindow().getDecorView().setHapticFeedbackEnabled(true); mDetector = new GestureDetector(this, this); mMousePadGestureDetector = new MousePadGestureDetector(this); mDetector.setOnDoubleTapListener(this); keyListenerView = findViewById(R.id.keyListener); keyListenerView.setDeviceId(deviceId); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) { scrollDirection = -1; } else { scrollDirection = 1; } String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key), getString(R.string.mousepad_default_double)); String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key), getString(R.string.mousepad_default_triple)); String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key), getString(R.string.mousepad_default_sensitivity)); String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key), getString(R.string.mousepad_default_acceleration_profile)); mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName); doubleTapAction = ClickType.fromString(doubleTapSetting); tripleTapAction = ClickType.fromString(tripleTapSetting); //Technically xdpi and ydpi should be handled separately, //but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier. displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi; switch (sensitivitySetting) { case "slowest": mCurrentSensitivity = 0.2f; break; case "aboveSlowest": mCurrentSensitivity = 0.5f; break; case "default": mCurrentSensitivity = 1.0f; break; case "aboveDefault": mCurrentSensitivity = 1.5f; break; case "fastest": mCurrentSensitivity = 2.0f; break; default: mCurrentSensitivity = 1.0f; return; } final View decorView = getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { int fullscreenType = 0; fullscreenType |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { fullscreenType |= View.SYSTEM_UI_FLAG_FULLSCREEN; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { fullscreenType |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } getWindow().getDecorView().setSystemUiVisibility(fullscreenType); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_mousepad, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_right_click: sendRightClick(); return true; case R.id.menu_middle_click: sendMiddleClick(); return true; case R.id.menu_show_keyboard: showKeyboard(); return true; default: return super.onOptionsItemSelected(item); } } @Override public boolean onTouchEvent(MotionEvent event) { if (mMousePadGestureDetector.onTouchEvent(event)) { return true; } if (mDetector.onTouchEvent(event)) { return true; } int actionType = event.getAction(); if (isScrolling) { if (actionType == MotionEvent.ACTION_UP) { isScrolling = false; } else { return false; } } switch (actionType) { case MotionEvent.ACTION_DOWN: mPrevX = event.getX(); mPrevY = event.getY(); break; case MotionEvent.ACTION_MOVE: mCurrentX = event.getX(); mCurrentY = event.getY(); - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity; float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity; // Run the mouse delta through the pointer acceleration profile mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime()); mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta); plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y); mPrevX = mCurrentX; mPrevY = mCurrentY; }); break; } return true; } @Override public boolean onDown(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { //From GestureDetector, left empty } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onGenericMotionEvent(MotionEvent e) { if (e.getAction() == MotionEvent.ACTION_SCROLL) { final float distanceY = e.getAxisValue(MotionEvent.AXIS_VSCROLL); accumulatedDistanceY += distanceY; if (accumulatedDistanceY > MinDistanceToSendGenericScroll || accumulatedDistanceY < -MinDistanceToSendGenericScroll) { sendScroll(accumulatedDistanceY); accumulatedDistanceY = 0; } } return super.onGenericMotionEvent(e); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, final float distanceX, final float distanceY) { // If only one thumb is used then cancel the scroll gesture if (e2.getPointerCount() <= 1) { return false; } isScrolling = true; accumulatedDistanceY += distanceY; if (accumulatedDistanceY > MinDistanceToSendScroll || accumulatedDistanceY < -MinDistanceToSendScroll) { sendScroll(scrollDirection * accumulatedDistanceY); accumulatedDistanceY = 0; } return true; } @Override public void onLongPress(MotionEvent e) { getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleClick); return true; } @Override public boolean onDoubleTap(MotionEvent e) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick); return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } @Override public boolean onTripleFingerTap(MotionEvent ev) { switch (tripleTapAction) { case RIGHT: sendRightClick(); break; case MIDDLE: sendMiddleClick(); break; default: } return true; } @Override public boolean onDoubleFingerTap(MotionEvent ev) { switch (doubleTapAction) { case RIGHT: sendRightClick(); break; case MIDDLE: sendMiddleClick(); break; default: } return true; } private void sendMiddleClick() { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick); } private void sendRightClick() { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick); } private void sendScroll(final float y) { - BackgroundService.runWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y)); + BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y)); } //TODO: Does not work on KitKat with or without requestFocus() private void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); keyListenerView.requestFocus(); imm.toggleSoftInputFromWindow(keyListenerView.getWindowToken(), 0, 0); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } } diff --git a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java index 5cb252a1..b689b7ea 100644 --- a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java +++ b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java @@ -1,72 +1,72 @@ package org.kde.kdeconnect.Plugins.PhotoPlugin; import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import org.kde.kdeconnect.BackgroundService; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; public class PhotoActivity extends AppCompatActivity { private Uri photoURI; private PhotoPlugin plugin; @Override protected void onStart() { super.onStart(); - BackgroundService.runWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> { this.plugin = plugin; }); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { File photoFile = null; try { photoFile = createImageFile(); } catch (IOException ignored) { } // Continue only if the File was successfully created if (photoFile != null) { photoURI = FileProvider.getUriForFile(this, "org.kde.kdeconnect_tp.fileprovider", photoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); startActivityForResult(takePictureIntent, 1); } } } private File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); return File.createTempFile( imageFileName, /* prefix */ ".jpg", /* suffix */ storageDir /* directory */ ); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == -1) { plugin.sendPhoto(photoURI); } finish(); } } diff --git a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java index 26ca6352..fedea60c 100644 --- a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java @@ -1,141 +1,141 @@ /* * Copyright 2014 Ahmed I. Khalil * * 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 . */ package org.kde.kdeconnect.Plugins.PresenterPlugin; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import androidx.appcompat.app.AppCompatActivity; import androidx.media.VolumeProviderCompat; public class PresenterActivity extends AppCompatActivity { private MediaSessionCompat mMediaSession; private PresenterPlugin plugin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_presenter); final String deviceId = getIntent().getStringExtra("deviceId"); - BackgroundService.runWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> { + BackgroundService.RunWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> { this.plugin = plugin; findViewById(R.id.next_button).setOnClickListener(v -> plugin.sendNext()); findViewById(R.id.previous_button).setOnClickListener(v -> plugin.sendPrevious()); })); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_presenter, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.fullscreen: plugin.sendFullscreen(); return true; case R.id.exit_presentation: plugin.sendEsc(); return true; default: return super.onContextItemSelected(item); } } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); if (mMediaSession != null) { mMediaSession.setActive(true); return; } createMediaSession(); //Mediasession will keep } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); if (mMediaSession != null) { PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); boolean screenOn; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { screenOn = pm.isInteractive(); } else { screenOn = pm.isScreenOn(); } if (screenOn) { mMediaSession.release(); } // else we are in the lockscreen, keep the mediasession } } private void createMediaSession() { mMediaSession = new MediaSessionCompat(this, "kdeconnect"); mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_PLAYING, 0, 0) .build()); mMediaSession.setPlaybackToRemote(getVolumeProvider()); mMediaSession.setActive(true); } private VolumeProviderCompat getVolumeProvider() { final int VOLUME_UP = 1; final int VOLUME_DOWN = -1; return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 1, 0) { @Override public void onAdjustVolume(int direction) { if (direction == VOLUME_UP) { plugin.sendNext(); } else if (direction == VOLUME_DOWN) { plugin.sendPrevious(); } } }; } } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java index 5f62e488..d3e96f26 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java @@ -1,176 +1,176 @@ /* * Copyright 2015 Aleix Pol Gonzalez * Copyright 2015 Albert Vaca Cintora * * 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 . */ package org.kde.kdeconnect.Plugins.RunCommandPlugin; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collections; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; public class RunCommandActivity extends AppCompatActivity { private String deviceId; private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView; private ArrayList commandItems; private void updateView() { - BackgroundService.runWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { runOnUiThread(() -> { ListView view = findViewById(R.id.runcommandslist); registerForContextMenu(view); commandItems = new ArrayList<>(); for (JSONObject obj : plugin.getCommandList()) { try { commandItems.add(new CommandEntry(obj.getString("name"), obj.getString("command"), obj.getString("key"))); } catch (JSONException e) { e.printStackTrace(); } } Collections.sort(commandItems, (lhs, rhs) -> { String lName = ((CommandEntry) lhs).getName(); String rName = ((CommandEntry) rhs).getName(); return lName.compareTo(rName); }); ListAdapter adapter = new ListAdapter(RunCommandActivity.this, commandItems); view.setAdapter(adapter); view.setOnItemClickListener((adapterView, view1, i, l) -> { CommandEntry entry = (CommandEntry) commandItems.get(i); plugin.runCommand(entry.getKey()); }); TextView explanation = findViewById(R.id.addcomand_explanation); String text = getString(R.string.addcommand_explanation); if (!plugin.canAddCommand()) { text += "\n" + getString(R.string.addcommand_explanation2); } explanation.setText(text); explanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE); }); }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_runcommand); deviceId = getIntent().getStringExtra("deviceId"); boolean canAddCommands = BackgroundService.getInstance().getDevice(deviceId).getPlugin(RunCommandPlugin.class).canAddCommand(); FloatingActionButton addCommandButton = findViewById(R.id.add_command_button); if (canAddCommands) { addCommandButton.show(); } else { addCommandButton.hide(); } addCommandButton.setOnClickListener(v -> { - BackgroundService.runWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> { plugin.sendSetupPacket(); AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this) .setTitle(R.string.add_command) .setMessage(R.string.add_command_description) .setPositiveButton(R.string.ok, null) .create(); dialog.show(); }); }); updateView(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.runcommand_context, menu); } @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); if (item.getItemId() == R.id.copy_url_to_clipboard) { CommandEntry entry = (CommandEntry) commandItems.get(info.position); String url = "kdeconnect://runcommand/" + deviceId + "/" + entry.getKey(); ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(url); Toast toast = Toast.makeText(this, R.string.clipboard_toast, Toast.LENGTH_SHORT); toast.show(); } return false; } @Override protected void onResume() { super.onResume(); - BackgroundService.runWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { plugin.addCommandsUpdatedCallback(commandsChangedCallback); }); } @Override protected void onPause() { super.onPause(); - BackgroundService.runWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> { plugin.removeCommandsUpdatedCallback(commandsChangedCallback); }); } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java index 37847a5e..9dfbe308 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java @@ -1,102 +1,102 @@ /* * Copyright 2014 Albert Vaca Cintora * * 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 . */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.app.Activity; import android.content.ClipData; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import androidx.appcompat.app.AppCompatActivity; public class SendFileActivity extends AppCompatActivity { private String mDeviceId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); mDeviceId = getIntent().getStringExtra("deviceId"); Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } intent.addCategory(Intent.CATEGORY_OPENABLE); try { startActivityForResult( Intent.createChooser(intent, getString(R.string.send_files)), Activity.RESULT_FIRST_USER); } catch (android.content.ActivityNotFoundException ex) { Toast.makeText(this, R.string.no_file_browser, Toast.LENGTH_SHORT).show(); finish(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case Activity.RESULT_FIRST_USER: if (resultCode == RESULT_OK) { final ArrayList uris = new ArrayList<>(); Uri uri = data.getData(); if (uri != null) { uris.add(uri); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { ClipData clipdata = data.getClipData(); if (clipdata != null) { for (int i = 0; i < clipdata.getItemCount(); i++) { uris.add(clipdata.getItemAt(i).getUri()); } } } if (uris.isEmpty()) { Log.w("SendFileActivity", "No files to send?"); } else { - BackgroundService.runWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris)); + BackgroundService.RunWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris)); } } finish(); break; default: super.onActivityResult(requestCode, resultCode, data); } } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java index f93e6bfa..affa4e45 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -1,173 +1,173 @@ /* * Copyright 2014 Albert Vaca Cintora * * 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 . */ package org.kde.kdeconnect.Plugins.SharePlugin; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.UserInterface.List.EntryItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; public class ShareActivity extends AppCompatActivity { private SwipeRefreshLayout mSwipeRefreshLayout; @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.refresh, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: updateComputerListAction(); break; default: break; } return true; } private void updateComputerListAction() { updateComputerList(); BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange); mSwipeRefreshLayout.setRefreshing(true); new Thread(() -> { try { Thread.sleep(1500); } catch (InterruptedException ignored) { } runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false)); }).start(); } private void updateComputerList() { final Intent intent = getIntent(); String action = intent.getAction(); if (!Intent.ACTION_SEND.equals(action) && !Intent.ACTION_SEND_MULTIPLE.equals(action)) { finish(); return; } BackgroundService.RunCommand(this, service -> { Collection devices = service.getDevices().values(); final ArrayList devicesList = new ArrayList<>(); final ArrayList items = new ArrayList<>(); SectionItem section = new SectionItem(getString(R.string.share_to)); items.add(section); for (Device d : devices) { if (d.isReachable() && d.isPaired()) { devicesList.add(d); items.add(new EntryItem(d.getName())); section.isEmpty = false; } } runOnUiThread(() -> { ListView list = findViewById(R.id.devices_list); list.setAdapter(new ListAdapter(ShareActivity.this, items)); list.setOnItemClickListener((adapterView, view, i, l) -> { Device device = devicesList.get(i - 1); //NOTE: -1 because of the title! - BackgroundService.runWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent)); + BackgroundService.RunWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent)); finish(); }); }); }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.devices_list); ActionBar actionBar = getSupportActionBar(); mSwipeRefreshLayout = findViewById(R.id.refresh_list_layout); mSwipeRefreshLayout.setOnRefreshListener( this::updateComputerListAction ); if (actionBar != null) { actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); } } @Override protected void onStart() { super.onStart(); final Intent intent = getIntent(); final String deviceId = intent.getStringExtra("deviceId"); if (deviceId != null) { - BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> { + BackgroundService.RunWithPlugin(this, deviceId, SharePlugin.class, plugin -> { plugin.share(intent); finish(); }); } else { BackgroundService.addGuiInUseCounter(this); BackgroundService.RunCommand(this, service -> { service.onNetworkChange(); service.addDeviceListChangedCallback("ShareActivity", this::updateComputerList); }); updateComputerList(); } } @Override protected void onStop() { BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity")); BackgroundService.removeGuiInUseCounter(this); super.onStop(); } } diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java similarity index 98% rename from src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java rename to src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java index 0517dd73..3b5b306f 100644 --- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java @@ -1,151 +1,151 @@ /* * Copyright 2018 Nicolas Fella * * 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 . */ package org.kde.kdeconnect.Plugins.SystemvolumePlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; @PluginFactory.LoadablePlugin -public class SystemvolumePlugin extends Plugin { +public class SystemVolumePlugin extends Plugin { private final static String PACKET_TYPE_SYSTEMVOLUME = "kdeconnect.systemvolume"; private final static String PACKET_TYPE_SYSTEMVOLUME_REQUEST = "kdeconnect.systemvolume.request"; public interface SinkListener { void sinksChanged(); } private final ConcurrentHashMap sinks; private final ArrayList listeners; - public SystemvolumePlugin() { + public SystemVolumePlugin() { sinks = new ConcurrentHashMap<>(); listeners = new ArrayList<>(); } @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_systemvolume); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_systemvolume_desc); } @Override public boolean onPacketReceived(NetworkPacket np) { if (np.has("sinkList")) { sinks.clear(); try { JSONArray sinkArray = np.getJSONArray("sinkList"); for (int i = 0; i < sinkArray.length(); i++) { JSONObject sinkObj = sinkArray.getJSONObject(i); Sink sink = new Sink(sinkObj); sinks.put(sink.getName(), sink); } } catch (JSONException e) { e.printStackTrace(); } for (SinkListener l : listeners) { l.sinksChanged(); } } else { String name = np.getString("name"); if (sinks.containsKey(name)) { if (np.has("volume")) { sinks.get(name).setVolume(np.getInt("volume")); } if (np.has("muted")) { sinks.get(name).setMute(np.getBoolean("muted")); } } } return true; } void sendVolume(String name, int volume) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("volume", volume); np.set("name", name); device.sendPacket(np); } void sendMute(String name, boolean mute) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("muted", mute); np.set("name", name); device.sendPacket(np); } void requestSinkList() { NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST); np.set("requestSinks", true); device.sendPacket(np); } @Override public boolean hasMainActivity() { return false; } @Override public boolean displayInContextMenu() { return false; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_SYSTEMVOLUME}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_SYSTEMVOLUME_REQUEST}; } Collection getSinks() { return sinks.values(); } void addSinkListener(SinkListener listener) { listeners.add(listener); } void removeSinkListener(SinkListener listener) { listeners.remove(listener); } } diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java index aa09417e..c102528f 100644 --- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java +++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java @@ -1,153 +1,153 @@ /* * Copyright 2018 Nicolas Fella * * 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 . */ package org.kde.kdeconnect.Plugins.SystemvolumePlugin; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect_tp.R; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; -public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemvolumePlugin.SinkListener { +public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemVolumePlugin.SinkListener { - private SystemvolumePlugin plugin; + private SystemVolumePlugin plugin; private Activity activity; private SinkAdapter adapter; private Context context; private boolean tracking; @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setDivider(null); setListAdapter(new SinkAdapter(getContext(), new Sink[0])); } @Override public void updateSink(final Sink sink) { // Don't set progress while the slider is moved if (!tracking) { activity.runOnUiThread(() -> adapter.notifyDataSetChanged()); } } public void connectToPlugin(final String deviceId) { - BackgroundService.runWithPlugin(activity, deviceId, SystemvolumePlugin.class, plugin -> { + BackgroundService.RunWithPlugin(activity, deviceId, SystemVolumePlugin.class, plugin -> { this.plugin = plugin; plugin.addSinkListener(SystemvolumeFragment.this); plugin.requestSinkList(); }); } @Override public void onAttach(Context context) { super.onAttach(context); activity = getActivity(); this.context = context; } @Override public void sinksChanged() { for (Sink sink : plugin.getSinks()) { sink.addListener(SystemvolumeFragment.this); } activity.runOnUiThread(() -> { adapter = new SinkAdapter(context, plugin.getSinks().toArray(new Sink[0])); setListAdapter(adapter); }); } private class SinkAdapter extends ArrayAdapter { private SinkAdapter(@NonNull Context context, @NonNull Sink[] objects) { super(context, R.layout.list_item_systemvolume, objects); } @NonNull @Override public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) { View view = getLayoutInflater().inflate(R.layout.list_item_systemvolume, parent, false); UIListener listener = new UIListener(getItem(position)); ((TextView) view.findViewById(R.id.systemvolume_label)).setText(getItem(position).getDescription()); final SeekBar seekBar = view.findViewById(R.id.systemvolume_seek); seekBar.setMax(getItem(position).getMaxVolume()); seekBar.setProgress(getItem(position).getVolume()); seekBar.setOnSeekBarChangeListener(listener); ImageButton button = view.findViewById(R.id.systemvolume_mute); int iconRes = getItem(position).isMute() ? R.drawable.ic_volume_mute_black : R.drawable.ic_volume_black; button.setImageResource(iconRes); button.setOnClickListener(listener); return view; } } private class UIListener implements SeekBar.OnSeekBarChangeListener, ImageButton.OnClickListener { private final Sink sink; private UIListener(Sink sink) { this.sink = sink; } @Override public void onProgressChanged(final SeekBar seekBar, int i, boolean b) { BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress())); } @Override public void onStartTrackingTouch(SeekBar seekBar) { tracking = true; } @Override public void onStopTrackingTouch(final SeekBar seekBar) { tracking = false; BackgroundService.RunCommand(activity, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress())); } @Override public void onClick(View view) { plugin.sendMute(sink.getName(), !sink.isMute()); } } }