Kotlinized Magikeyboard

This commit is contained in:
J-Jamet
2019-07-18 21:05:29 +02:00
parent 3d01e09198
commit 4b659248f7
17 changed files with 660 additions and 807 deletions

View File

@@ -407,7 +407,7 @@ class GroupActivity : LockingActivity(),
EntryActivity.launch(this@GroupActivity, entry, readOnly)
},
{
MagikIME.setEntryKey(getEntry(entry))
MagikIME.entryKey = getEntry(entry)
// Show the notification if allowed in Preferences
if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) {
startService(Intent(
@@ -441,7 +441,7 @@ class GroupActivity : LockingActivity(),
if (entry.containsCustomFields()) {
entry.fields
.doActionToAllCustomProtectedField { key, value ->
entryModel.addCustomField(
entryModel.customFields.add(
Field(key, value.toString()))
}
}

View File

@@ -1,170 +0,0 @@
package com.kunzisoft.keepass.magikeyboard;
import android.app.NotificationChannel;
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.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v7.preference.PreferenceManager;
import android.util.Log;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver;
import com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver;
import com.kunzisoft.keepass.timeout.TimeoutHelper;
import static com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver.LOCK_ACTION;
public class KeyboardEntryNotificationService extends Service {
private static final String TAG = KeyboardEntryNotificationService.class.getName();
private static final String CHANNEL_ID_KEYBOARD = "com.kunzisoft.keyboard.notification.entry.channel";
private static final String CHANNEL_NAME_KEYBOARD = "Magikeyboard notification";
private NotificationManager notificationManager;
private Thread cleanNotificationTimer;
private int notificationId = 582;
private long notificationTimeoutMilliSecs;
private LockBroadcastReceiver lockBroadcastReceiver;
private PendingIntent pendingDeleteIntent;
public KeyboardEntryNotificationService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// Create notification channel for Oreo+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID_KEYBOARD,
CHANNEL_NAME_KEYBOARD,
NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}
// Register a lock receiver to stop notification service when lock on keyboard is performed
lockBroadcastReceiver = new LockBroadcastReceiver() {
@Override
public void onReceiveLock(Context context, Intent intent) {
context.stopService(new Intent(context, KeyboardEntryNotificationService.class));
}
};
registerReceiver(lockBroadcastReceiver, new IntentFilter(LOCK_ACTION));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
Log.w(TAG, "null intent");
} else {
newNotification();
}
return START_NOT_STICKY;
}
private void newNotification() {
Intent deleteIntent = new Intent(this, NotificationDeleteBroadcastReceiver.class);
pendingDeleteIntent =
PendingIntent.getBroadcast(getApplicationContext(), 0, deleteIntent, 0);
if (MagikIME.getEntryKey() != null) {
String entryTitle = getString(R.string.keyboard_notification_entry_content_title_text);
String entryUsername = "";
if (!MagikIME.getEntryKey().getTitle().isEmpty())
entryTitle = MagikIME.getEntryKey().getTitle();
if (!MagikIME.getEntryKey().getUsername().isEmpty())
entryUsername = MagikIME.getEntryKey().getUsername();
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID_KEYBOARD)
.setSmallIcon(R.drawable.ic_vpn_key_white_24dp)
.setContentTitle(getString(R.string.keyboard_notification_entry_content_title, entryTitle))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setContentText(getString(R.string.keyboard_notification_entry_content_text, entryUsername))
.setAutoCancel(false)
.setContentIntent(null)
.setDeleteIntent(pendingDeleteIntent);
notificationManager.cancel(notificationId);
notificationManager.notify(notificationId, builder.build());
// Timeout only if notification clear is available
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(getString(R.string.keyboard_notification_entry_clear_close_key),
getResources().getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) {
String keyboardTimeout = sharedPreferences.getString(getString(R.string.keyboard_entry_timeout_key),
getString(R.string.timeout_default));
try {
notificationTimeoutMilliSecs = Long.parseLong(keyboardTimeout);
} catch (NumberFormatException e) {
notificationTimeoutMilliSecs = TimeoutHelper.DEFAULT_TIMEOUT;
}
if (notificationTimeoutMilliSecs != TimeoutHelper.TIMEOUT_NEVER) {
stopTask(cleanNotificationTimer);
cleanNotificationTimer = new Thread(() -> {
int maxPos = 100;
long posDurationMills = notificationTimeoutMilliSecs / maxPos;
for (int pos = maxPos; pos > 0; --pos) {
builder.setProgress(maxPos, pos, false);
notificationManager.notify(notificationId, builder.build());
try {
Thread.sleep(posDurationMills);
} catch (InterruptedException e) {
break;
}
}
try {
pendingDeleteIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Unable to delete keyboard notification");
}
});
cleanNotificationTimer.start();
}
}
}
}
private void stopTask(Thread task) {
if (task != null && task.isAlive())
task.interrupt();
}
private void destroyKeyboardNotification() {
stopTask(cleanNotificationTimer);
cleanNotificationTimer = null;
unregisterReceiver(lockBroadcastReceiver);
pendingDeleteIntent.cancel();
notificationManager.cancel(notificationId);
}
@Override
public void onDestroy() {
destroyKeyboardNotification();
super.onDestroy();
}
}

