keepass2android/src/keepass2android/services/CopyToClipboardService.cs

786 lines
24 KiB
C#

/*
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll.
Keepass2Android 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.
Keepass2Android 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 Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Android.AccessibilityServices;
using Android.Support.V4.App;
using Java.Util;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Widget;
using Android.Preferences;
using Android.Views.Accessibility;
using KeePassLib;
using KeePassLib.Utility;
using Android.Views.InputMethods;
using KeePass.Util.Spr;
namespace keepass2android
{
/// <summary>
/// Service to show the notifications to make the current entry accessible through clipboard or the KP2A keyboard.
/// </summary>
/// The name reflects only the possibility through clipboard because keyboard was introduced later.
/// The notifications require to be displayed by a service in order to be kept when the activity is closed
/// after searching for a URL.
[Service]
public class CopyToClipboardService : Service
{
class PasswordAccessNotificationBuilder
{
private readonly Context _ctx;
private readonly NotificationManager _notificationManager;
public PasswordAccessNotificationBuilder(Context ctx, NotificationManager notificationManager)
{
_ctx = ctx;
_notificationManager = notificationManager;
}
private bool _hasPassword;
private bool _hasUsername;
private bool _hasKeyboard;
public void AddPasswordAccess()
{
_hasPassword = true;
}
public void AddUsernameAccess()
{
_hasUsername = true;
}
public void AddKeyboardAccess()
{
_hasKeyboard = true;
}
public int CreateNotifications(string entryName)
{
if (((int) Build.VERSION.SdkInt < 16) ||
(PreferenceManager.GetDefaultSharedPreferences(_ctx)
.GetBoolean(_ctx.GetString(Resource.String.ShowSeparateNotifications_key),
_ctx.Resources.GetBoolean(Resource.Boolean.ShowSeparateNotifications_default))))
{
return CreateSeparateNotifications(entryName);
}
else
{
return CreateCombinedNotification(entryName);
}
}
private int CreateCombinedNotification(string entryName)
{
if ((!_hasUsername) && (!_hasPassword) && (!_hasKeyboard))
return 0;
NotificationCompat.Builder notificationBuilder;
if (_hasKeyboard)
{
notificationBuilder = GetNotificationBuilder(Intents.CheckKeyboard, Resource.String.available_through_keyboard,
Resource.Drawable.ic_notify_keyboard, entryName);
}
else
{
notificationBuilder = GetNotificationBuilder(null, Resource.String.entry_is_available, Resource.Drawable.ic_launcher_gray,
entryName);
}
//add action buttons to base notification:
if (_hasUsername)
notificationBuilder.AddAction(new NotificationCompat.Action(Resource.Drawable.ic_action_username,
_ctx.GetString(Resource.String.menu_copy_user),
GetPendingIntent(Intents.CopyUsername, Resource.String.menu_copy_user)));
if (_hasPassword)
notificationBuilder.AddAction(new NotificationCompat.Action(Resource.Drawable.ic_action_password,
_ctx.GetString(Resource.String.menu_copy_pass),
GetPendingIntent(Intents.CopyPassword, Resource.String.menu_copy_pass)));
notificationBuilder.SetPriority((int)Android.App.NotificationPriority.Max);
var notification = notificationBuilder.Build();
notification.DeleteIntent = CreateDeleteIntent(NotifyCombined);
_notificationManager.Notify(NotifyCombined, notification);
return 1;
}
private int CreateSeparateNotifications(string entryName)
{
int numNotifications = 0;
if (_hasPassword)
{
// only show notification if password is available
Notification password = GetNotification(Intents.CopyPassword, Resource.String.copy_password,
Resource.Drawable.ic_action_password, entryName);
numNotifications++;
password.DeleteIntent = CreateDeleteIntent(NotifyPassword);
_notificationManager.Notify(NotifyPassword, password);
}
if (_hasUsername)
{
// only show notification if username is available
Notification username = GetNotification(Intents.CopyUsername, Resource.String.copy_username,
Resource.Drawable.ic_action_username, entryName);
username.DeleteIntent = CreateDeleteIntent(NotifyUsername);
_notificationManager.Notify(NotifyUsername, username);
numNotifications++;
}
if (_hasKeyboard)
{
// only show notification if username is available
Notification keyboard = GetNotification(Intents.CheckKeyboard, Resource.String.available_through_keyboard,
Resource.Drawable.ic_notify_keyboard, entryName);
keyboard.DeleteIntent = CreateDeleteIntent(NotifyKeyboard);
_notificationManager.Notify(NotifyKeyboard, keyboard);
numNotifications++;
}
return numNotifications;
}
//creates a delete intent (started when notification is cancelled by user or something else)
//requires different request codes for every item (otherwise the intents are identical)
PendingIntent CreateDeleteIntent(int requestCode)
{
Intent intent = new Intent(ActionNotificationCancelled);
Bundle extra = new Bundle();
extra.PutInt("requestCode", requestCode);
intent.PutExtras(extra);
return PendingIntent.GetBroadcast(_ctx, requestCode, intent, PendingIntentFlags.CancelCurrent);
}
private Notification GetNotification(String intentText, int descResId, int drawableResId, String entryName)
{
var builder = GetNotificationBuilder(intentText, descResId, drawableResId, entryName);
return builder.Build();
}
private NotificationCompat.Builder GetNotificationBuilder(string intentText, int descResId, int drawableResId, string entryName)
{
String desc = _ctx.GetString(descResId);
String title = _ctx.GetString(Resource.String.app_name);
if (!String.IsNullOrEmpty(entryName))
title += " (" + entryName + ")";
PendingIntent pending;
if (intentText == null)
{
pending = PendingIntent.GetActivity(_ctx.ApplicationContext, 0, new Intent(), 0);
}
else
{
pending = GetPendingIntent(intentText, descResId);
}
var builder = new NotificationCompat.Builder(_ctx);
builder.SetSmallIcon(drawableResId)
.SetContentText(desc)
.SetContentTitle(entryName)
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetTicker(entryName + ": " + desc)
.SetVisibility((int)Android.App.NotificationVisibility.Secret)
.SetContentIntent(pending);
return builder;
}
private PendingIntent GetPendingIntent(string intentText, int descResId)
{
PendingIntent pending;
Intent intent = new Intent(intentText);
intent.SetPackage(_ctx.PackageName);
pending = PendingIntent.GetBroadcast(_ctx, descResId, intent, PendingIntentFlags.CancelCurrent);
return pending;
}
}
public const int NotifyUsername = 1;
public const int NotifyPassword = 2;
public const int NotifyKeyboard = 3;
public const int ClearClipboard = 4;
public const int NotifyCombined = 5;
static public void CopyValueToClipboardWithTimeout(Context ctx, string text)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.CopyStringToClipboard);
i.PutExtra(_stringtocopy, text);
ctx.StartService(i);
}
static public void ActivateKeyboard(Context ctx)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.ActivateKeyboard);
ctx.StartService(i);
}
public static void CancelNotifications(Context ctx)
{
Intent i = new Intent(ctx, typeof(CopyToClipboardService));
i.SetAction(Intents.ClearNotificationsAndData);
ctx.StartService(i);
}
public CopyToClipboardService(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
NotificationDeletedBroadcastReceiver _notificationDeletedBroadcastReceiver;
StopOnLockBroadcastReceiver _stopOnLockBroadcastReceiver;
public CopyToClipboardService()
{
}
public override IBinder OnBind(Intent intent)
{
return null;
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
Kp2aLog.Log("Received intent to provide access to entry");
_stopOnLockBroadcastReceiver = new StopOnLockBroadcastReceiver(this);
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.DatabaseLocked);
RegisterReceiver(_stopOnLockBroadcastReceiver, filter);
if ((intent.Action == Intents.ShowNotification) || (intent.Action == Intents.UpdateKeyboard))
{
String uuidBytes = intent.GetStringExtra(EntryActivity.KeyEntry);
String searchUrl = intent.GetStringExtra(SearchUrlTask.UrlToSearchKey);
PwUuid entryId = PwUuid.Zero;
if (uuidBytes != null)
entryId = new PwUuid(MemUtil.HexStringToByteArray(uuidBytes));
PwEntryOutput entry;
try
{
if ((App.Kp2a.GetDb().LastOpenedEntry != null)
&& (entryId.Equals(App.Kp2a.GetDb().LastOpenedEntry.Uuid)))
{
entry = App.Kp2a.GetDb().LastOpenedEntry;
}
else
{
entry = new PwEntryOutput(App.Kp2a.GetDb().Entries[entryId], App.Kp2a.GetDb().KpDatabase);
}
}
catch (Exception)
{
//seems like restarting the service happened after closing the DB
StopSelf();
return StartCommandResult.NotSticky;
}
if (intent.Action == Intents.ShowNotification)
{
//first time opening the entry -> bring up the notifications
bool closeAfterCreate = intent.GetBooleanExtra(EntryActivity.KeyCloseAfterCreate, false);
DisplayAccessNotifications(entry, closeAfterCreate, searchUrl);
}
else //UpdateKeyboard
{
#if !EXCLUDE_KEYBOARD
//this action is received when the data in the entry has changed (e.g. by plugins)
//update the keyboard data.
//Check if keyboard is (still) available
if (Keepass2android.Kbbridge.KeyboardData.EntryId == entry.Uuid.ToHexString())
MakeAccessibleForKeyboard(entry, searchUrl);
#endif
}
}
if (intent.Action == Intents.CopyStringToClipboard)
{
TimeoutCopyToClipboard(intent.GetStringExtra(_stringtocopy));
}
if (intent.Action == Intents.ActivateKeyboard)
{
ActivateKp2aKeyboard();
}
if (intent.Action == Intents.ClearNotificationsAndData)
{
ClearNotifications();
}
return StartCommandResult.RedeliverIntent;
}
private void OnLockDatabase()
{
Kp2aLog.Log("Stopping clipboard service due to database lock");
StopSelf();
}
private NotificationManager _notificationManager;
private int _numElementsToWaitFor;
public override void OnDestroy()
{
Kp2aLog.Log("CopyToClipboardService.OnDestroy");
// These members might never get initialized if the app timed out
if (_stopOnLockBroadcastReceiver != null)
{
UnregisterReceiver(_stopOnLockBroadcastReceiver);
}
if (_notificationDeletedBroadcastReceiver != null)
{
UnregisterReceiver(_notificationDeletedBroadcastReceiver);
}
if (_notificationManager != null)
{
_notificationManager.Cancel(NotifyPassword);
_notificationManager.Cancel(NotifyUsername);
_notificationManager.Cancel(NotifyKeyboard);
_notificationManager.Cancel(NotifyCombined);
_numElementsToWaitFor = 0;
ClearKeyboard(true);
}
if (_clearClipboardTask != null)
{
Kp2aLog.Log("Clearing clipboard due to stop CopyToClipboardService");
_clearClipboardTask.Run();
}
Kp2aLog.Log("Destroyed Show-Notification-Receiver.");
base.OnDestroy();
}
private const string ActionNotificationCancelled = "notification_cancelled";
public void DisplayAccessNotifications(PwEntryOutput entry, bool closeAfterCreate, string searchUrl)
{
var hadKeyboardData = ClearNotifications();
String entryName = entry.OutputStrings.ReadSafe(PwDefs.TitleField);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
var notBuilder = new PasswordAccessNotificationBuilder(this, _notificationManager);
if (prefs.GetBoolean(GetString(Resource.String.CopyToClipboardNotification_key), Resources.GetBoolean(Resource.Boolean.CopyToClipboardNotification_default)))
{
if (entry.OutputStrings.ReadSafe(PwDefs.PasswordField).Length > 0)
{
notBuilder.AddPasswordAccess();
}
if (entry.OutputStrings.ReadSafe(PwDefs.UserNameField).Length > 0)
{
notBuilder.AddUsernameAccess();
}
}
bool hasKeyboardDataNow = false;
if (prefs.GetBoolean(GetString(Resource.String.UseKp2aKeyboard_key), Resources.GetBoolean(Resource.Boolean.UseKp2aKeyboard_default)))
{
//keyboard
hasKeyboardDataNow = MakeAccessibleForKeyboard(entry, searchUrl);
if (hasKeyboardDataNow)
{
notBuilder.AddKeyboardAccess();
if (closeAfterCreate && Keepass2android.Autofill.AutoFillService.IsAvailable && (!Keepass2android.Autofill.AutoFillService.IsRunning))
{
if (!prefs.GetBoolean("has_asked_autofillservice", false))
{
var i = new Intent(this, typeof (ActivateAutoFillActivity));
i.AddFlags(ActivityFlags.NewTask | ActivityFlags.ClearTask);
StartActivity(i);
prefs.Edit().PutBoolean("has_asked_autofillservice", true).Commit();
}
}
else ActivateKeyboardIfAppropriate(closeAfterCreate, prefs);
}
}
if ((!hasKeyboardDataNow) && (hadKeyboardData))
{
ClearKeyboard(true); //this clears again and then (this is the point) broadcasts that we no longer have keyboard data
}
_numElementsToWaitFor = notBuilder.CreateNotifications(entryName);
if (_numElementsToWaitFor == 0)
{
StopSelf();
return;
}
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.CopyUsername);
filter.AddAction(Intents.CopyPassword);
filter.AddAction(Intents.CheckKeyboard);
//register receiver to get notified when notifications are discarded in which case we can shutdown the service
_notificationDeletedBroadcastReceiver = new NotificationDeletedBroadcastReceiver(this);
IntentFilter deletefilter = new IntentFilter();
deletefilter.AddAction(ActionNotificationCancelled);
RegisterReceiver(_notificationDeletedBroadcastReceiver, deletefilter);
}
public void ActivateKeyboardIfAppropriate(bool closeAfterCreate, ISharedPreferences prefs)
{
if (prefs.GetBoolean("kp2a_switch_rooted", false))
{
//switch rooted
bool onlySwitchOnSearch = prefs.GetBoolean(
GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false);
if (closeAfterCreate || (!onlySwitchOnSearch))
{
ActivateKp2aKeyboard();
}
}
else
{
//if the app is about to be closed again (e.g. after searching for a URL and returning to the browser:
// automatically bring up the Keyboard selection dialog
if ((closeAfterCreate) &&
prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key),
Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
{
ActivateKp2aKeyboard();
}
}
}
private bool ClearNotifications()
{
// Notification Manager
_notificationManager = (NotificationManager)GetSystemService(NotificationService);
_notificationManager.Cancel(NotifyPassword);
_notificationManager.Cancel(NotifyUsername);
_notificationManager.Cancel(NotifyKeyboard);
_notificationManager.Cancel(NotifyCombined);
_numElementsToWaitFor = 0;
bool hadKeyboardData = ClearKeyboard(false); //do not broadcast if the keyboard was changed
return hadKeyboardData;
}
bool MakeAccessibleForKeyboard(PwEntryOutput entry, string searchUrl)
{
#if EXCLUDE_KEYBOARD
return false;
#else
bool hasData = false;
Keepass2android.Kbbridge.KeyboardDataBuilder kbdataBuilder = new Keepass2android.Kbbridge.KeyboardDataBuilder();
String[] keys = {PwDefs.UserNameField,
PwDefs.PasswordField,
PwDefs.UrlField,
PwDefs.NotesField,
PwDefs.TitleField
};
int[] resIds = {Resource.String.entry_user_name,
Resource.String.entry_password,
Resource.String.entry_url,
Resource.String.entry_comment,
Resource.String.entry_title };
//add standard fields:
int i=0;
foreach (string key in keys)
{
String value = entry.OutputStrings.ReadSafe(key);
if (value.Length > 0)
{
kbdataBuilder.AddString(key, GetString(resIds[i]), value);
hasData = true;
}
i++;
}
//add additional fields:
foreach (var pair in entry.OutputStrings)
{
var key = pair.Key;
var value = pair.Value.ReadString();
if (!PwDefs.IsStandardField(key)) {
kbdataBuilder.AddString(pair.Key, pair.Key, value);
hasData = true;
}
}
kbdataBuilder.Commit();
Keepass2android.Kbbridge.KeyboardData.EntryName = entry.OutputStrings.ReadSafe(PwDefs.TitleField);
Keepass2android.Kbbridge.KeyboardData.EntryId = entry.Uuid.ToHexString();
if (hasData)
Keepass2android.Autofill.AutoFillService.NotifyNewData(searchUrl);
return hasData;
#endif
}
public void OnWaitElementDeleted(int itemId)
{
_numElementsToWaitFor--;
if (_numElementsToWaitFor <= 0)
{
StopSelf();
}
if ((itemId == NotifyKeyboard) || (itemId == NotifyCombined))
{
//keyboard notification was deleted -> clear entries in keyboard
ClearKeyboard(true);
}
}
bool ClearKeyboard(bool broadcastClear)
{
#if !EXCLUDE_KEYBOARD
Keepass2android.Kbbridge.KeyboardData.AvailableFields.Clear();
Keepass2android.Kbbridge.KeyboardData.EntryName = null;
bool hadData = Keepass2android.Kbbridge.KeyboardData.EntryId != null;
Keepass2android.Kbbridge.KeyboardData.EntryId = null;
if ((hadData) && broadcastClear)
SendBroadcast(new Intent(Intents.KeyboardCleared));
return hadData;
#else
return false;
#endif
}
private readonly Timer _timer = new Timer();
internal void TimeoutCopyToClipboard(String text)
{
Util.CopyToClipboard(this, text);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
String sClipClear = prefs.GetString(GetString(Resource.String.clipboard_timeout_key), GetString(Resource.String.clipboard_timeout_default));
long clipClearTime = long.Parse(sClipClear);
_clearClipboardTask = new ClearClipboardTask(this, text, _uiThreadCallback);
if (clipClearTime > 0)
{
_numElementsToWaitFor++;
_timer.Schedule(_clearClipboardTask, clipClearTime);
}
}
// Task which clears the clipboard, and sends a toast to the foreground.
private class ClearClipboardTask : TimerTask
{
private readonly String _clearText;
private readonly CopyToClipboardService _service;
private readonly Handler _handler;
public ClearClipboardTask(CopyToClipboardService service, String clearText, Handler handler)
{
_clearText = clearText;
_service = service;
_handler = handler;
}
public override void Run()
{
String currentClip = Util.GetClipboard(_service);
_handler.Post(() => _service.OnWaitElementDeleted(ClearClipboard));
if (currentClip.Equals(_clearText))
{
Util.CopyToClipboard(_service, "");
_handler.Post(() =>
{
Toast.MakeText(_service, Resource.String.ClearClipboard, ToastLength.Long).Show();
});
}
}
}
// Setup to allow the toast to happen in the foreground
readonly Handler _uiThreadCallback = new Handler();
private ClearClipboardTask _clearClipboardTask;
private const string _stringtocopy = "StringToCopy";
private class StopOnLockBroadcastReceiver : BroadcastReceiver
{
readonly CopyToClipboardService _service;
public StopOnLockBroadcastReceiver(CopyToClipboardService service)
{
_service = service;
}
public override void OnReceive(Context context, Intent intent)
{
switch (intent.Action)
{
case Intents.DatabaseLocked:
_service.OnLockDatabase();
break;
}
}
}
class NotificationDeletedBroadcastReceiver : BroadcastReceiver
{
readonly CopyToClipboardService _service;
public NotificationDeletedBroadcastReceiver(CopyToClipboardService service)
{
_service = service;
}
#region implemented abstract members of BroadcastReceiver
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action == ActionNotificationCancelled)
{
_service.OnWaitElementDeleted(intent.Extras.GetInt("requestCode"));
}
}
#endregion
}
internal void ActivateKp2aKeyboard()
{
string currentIme = Android.Provider.Settings.Secure.GetString(
ContentResolver,
Android.Provider.Settings.Secure.DefaultInputMethod);
string kp2aIme = PackageName + "/keepass2android.softkeyboard.KP2AKeyboard";
InputMethodManager imeManager = (InputMethodManager)ApplicationContext.GetSystemService(InputMethodService);
if (imeManager == null)
{
Toast.MakeText(this, Resource.String.not_possible_im_picker, ToastLength.Long).Show();
return;
}
if (currentIme == kp2aIme)
{
imeManager.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.None);
}
else
{
IList<InputMethodInfo> inputMethodProperties = imeManager.EnabledInputMethodList;
if (!inputMethodProperties.Any(imi => imi.Id.Equals(kp2aIme)))
{
Toast.MakeText(this, Resource.String.please_activate_keyboard, ToastLength.Long).Show();
Intent settingsIntent = new Intent(Android.Provider.Settings.ActionInputMethodSettings);
settingsIntent.SetFlags(ActivityFlags.NewTask);
StartActivity(settingsIntent);
}
else
{
#if !EXCLUDE_KEYBOARD
Keepass2android.Kbbridge.ImeSwitcher.SwitchToKeyboard(this, kp2aIme, false);
#endif
}
}
}
}
[BroadcastReceiver(Permission = "keepass2android." + AppNames.PackagePart + ".permission.CopyToClipboard")]
[IntentFilter(new[] { Intents.CopyUsername, Intents.CopyPassword, Intents.CheckKeyboard })]
class CopyToClipboardBroadcastReceiver : BroadcastReceiver
{
public CopyToClipboardBroadcastReceiver(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public CopyToClipboardBroadcastReceiver()
{
}
public override void OnReceive(Context context, Intent intent)
{
String action = intent.Action;
//check if we have a last opened entry
//this should always be non-null, but if the OS has killed the app, it might occur.
if (App.Kp2a.GetDb().LastOpenedEntry == null)
{
Intent i = new Intent(context, typeof(AppKilledInfo));
i.SetFlags(ActivityFlags.ClearTask | ActivityFlags.NewTask);
context.StartActivity(i);
return;
}
if (action.Equals(Intents.CopyUsername))
{
String username = App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.ReadSafe(PwDefs.UserNameField);
if (username.Length > 0)
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, username);
}
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
}
else if (action.Equals(Intents.CopyPassword))
{
String password = App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.ReadSafe(PwDefs.PasswordField);
if (password.Length > 0)
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, password);
}
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
}
else if (action.Equals(Intents.CheckKeyboard))
{
CopyToClipboardService.ActivateKeyboard(context);
}
}
};
}