diff --git a/AndroidManifest.xml b/AndroidManifest.xml
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,6 +22,7 @@
+
@@ -225,6 +226,16 @@
android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" />
+
+
+
+
+
+
diff --git a/res/drawable/ic_fingerprint_black_24dp.xml b/res/drawable/ic_fingerprint_black_24dp.xml
new file mode 100644
--- /dev/null
+++ b/res/drawable/ic_fingerprint_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -242,4 +242,17 @@
Dark theme
+ Tile service for unlocking from your phone or tablet
+ Unlock Computer
+ Unlock using android fingerprint sensor
+ Touch Sensor
+ Touch the fingerprint sensor to unlock your computer. KDEConnect will unlock the paired computer after successful identification
+ Authentication successful
+ Authentication failed
+ KDEconnect is disconnected
+ Security Exception. Try Again
+ UnLock Plugin
+ Unlock remote device from android
+ Lock
+
diff --git a/src/org/kde/kdeconnect/Plugins/LockPlugin/LockHelper.java b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockHelper.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockHelper.java
@@ -0,0 +1,228 @@
+package org.kde.kdeconnect.Plugins.LockPlugin;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+
+import org.kde.kdeconnect_tp.R;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.UUID;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+@RequiresApi(api = Build.VERSION_CODES.N)
+public class LockHelper extends FingerprintManager.AuthenticationCallback {
+
+ public interface LockCallback {
+ void onSuccess();
+ void onFailure();
+ void onError(int errCode, CharSequence errString);
+ }
+
+ private static final String KEY_NAME = UUID.randomUUID().toString();
+ private static final String TAG = LockHelper.class.getSimpleName();
+
+ private static volatile LockHelper mLockHelper;
+ private ArrayList callbacks = new ArrayList<>();
+
+ public void addLockCallback(LockCallback newCallback) {
+ callbacks.add(newCallback);
+ }
+
+ public void removeLockCallback(LockCallback oldCallback) {
+ callbacks.remove(oldCallback);
+ }
+
+
+ private Dialog mDialog;
+
+ // fp stuff
+ private KeyStore mKeyStore;
+ private Cipher mCipher;
+ private CancellationSignal mCancellationSignal;
+
+
+ private LockHelper(Context context) {
+ if (mLockHelper != null) {
+ throw new RuntimeException("Use getInstance()");
+ }
+
+ this.mDialog = new AlertDialog.Builder(context)
+ .setTitle(R.string.title_fp_unlock_diag)
+ .setMessage(R.string.mesg_fp_unlock_diag)
+ .setIcon(R.drawable.ic_fingerprint_black_24dp)
+ .setOnDismissListener(dialog -> onAuthenticationFailed())
+ .setNeutralButton(R.string.cancel, (dialog, which) -> {
+ dialog.dismiss();
+ onAuthenticationFailed();
+ })
+ .create();
+ }
+
+ public synchronized static LockHelper getInstance(Context context) {
+ if(mLockHelper == null) {
+ mLockHelper = new LockHelper(context);
+ }
+ return mLockHelper;
+ }
+
+ boolean shouldAuth() {
+ return !mDialog.isShowing();
+ }
+
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+ private boolean generateKey() {
+ mKeyStore = null;
+ KeyGenerator keyGenerator;
+
+ try {
+ mKeyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
+ } catch (NoSuchAlgorithmException |
+ NoSuchProviderException e) {
+ return false;
+ } catch (KeyStoreException e) {
+ return false;
+ }
+
+ try {
+ mKeyStore.load(null);
+ keyGenerator.init(new
+ KeyGenParameterSpec.Builder(KEY_NAME,
+ KeyProperties.PURPOSE_ENCRYPT |
+ KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ .setUserAuthenticationRequired(true)
+ .setEncryptionPaddings(
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ .build());
+ keyGenerator.generateKey();
+
+ return true;
+ } catch (NoSuchAlgorithmException
+ | InvalidAlgorithmParameterException
+ | CertificateException
+ | IOException e) {
+ return false;
+ }
+ }
+
+ private boolean cipherInit() {
+
+ if (!generateKey()) {
+ onAuthenticationFailed();
+ return false;
+ }
+
+ try {
+ mCipher = Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_CBC + "/"
+ + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+ } catch (NoSuchAlgorithmException |
+ NoSuchPaddingException e) {
+ onAuthenticationFailed();
+ return false;
+ }
+
+ try {
+ mKeyStore.load(null);
+ SecretKey key = (SecretKey) mKeyStore.getKey(KEY_NAME, null);
+ mCipher.init(Cipher.ENCRYPT_MODE, key);
+ return true;
+ } catch (KeyPermanentlyInvalidatedException e) {
+ onAuthenticationFailed();
+ return false;
+ } catch (KeyStoreException | CertificateException
+ | UnrecoverableKeyException | IOException
+ | NoSuchAlgorithmException | InvalidKeyException e) {
+ onAuthenticationFailed();
+ return false;
+ }
+ }
+
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ super.onAuthenticationError(errorCode, errString);
+ mDialog.dismiss();
+ Log.e(TAG, "onAuthenticationError: " + errString);
+ for (LockCallback c :
+ callbacks) {
+ c.onError(errorCode, errString);
+ }
+ onAuthenticationFailed();
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+ super.onAuthenticationHelp(helpCode, helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+ super.onAuthenticationSucceeded(result);
+ mDialog.setOnDismissListener(null);
+ mDialog.dismiss();
+ for (LockCallback c :
+ callbacks) {
+ c.onSuccess();
+ }
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ super.onAuthenticationFailed();
+ mDialog.dismiss();
+ for (LockCallback c :
+ callbacks) {
+ c.onFailure();
+ }
+ if (mCancellationSignal != null) {
+ mCancellationSignal.cancel();
+ mCancellationSignal = null;
+ }
+ }
+
+ public void pleaseAuthenticate(FingerprintManager manager) {
+ if(!cipherInit()) {
+ onAuthenticationFailed();
+ return;
+ }
+ mCancellationSignal = new CancellationSignal();
+ try {
+ manager.authenticate(new FingerprintManager.CryptoObject(mCipher),
+ mCancellationSignal, 0, this, null);
+ } catch (SecurityException e) {
+ mDialog.dismiss();
+ for (LockCallback c :
+ callbacks) {
+ c.onError(-32, e.getMessage());
+ }
+ Log.e(TAG, "pleaseAuthenticate: ", e);
+ }
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/LockPlugin/LockPlugin.java b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockPlugin.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockPlugin.java
@@ -0,0 +1,65 @@
+package org.kde.kdeconnect.Plugins.LockPlugin;
+
+import org.kde.kdeconnect.NetworkPacket;
+import org.kde.kdeconnect.Plugins.Plugin;
+import org.kde.kdeconnect_tp.R;
+
+
+public class LockPlugin extends Plugin {
+
+ public final static String PACKET_TYPE_LOCK = "kdeconnect.lock";
+ private final static String TAG = LockPlugin.class.getSimpleName();
+
+ @Override
+ public String getDisplayName() {
+ return context.getResources().getString(R.string.pref_plugin_lock);
+ }
+
+ @Override
+ public String getDescription() {
+ return context.getResources().getString(R.string.pref_plugin_lock_desc);
+ }
+
+ public void unlockDevice() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_LOCK);
+ device.sendPacket(np);
+ }
+
+ @Override
+ public boolean onPacketReceived(NetworkPacket np) {
+ return false;
+ }
+
+ @Override
+ public String getActionName() {
+ return context.getString(R.string.lock);
+ }
+
+ /* Should it be available via UI?
+ @Override
+ public void startMainActivity(Activity activity) {
+ unlockDevice();
+ }
+
+ @Override
+ public boolean hasMainActivity() {
+ return true;
+ }
+
+ @Override
+ public boolean displayInContextMenu() {
+ return false;
+ }
+ */
+
+ @Override
+ public String[] getSupportedPacketTypes() {
+ return new String[]{PACKET_TYPE_LOCK};
+ }
+
+ @Override
+ public String[] getOutgoingPacketTypes() {
+ return new String[]{PACKET_TYPE_LOCK};
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/LockPlugin/LockTileService.java b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockTileService.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockTileService.java
@@ -0,0 +1,126 @@
+package org.kde.kdeconnect.Plugins.LockPlugin;
+
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+import android.support.annotation.RequiresApi;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.kde.kdeconnect.BackgroundService;
+import org.kde.kdeconnect.Device;
+import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin;
+import org.kde.kdeconnect_tp.R;
+
+import java.util.Locale;
+
+@RequiresApi(api = Build.VERSION_CODES.N)
+public class LockTileService extends TileService implements LockHelper.LockCallback {
+
+ private LockHelper mLockHelper;
+ private static final String STATE_SELECTED_DEVICE = "selected_device";
+ private final static String TAG = LockTileService.class.getSimpleName();
+
+ @Override
+ public void onClick() {
+ super.onClick();
+ if(isLocked()) {
+ unlockAndRun(this::unlockComputer);
+ } else {
+ if (mLockHelper.shouldAuth()) {
+ showDialog(mLockHelper.getDialog());
+ mLockHelper.pleaseAuthenticate((FingerprintManager)
+ getSystemService(FINGERPRINT_SERVICE));
+ }
+ }
+ }
+
+ @Override
+ public void onStartListening() {
+ Tile tile = getQsTile();
+ tile.setIcon(Icon.createWithResource(this, R.drawable.ic_fingerprint_black_24dp));
+ tile.setLabel(getString(R.string.label_qs_unlock));
+ tile.setContentDescription(getString(R.string.desc_qs_unlock));
+ tile.setState(Tile.STATE_ACTIVE);
+ tile.updateTile();
+ mLockHelper = LockHelper.getInstance(this);
+ mLockHelper.addLockCallback(this);
+ }
+
+ @Override
+ public void onSuccess() {
+ //TODO: are toasts really necessary? they ugly
+ if(unlockComputer(1)) {
+ Toast.makeText(this, "Authenticated Successfully", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(LockTileService.this, R.string.mesg_app_discon, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure() {
+ Toast.makeText(this, "Authentication Failed", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(int errCode, CharSequence errString) {
+ Toast.makeText(this, String.format(Locale.getDefault(), "%d : %s", errCode, errString),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ boolean bool = false;
+
+ private boolean unlockComputer() {
+ BackgroundService.RunCommand(this, service -> {
+ Device device = service.getDevice(getSharedPreferences(STATE_SELECTED_DEVICE, Context.MODE_PRIVATE)
+ .getString(STATE_SELECTED_DEVICE, null));
+ LockPlugin p = device.getPlugin(LockPlugin.class);
+ if (p == null) {
+ bool = false;
+ return;
+ }
+ p.unlockDevice();
+ bool = true;
+ });
+ return bool;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mLockHelper.removeLockCallback(this);
+ }
+
+ private boolean unlockComputer(int a) {
+ BackgroundService.RunCommand(this, service -> {
+ //TODO: don't piggyback on runcommand
+ String deviceId = getSharedPreferences(STATE_SELECTED_DEVICE,
+ Context.MODE_PRIVATE)
+ .getString(STATE_SELECTED_DEVICE, null);
+ Device device = service.getDevice(deviceId);
+ RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
+ if (plugin == null) {
+ bool = false;
+ return;
+ }
+ for (JSONObject obj : plugin.getCommandList()) {
+ try {
+ // ideally there should only be one command named "unlock"
+ // all of them are executed though
+ // command: loginctl unlock-session
+ if(obj.getString("name").equals("unlock")) {
+ plugin.runCommand(obj.getString("key"));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ bool = true;
+ });
+ return bool;
+ }
+}