View File

@@ -0,0 +1,160 @@
package com.kunzisoft.keepass.magikeyboard
import android.app.NotificationChannel
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.os.Build
import android.os.IBinder
import android.support.v4.app.NotificationCompat
import android.support.v7.preference.PreferenceManager
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver.Companion.LOCK_ACTION
import com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver
import com.kunzisoft.keepass.timeout.TimeoutHelper
class KeyboardEntryNotificationService : Service() {
private var notificationManager: NotificationManager? = null
private var cleanNotificationTimer: Thread? = null
private val notificationId = 582
private var notificationTimeoutMilliSecs: Long = 0
private var lockBroadcastReceiver: LockBroadcastReceiver? = null
private var pendingDeleteIntent: PendingIntent? = null
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Create notification channel for Oreo+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID_KEYBOARD,
CHANNEL_NAME_KEYBOARD,
NotificationManager.IMPORTANCE_LOW)
notificationManager?.createNotificationChannel(channel)
}
// Register a lock receiver to stop notification service when lock on keyboard is performed
lockBroadcastReceiver = object : LockBroadcastReceiver() {
override fun onReceiveLock(context: Context, intent: Intent) {
context.stopService(Intent(context, KeyboardEntryNotificationService::class.java))
}
}
registerReceiver(lockBroadcastReceiver, IntentFilter(LOCK_ACTION))
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
Log.w(TAG, "null intent")
} else {
newNotification()
}
return START_NOT_STICKY
}
private fun newNotification() {
val deleteIntent = Intent(this, NotificationDeleteBroadcastReceiver::class.java)
pendingDeleteIntent = PendingIntent.getBroadcast(applicationContext, 0, deleteIntent, 0)
if (MagikIME.entryKey != null) {
var entryTitle: String? = getString(R.string.keyboard_notification_entry_content_title_text)
var entryUsername: String? = ""
if (MagikIME.entryKey!!.title.isNotEmpty())
entryTitle = MagikIME.entryKey!!.title
if (MagikIME.entryKey!!.username.isNotEmpty())
entryUsername = MagikIME.entryKey!!.username
val builder = NotificationCompat.Builder(this, CHANNEL_ID_KEYBOARD)
.setSmallIcon(R.drawable.ic_vpn_key_white_24dp)
.setContentTitle(getString(R.string.keyboard_notification_entry_content_title, entryTitle))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setContentText(getString(R.string.keyboard_notification_entry_content_text, entryUsername))
.setAutoCancel(false)
.setContentIntent(null)
.setDeleteIntent(pendingDeleteIntent)
notificationManager?.cancel(notificationId)
notificationManager?.notify(notificationId, builder.build())
// Timeout only if notification clear is available
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
if (sharedPreferences.getBoolean(getString(R.string.keyboard_notification_entry_clear_close_key),
resources.getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) {
val keyboardTimeout = sharedPreferences.getString(getString(R.string.keyboard_entry_timeout_key),
getString(R.string.timeout_default))
notificationTimeoutMilliSecs = try {
java.lang.Long.parseLong(keyboardTimeout)
} catch (e: NumberFormatException) {
TimeoutHelper.DEFAULT_TIMEOUT
}
if (notificationTimeoutMilliSecs != TimeoutHelper.TIMEOUT_NEVER) {
stopTask(cleanNotificationTimer)
cleanNotificationTimer = Thread {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 1) {
builder.setProgress(maxPos, pos, false)
notificationManager?.notify(notificationId, builder.build())
try {
Thread.sleep(posDurationMills)
} catch (e: InterruptedException) {
break
}
}
try {
pendingDeleteIntent?.send()
} catch (e: PendingIntent.CanceledException) {
Log.e(TAG, "Unable to delete keyboard notification")
}
}
cleanNotificationTimer?.start()
}
}
}
}
private fun stopTask(task: Thread?) {
if (task != null && task.isAlive)
task.interrupt()
}
private fun destroyKeyboardNotification() {
stopTask(cleanNotificationTimer)
cleanNotificationTimer = null
unregisterReceiver(lockBroadcastReceiver)
pendingDeleteIntent?.cancel()
notificationManager?.cancel(notificationId)
}
override fun onDestroy() {
destroyKeyboardNotification()
super.onDestroy()
}
companion object {
private val TAG = KeyboardEntryNotificationService::class.java.name
private const val CHANNEL_ID_KEYBOARD = "com.kunzisoft.keyboard.notification.entry.channel"
private const val CHANNEL_NAME_KEYBOARD = "Magikeyboard notification"
}
}

