diff --git a/AndroidManifest.xml b/AndroidManifest.xml --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -227,7 +227,7 @@ diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -251,5 +251,8 @@ 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 --- a/src/org/kde/kdeconnect/Plugins/LockPlugin/LockHelper.java +++ b/src/org/kde/kdeconnect/Plugins/LockPlugin/LockHelper.java @@ -3,7 +3,6 @@ import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; import android.hardware.fingerprint.FingerprintManager; import android.os.Build; import android.os.CancellationSignal; @@ -12,13 +11,7 @@ import android.security.keystore.KeyProperties; import android.support.annotation.RequiresApi; import android.util.Log; -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.io.IOException; @@ -30,6 +23,7 @@ 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; @@ -40,42 +34,59 @@ @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 STATE_SELECTED_DEVICE = "selected_device"; 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; - private Context mContext; // fp stuff private KeyStore mKeyStore; private Cipher mCipher; private CancellationSignal mCancellationSignal; - private boolean unlocked = false; - LockHelper(Context context) { - this.mContext = context; - this.mDialog = new AlertDialog.Builder(context).setTitle(R.string.title_fp_unlock_diag) - .setMessage(mContext.getString(R.string.mesg_fp_unlock_diag)) + 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(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - onAuthenticationFailed(); - } - }) - .setNeutralButton(mContext.getString(R.string.cancel), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - onAuthenticationFailed(); - } + .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(); } @@ -159,6 +170,10 @@ super.onAuthenticationError(errorCode, errString); mDialog.dismiss(); Log.e(TAG, "onAuthenticationError: " + errString); + for (LockCallback c : + callbacks) { + c.onError(errorCode, errString); + } onAuthenticationFailed(); } @@ -172,12 +187,9 @@ super.onAuthenticationSucceeded(result); mDialog.setOnDismissListener(null); mDialog.dismiss(); - if(unlockComputer()) { - Toast.makeText(mContext, R.string.mesg_auth_success, Toast.LENGTH_SHORT).show(); - } - if (mCancellationSignal != null) { - mCancellationSignal.cancel(); - mCancellationSignal = null; + for (LockCallback c : + callbacks) { + c.onSuccess(); } } @@ -185,46 +197,16 @@ public void onAuthenticationFailed() { super.onAuthenticationFailed(); mDialog.dismiss(); - Toast.makeText(mContext, R.string.mesg_auth_fail, Toast.LENGTH_SHORT).show(); + for (LockCallback c : + callbacks) { + c.onFailure(); + } if (mCancellationSignal != null) { mCancellationSignal.cancel(); mCancellationSignal = null; } } - boolean unlockComputer() { - BackgroundService.RunCommand(mContext, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - //TODO: don't piggyback on runcommand - String deviceId = mContext.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) { - Toast.makeText(mContext, R.string.mesg_app_discon, Toast.LENGTH_SHORT).show(); - unlocked = 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(); - } - } - unlocked = true; - } - }); - return unlocked; - } - public void pleaseAuthenticate(FingerprintManager manager) { if(!cipherInit()) { onAuthenticationFailed(); @@ -236,7 +218,11 @@ mCancellationSignal, 0, this, null); } catch (SecurityException e) { mDialog.dismiss(); - Toast.makeText(mContext, R.string.mesg_sec_exc, Toast.LENGTH_SHORT).show(); + 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; + } +} diff --git a/src/org/kde/kdeconnect/Plugins/LockPlugin/TileService.java b/src/org/kde/kdeconnect/Plugins/LockPlugin/TileService.java deleted file mode 100644 --- a/src/org/kde/kdeconnect/Plugins/LockPlugin/TileService.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.kde.kdeconnect.Plugins.LockPlugin; - -import android.annotation.TargetApi; -import android.graphics.drawable.Icon; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; -import android.service.quicksettings.Tile; - -import org.kde.kdeconnect_tp.R; - -@TargetApi(Build.VERSION_CODES.N) -public class TileService extends android.service.quicksettings.TileService { - - private LockHelper mLockHelper = null; - - @Override - public void onClick() { - super.onClick(); - if (mLockHelper == null) - mLockHelper = new LockHelper(this); - if(isLocked()) { - unlockAndRun(new Runnable() { - @Override - public void run() { - mLockHelper.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(); - - } -}