View File

@@ -1,313 +0,0 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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 3 of the License, or
* (at your option) any later version.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.magikeyboard;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.media.AudioManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.PopupWindow;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.magikeyboard.adapter.FieldsAdapter;
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver;
import com.kunzisoft.keepass.magikeyboard.view.MagikeyboardView;
import com.kunzisoft.keepass.model.Entry;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import static com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver.LOCK_ACTION;
public class MagikIME extends InputMethodService
implements KeyboardView.OnKeyboardActionListener {
private static final String TAG = MagikIME.class.getName();
public static final int KEY_BACK_KEYBOARD = 600;
public static final int KEY_CHANGE_KEYBOARD = 601;
private static final int KEY_UNLOCK = 610;
private static final int KEY_LOCK = 611;
private static final int KEY_ENTRY = 620;
private static final int KEY_USERNAME = 500;
private static final int KEY_PASSWORD = 510;
private static final int KEY_URL = 520;
private static final int KEY_FIELDS = 530;
private static Entry entryKey = null;
private KeyboardView keyboardView;
private Keyboard keyboard;
private Keyboard keyboard_entry;
private PopupWindow popupCustomKeys;
private FieldsAdapter fieldsAdapter;
private boolean playSoundDuringCLick;
private LockBroadcastReceiver lockBroadcastReceiver;
@Override
public void onCreate() {
super.onCreate();
// Remove the entry and lock the keyboard when the lock signal is receive
lockBroadcastReceiver = new LockBroadcastReceiver() {
@Override
public void onReceiveLock(Context context, Intent intent) {
entryKey = null;
assignKeyboardView();
}
};
registerReceiver(lockBroadcastReceiver, new IntentFilter(LOCK_ACTION));
}
@Override
public View onCreateInputView() {
keyboardView = (MagikeyboardView) getLayoutInflater().inflate(R.layout.keyboard_view, null);
keyboard = new Keyboard(this, R.xml.keyboard_password);
keyboard_entry = new Keyboard(this, R.xml.keyboard_password_entry);
assignKeyboardView();
keyboardView.setOnKeyboardActionListener(this);
keyboardView.setPreviewEnabled(false);
Context context = getBaseContext();
View popupFieldsView = LayoutInflater.from(context)
.inflate(R.layout.keyboard_popup_fields, new FrameLayout(context));
if (popupCustomKeys != null)
popupCustomKeys.dismiss();
popupCustomKeys = new PopupWindow(context);
popupCustomKeys.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
popupCustomKeys.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
popupCustomKeys.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
popupCustomKeys.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
popupCustomKeys.setContentView(popupFieldsView);
RecyclerView recyclerView = popupFieldsView.findViewById(R.id.keyboard_popup_fields_list);
fieldsAdapter = new FieldsAdapter(this);
fieldsAdapter.setOnItemClickListener(item -> getCurrentInputConnection().commitText(item.getValue(), 1));
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true));
recyclerView.setAdapter(fieldsAdapter);
View closeView = popupFieldsView.findViewById(R.id.keyboard_popup_close);
closeView.setOnClickListener(v -> popupCustomKeys.dismiss());
return keyboardView;
}
private void assignKeyboardView() {
if (keyboardView != null) {
if (entryKey != null) {
if (keyboard_entry != null)
keyboardView.setKeyboard(keyboard_entry);
} else {
if (keyboard != null) {
dismissCustomKeys();
keyboardView.setKeyboard(keyboard);
}
}
// Define preferences
keyboardView.setHapticFeedbackEnabled(PreferencesUtil.INSTANCE.enableKeyboardVibration(this));
playSoundDuringCLick = PreferencesUtil.INSTANCE.enableKeyboardSound(this);
}
}
@Override
public void onStartInputView(EditorInfo info, boolean restarting) {
super.onStartInputView(info, restarting);
assignKeyboardView();
}
public static Entry getEntryKey() {
return entryKey;
}
public static void setEntryKey(Entry entry) {
entryKey = entry;
}
public static void deleteEntryKey(Context context) {
Intent lockIntent = new Intent(LOCK_ACTION);
context.sendBroadcast(lockIntent);
entryKey = null;
}
private void playVibration(int keyCode){
switch(keyCode){
case Keyboard.KEYCODE_DELETE:
break;
default: keyboardView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
}
private void playClick(int keyCode){
AudioManager am = (AudioManager)getSystemService(AUDIO_SERVICE);
if (am != null)
switch(keyCode){
case Keyboard.KEYCODE_DONE:
case 10:
am.playSoundEffect(AudioManager.FX_KEYPRESS_RETURN);
break;
case Keyboard.KEYCODE_DELETE:
am.playSoundEffect(AudioManager.FX_KEYPRESS_DELETE);
break;
default: am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD);
}
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
InputConnection inputConnection = getCurrentInputConnection();
// Vibrate
playVibration(primaryCode);
// Play a sound
if (playSoundDuringCLick)
playClick(primaryCode);
switch(primaryCode){
case KEY_BACK_KEYBOARD:
try {
InputMethodManager imeManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
assert imeManager != null;
assert getWindow().getWindow() != null;
imeManager.switchToLastInputMethod(getWindow().getWindow().getAttributes().token);
} catch (Exception e) {
Log.e(TAG, "Unable to switch to the previous IME", e);
InputMethodManager imeManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imeManager != null)
imeManager.showInputMethodPicker();
}
break;
case KEY_CHANGE_KEYBOARD:
InputMethodManager imeManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imeManager != null)
imeManager.showInputMethodPicker();
break;
case KEY_UNLOCK:
// TODO Unlock key
break;
case KEY_ENTRY:
// Stop current service and reinit entry
stopService(new Intent(this, KeyboardEntryNotificationService.class));
entryKey = null;
Intent intent = new Intent(this, KeyboardLauncherActivity.class);
// New task needed because don't launch from an Activity context
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
break;
case KEY_LOCK:
deleteEntryKey(this);
dismissCustomKeys();
break;
case KEY_USERNAME:
if (entryKey != null) {
inputConnection.commitText(entryKey.getUsername(), 1);
}
break;
case KEY_PASSWORD:
if (entryKey != null) {
inputConnection.commitText(entryKey.getPassword(), 1);
}
break;
case KEY_URL:
if (entryKey != null) {
inputConnection.commitText(entryKey.getUrl(), 1);
}
break;
case KEY_FIELDS:
fieldsAdapter.setFields(entryKey.getCustomFields());
popupCustomKeys.showAtLocation(keyboardView, Gravity.END | Gravity.TOP, 0, 0);
break;
case Keyboard.KEYCODE_DELETE :
inputConnection.deleteSurroundingText(1, 0);
break;
case Keyboard.KEYCODE_DONE:
inputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
break;
default:
}
}
@Override
public void onPress(int primaryCode) {
AudioManager am = (AudioManager)getSystemService(AUDIO_SERVICE);
if (am != null)
switch(primaryCode){
case Keyboard.KEYCODE_DELETE:
keyboardView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
break;
default:
}
}
@Override
public void onRelease(int primaryCode) {
switch(primaryCode){
case Keyboard.KEYCODE_DELETE:
break;
default:
}
}
@Override
public void onText(CharSequence text) {}
@Override
public void swipeDown() {}
@Override
public void swipeLeft() {}
@Override
public void swipeRight() {}
@Override
public void swipeUp() {}
private void dismissCustomKeys() {
if (popupCustomKeys != null)
popupCustomKeys.dismiss();
if (fieldsAdapter != null)
fieldsAdapter.clear();
}
@Override
public void onDestroy() {
dismissCustomKeys();
unregisterReceiver(lockBroadcastReceiver);
super.onDestroy();
}
}

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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 3 of the License, or
* (at your option) any later version.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.magikeyboard
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.inputmethodservice.InputMethodService
import android.inputmethodservice.Keyboard
import android.inputmethodservice.KeyboardView
import android.media.AudioManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.PopupWindow
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.magikeyboard.adapter.FieldsAdapter
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver
import com.kunzisoft.keepass.magikeyboard.receiver.LockBroadcastReceiver.Companion.LOCK_ACTION
import com.kunzisoft.keepass.magikeyboard.view.MagikeyboardView
import com.kunzisoft.keepass.model.Entry
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.settings.PreferencesUtil
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private var keyboardView: KeyboardView? = null
private var keyboard: Keyboard? = null
private var keyboardEntry: Keyboard? = null
private var popupCustomKeys: PopupWindow? = null
private var fieldsAdapter: FieldsAdapter? = null
private var playSoundDuringCLick: Boolean = false
private var lockBroadcastReceiver: LockBroadcastReceiver? = null
override fun onCreate() {
super.onCreate()
// Remove the entry and lock the keyboard when the lock signal is receive
lockBroadcastReceiver = object : LockBroadcastReceiver() {
override fun onReceiveLock(context: Context, intent: Intent) {
entryKey = null
assignKeyboardView()
}
}
registerReceiver(lockBroadcastReceiver, IntentFilter(LOCK_ACTION))
}
override fun onCreateInputView(): View {
keyboardView = layoutInflater.inflate(R.layout.keyboard_view, null) as MagikeyboardView
if (keyboardView != null) {
keyboard = Keyboard(this, R.xml.keyboard_password)
keyboardEntry = Keyboard(this, R.xml.keyboard_password_entry)
assignKeyboardView()
keyboardView?.setOnKeyboardActionListener(this)
keyboardView?.isPreviewEnabled = false
val context = baseContext
val popupFieldsView = LayoutInflater.from(context)
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
popupCustomKeys?.dismiss()
popupCustomKeys = PopupWindow(context)
popupCustomKeys?.width = WindowManager.LayoutParams.WRAP_CONTENT
popupCustomKeys?.height = WindowManager.LayoutParams.WRAP_CONTENT
popupCustomKeys?.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
popupCustomKeys?.inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
popupCustomKeys?.contentView = popupFieldsView
val recyclerView = popupFieldsView.findViewById<RecyclerView>(R.id.keyboard_popup_fields_list)
fieldsAdapter = FieldsAdapter(this)
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
override fun onItemClick(item: Field) {
currentInputConnection.commitText(item.value, 1)
}
}
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true)
recyclerView.adapter = fieldsAdapter
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
return keyboardView!!
}
return super.onCreateInputView()
}
private fun assignKeyboardView() {
if (keyboardView != null) {
if (entryKey != null) {
if (keyboardEntry != null)
keyboardView?.keyboard = keyboardEntry
} else {
if (keyboard != null) {
dismissCustomKeys()
keyboardView?.keyboard = keyboard
}
}
// Define preferences
keyboardView?.isHapticFeedbackEnabled = PreferencesUtil.enableKeyboardVibration(this)
playSoundDuringCLick = PreferencesUtil.enableKeyboardSound(this)
}
}
override fun onStartInputView(info: EditorInfo, restarting: Boolean) {
super.onStartInputView(info, restarting)
assignKeyboardView()
}
private fun playVibration(keyCode: Int) {
when (keyCode) {
Keyboard.KEYCODE_DELETE -> {}
else -> keyboardView?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
private fun playClick(keyCode: Int) {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager?
when (keyCode) {
Keyboard.KEYCODE_DONE, 10 -> audioManager?.playSoundEffect(AudioManager.FX_KEYPRESS_RETURN)
Keyboard.KEYCODE_DELETE -> audioManager?.playSoundEffect(AudioManager.FX_KEYPRESS_DELETE)
else -> audioManager?.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD)
}
}
override fun onKey(primaryCode: Int, keyCodes: IntArray) {
val inputConnection = currentInputConnection
// Vibrate
playVibration(primaryCode)
// Play a sound
if (playSoundDuringCLick)
playClick(primaryCode)
when (primaryCode) {
KEY_BACK_KEYBOARD -> try {
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (window.window != null)
imeManager.switchToLastInputMethod(window.window!!.attributes.token)
} catch (e: Exception) {
Log.e(TAG, "Unable to switch to the previous IME", e)
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imeManager.showInputMethodPicker()
}
KEY_CHANGE_KEYBOARD -> {
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imeManager.showInputMethodPicker()
}
KEY_UNLOCK -> {
}
KEY_ENTRY -> {
// Stop current service and reinit entry
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
entryKey = null
val intent = Intent(this, KeyboardLauncherActivity::class.java)
// New task needed because don't launch from an Activity context
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
KEY_LOCK -> {
deleteEntryKey(this)
dismissCustomKeys()
}
KEY_USERNAME -> {
if (entryKey != null) {
inputConnection.commitText(entryKey!!.username, 1)
}
}
KEY_PASSWORD -> {
if (entryKey != null) {
inputConnection.commitText(entryKey!!.password, 1)
}
}
KEY_URL -> {
if (entryKey != null) {
inputConnection.commitText(entryKey!!.url, 1)
}
}
KEY_FIELDS -> {
if (entryKey != null) {
fieldsAdapter?.fields = entryKey!!.customFields
}
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
}
Keyboard.KEYCODE_DELETE -> inputConnection.deleteSurroundingText(1, 0)
Keyboard.KEYCODE_DONE -> inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
}// TODO Unlock key
}
override fun onPress(primaryCode: Int) {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager?
if (audioManager != null)
when (primaryCode) {
Keyboard.KEYCODE_DELETE -> keyboardView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}
override fun onRelease(primaryCode: Int) {
when (primaryCode) {
Keyboard.KEYCODE_DELETE -> { }
}
}
override fun onText(text: CharSequence) {}
override fun swipeDown() {}
override fun swipeLeft() {}
override fun swipeRight() {}
override fun swipeUp() {}
private fun dismissCustomKeys() {
popupCustomKeys?.dismiss()
fieldsAdapter?.clear()
}
override fun onDestroy() {
dismissCustomKeys()
unregisterReceiver(lockBroadcastReceiver)
super.onDestroy()
}
companion object {
private val TAG = MagikIME::class.java.name
const val KEY_BACK_KEYBOARD = 600
const val KEY_CHANGE_KEYBOARD = 601
private const val KEY_UNLOCK = 610
private const val KEY_LOCK = 611
private const val KEY_ENTRY = 620
private const val KEY_USERNAME = 500
private const val KEY_PASSWORD = 510
private const val KEY_URL = 520
private const val KEY_FIELDS = 530
var entryKey: Entry? = null
fun deleteEntryKey(context: Context) {
val lockIntent = Intent(LOCK_ACTION)
context.sendBroadcast(lockIntent)
entryKey = null
}
}
}

View File

@@ -1,76 +0,0 @@
package com.kunzisoft.keepass.magikeyboard.adapter;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.model.Field;
import java.util.ArrayList;
import java.util.List;
public class FieldsAdapter extends RecyclerView.Adapter<FieldsAdapter.FieldViewHolder> {
private LayoutInflater inflater;
private List<Field> fields;
private OnItemClickListener listener;
public FieldsAdapter(Context context) {
this.inflater = LayoutInflater.from(context);
this.fields = new ArrayList<>();
}
public void setFields(List<Field> fields) {
this.fields = fields;
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
@NonNull
@Override
public FieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false);
return new FieldViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull FieldViewHolder holder, int position) {
Field field = fields.get(position);
holder.name.setText(field.getName());
holder.bind(field, listener);
}
@Override
public int getItemCount() {
return fields.size();
}
public void clear() {
fields.clear();
}
public interface OnItemClickListener {
void onItemClick(Field item);
}
class FieldViewHolder extends RecyclerView.ViewHolder {
TextView name;
FieldViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.keyboard_popup_field_item_name);
}
void bind(final Field item, final OnItemClickListener listener) {
itemView.setOnClickListener(v -> listener.onItemClick(item));
}
}
}

View File

@@ -0,0 +1,53 @@
package com.kunzisoft.keepass.magikeyboard.adapter
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field
import java.util.ArrayList
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var fields: MutableList<Field> = ArrayList()
var onItemClickListener: OnItemClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
return FieldViewHolder(view)
}
override fun onBindViewHolder(holder: FieldViewHolder, position: Int) {
val field = fields[position]
holder.name.text = field.name
holder.bind(field, onItemClickListener)
}
override fun getItemCount(): Int {
return fields.size
}
fun clear() {
fields.clear()
}
interface OnItemClickListener {
fun onItemClick(item: Field)
}
inner class FieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var name: TextView = itemView.findViewById(R.id.keyboard_popup_field_item_name)
fun bind(item: Field, listener: OnItemClickListener?) {
itemView.setOnClickListener { listener?.onItemClick(item) }
}
}
}

View File

@@ -1,22 +0,0 @@
package com.kunzisoft.keepass.magikeyboard.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public abstract class LockBroadcastReceiver extends BroadcastReceiver {
public static final String LOCK_ACTION = "com.kunzisoft.keepass.LOCK";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null
&& action.equals(LOCK_ACTION)) {
onReceiveLock(context, intent);
}
}
public abstract void onReceiveLock(Context context, Intent intent);
}

View File

@@ -0,0 +1,23 @@
package com.kunzisoft.keepass.magikeyboard.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
abstract class LockBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action != null && action == LOCK_ACTION) {
onReceiveLock(context, intent)
}
}
abstract fun onReceiveLock(context: Context, intent: Intent)
companion object {
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
}
}

View File

@@ -1,27 +0,0 @@
package com.kunzisoft.keepass.magikeyboard.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService;
import com.kunzisoft.keepass.magikeyboard.MagikIME;
public class NotificationDeleteBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Clear the entry if define in preferences
SharedPreferences sharedPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPreferences.getBoolean(context.getString(R.string.keyboard_notification_entry_clear_close_key),
context.getResources().getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) {
MagikIME.deleteEntryKey(context);
}
// Stop the service in all cases
context.stopService(new Intent(context, KeyboardEntryNotificationService.class));
}
}

View File

@@ -0,0 +1,23 @@
package com.kunzisoft.keepass.magikeyboard.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
class NotificationDeleteBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Clear the entry if define in preferences
val sharedPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(context)
if (sharedPreferences.getBoolean(context.getString(R.string.keyboard_notification_entry_clear_close_key),
context.resources.getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) {
MagikIME.deleteEntryKey(context)
}
// Stop the service in all cases
context.stopService(Intent(context, KeyboardEntryNotificationService::class.java))
}
}

View File

@@ -1,39 +0,0 @@
package com.kunzisoft.keepass.magikeyboard.view;
import android.content.Context;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import static com.kunzisoft.keepass.magikeyboard.MagikIME.KEY_BACK_KEYBOARD;
import static com.kunzisoft.keepass.magikeyboard.MagikIME.KEY_CHANGE_KEYBOARD;
public class MagikeyboardView extends KeyboardView {
public MagikeyboardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MagikeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected boolean onLongPress(Keyboard.Key key) {
// TODO Action on long press
if (key.codes[0] == KEY_BACK_KEYBOARD) {
getOnKeyboardActionListener().onKey(KEY_CHANGE_KEYBOARD, null);
return true;
} else {
//Log.d("LatinKeyboardView", "KEY: " + key.codes[0]);
return super.onLongPress(key);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.kunzisoft.keepass.magikeyboard.view
import android.content.Context
import android.inputmethodservice.Keyboard
import android.inputmethodservice.KeyboardView
import android.os.Build
import android.support.annotation.RequiresApi
import android.util.AttributeSet
import com.kunzisoft.keepass.magikeyboard.MagikIME.Companion.KEY_BACK_KEYBOARD
import com.kunzisoft.keepass.magikeyboard.MagikIME.Companion.KEY_CHANGE_KEYBOARD
class MagikeyboardView : KeyboardView {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
override fun onLongPress(key: Keyboard.Key): Boolean {
// TODO Action on long press
if (key.codes[0] == KEY_BACK_KEYBOARD) {
onKeyboardActionListener.onKey(KEY_CHANGE_KEYBOARD, null)
return true
} else {
//Log.d("LatinKeyboardView", "KEY: " + key.codes[0]);
return super.onLongPress(key)
}
}
}

View File

@@ -1,99 +0,0 @@
package com.kunzisoft.keepass.model;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.List;
public class Entry implements Parcelable {
private String title;
private String username;
private String password;
private String url;
private List<Field> customFields;
public Entry() {
this.title = "";
this.username = "";
this.password = "";
this.url = "";
this.customFields = new ArrayList<>();
}
protected Entry(Parcel in) {
title = in.readString();
username = in.readString();
password = in.readString();
url = in.readString();
//noinspection unchecked
customFields = in.readArrayList(Field.class.getClassLoader());
}
public static final Creator<Entry> CREATOR = new Creator<Entry>() {
@Override
public Entry createFromParcel(Parcel in) {
return new Entry(in);
}
@Override
public Entry[] newArray(int size) {
return new Entry[size];
}
};
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<Field> getCustomFields() {
return customFields;
}
public void addCustomField(Field field) {
this.customFields.add(field);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(title);
parcel.writeString(username);
parcel.writeString(password);
parcel.writeString(url);
parcel.writeArray(customFields.toArray());
}
}

View File

@@ -0,0 +1,51 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import java.util.ArrayList
class Entry : Parcelable {
var title: String = ""
var username: String = ""
var password: String = ""
var url: String = ""
var customFields: MutableList<Field> = ArrayList()
constructor()
private constructor(parcel: Parcel) {
title = parcel.readString()
username = parcel.readString()
password = parcel.readString()
url = parcel.readString()
parcel.readList(customFields, Field::class.java.classLoader)
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, i: Int) {
parcel.writeString(title)
parcel.writeString(username)
parcel.writeString(password)
parcel.writeString(url)
parcel.writeArray(customFields.toTypedArray())
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
override fun createFromParcel(parcel: Parcel): Entry {
return Entry(parcel)
}
override fun newArray(size: Int): Array<Entry?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -1,59 +0,0 @@
package com.kunzisoft.keepass.model;
import android.os.Parcel;
import android.os.Parcelable;
public class Field implements Parcelable {
private String name;
private String value;
public Field(String name, String value) {
this.name = name;
this.value = value;
}
public Field(Parcel in) {
this.name = in.readString();
this.value = in.readString();
}
public static final Creator<Field> CREATOR = new Creator<Field>() {
@Override
public Field createFromParcel(Parcel in) {
return new Field(in);
}
@Override
public Field[] newArray(int size) {
return new Field[size];
}
};
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(value);
}
}

View File

@@ -0,0 +1,43 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
class Field : Parcelable {
var name: String? = null
var value: String? = null
constructor(name: String, value: String) {
this.name = name
this.value = value
}
constructor(parcel: Parcel) {
this.name = parcel.readString()
this.value = parcel.readString()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(name)
dest.writeString(value)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Field> = object : Parcelable.Creator<Field> {
override fun createFromParcel(parcel: Parcel): Field {
return Field(parcel)
}
override fun newArray(size: Int): Array<Field?> {
return arrayOfNulls(size)
}
}
}
}