From ffffeb9e851b861c782da9f82c7611f541222ebd Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 30 Jan 2018 21:08:30 +0100 Subject: [PATCH] Better autofill data transfer between each activity, New Autofill helper, Add consultation mode --- app/build.gradle | 1 - .../keepassdroid/AutoFillAuthActivity.java | 11 +- .../java/com/keepassdroid/GroupActivity.java | 192 ++++++++++-------- .../com/keepassdroid/PasswordActivity.java | 103 ++-------- .../keepassdroid/autofill/AutofillHelper.java | 173 ++++++++++++++++ .../dataSource/AutofillDataSource.java | 43 ++++ .../SharedPrefsAutofillRepository.java | 144 +++++++++++++ .../fileselect/FileSelectActivity.java | 58 ++++-- .../java/com/kunzisoft/keepass/KeePass.java | 3 +- .../res/layout/autofill_service_list_item.xml | 44 ++++ 10 files changed, 568 insertions(+), 204 deletions(-) create mode 100644 app/src/main/java/com/keepassdroid/autofill/AutofillHelper.java create mode 100644 app/src/main/java/com/keepassdroid/autofill/dataSource/AutofillDataSource.java create mode 100644 app/src/main/java/com/keepassdroid/autofill/dataSource/SharedPrefsAutofillRepository.java create mode 100644 app/src/main/res/layout/autofill_service_list_item.xml diff --git a/app/build.gradle b/app/build.gradle index 745b06d2e..b71eb51d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,7 +68,6 @@ def supportVersion = "26.1.0" def spongycastleVersion = "1.58.0.0" dependencies { - androidTestImplementation files('libs/junit4.jar') implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:design:$supportVersion" implementation "com.android.support:preference-v7:$supportVersion" diff --git a/app/src/main/java/com/keepassdroid/AutoFillAuthActivity.java b/app/src/main/java/com/keepassdroid/AutoFillAuthActivity.java index 673b231fb..0d15a9a64 100644 --- a/app/src/main/java/com/keepassdroid/AutoFillAuthActivity.java +++ b/app/src/main/java/com/keepassdroid/AutoFillAuthActivity.java @@ -24,13 +24,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.os.Build; +import android.os.Bundle; import android.support.annotation.RequiresApi; import com.keepassdroid.fileselect.FileSelectActivity; import com.kunzisoft.keepass.KeePass; -import static com.keepassdroid.PasswordActivity.KEY_AUTOFILL_RESPONSE; - @RequiresApi(api = Build.VERSION_CODES.O) public class AutoFillAuthActivity extends KeePass { @@ -41,8 +40,10 @@ public class AutoFillAuthActivity extends KeePass { } protected void startFileSelectActivity() { - Intent intent = new Intent(this, FileSelectActivity.class); - intent.putExtra(KEY_AUTOFILL_RESPONSE, true); - startActivityForResult(intent, 0); + // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) + Bundle extras = null; + if (getIntent() != null && getIntent().getExtras() != null) + extras = getIntent().getExtras(); + FileSelectActivity.launch(this, extras); } } diff --git a/app/src/main/java/com/keepassdroid/GroupActivity.java b/app/src/main/java/com/keepassdroid/GroupActivity.java index 622356adc..e8dc6354d 100644 --- a/app/src/main/java/com/keepassdroid/GroupActivity.java +++ b/app/src/main/java/com/keepassdroid/GroupActivity.java @@ -23,6 +23,7 @@ import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; @@ -34,9 +35,8 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; -import com.kunzisoft.keepass.KeePass; -import com.kunzisoft.keepass.R; import com.keepassdroid.app.App; +import com.keepassdroid.autofill.AutofillHelper; import com.keepassdroid.database.PwDatabase; import com.keepassdroid.database.PwDatabaseV3; import com.keepassdroid.database.PwDatabaseV4; @@ -50,6 +50,8 @@ import com.keepassdroid.view.ClickView; import com.keepassdroid.view.GroupAddEntryView; import com.keepassdroid.view.GroupRootView; import com.keepassdroid.view.GroupViewOnlyView; +import com.kunzisoft.keepass.KeePass; +import com.kunzisoft.keepass.R; public abstract class GroupActivity extends GroupBaseActivity implements GroupEditFragment.CreateGroupListener, IconPickerFragment.IconPickerListener { @@ -62,37 +64,46 @@ public abstract class GroupActivity extends GroupBaseActivity protected boolean readOnly = false; private static final String TAG = "Group Activity:"; - + public static void Launch(Activity act) { - Launch(act, null); + Launch(act, null, null); + } + + public static void Launch(Activity act, Bundle extras) { + Launch(act, null, extras); + } + + public static void Launch(Activity act, PwGroup group) { + Launch(act, group, null); } - public static void Launch(Activity act, PwGroup group) { - Intent i; + public static void Launch(Activity act, PwGroup group, Bundle extras) { + Intent intent; // Need to use PwDatabase since tree may be null PwDatabase db = App.getDB().pm; if ( db instanceof PwDatabaseV3 ) { - i = new Intent(act, GroupActivityV3.class); + intent = new Intent(act, GroupActivityV3.class); if ( group != null ) { PwGroupV3 g = (PwGroupV3) group; - i.putExtra(KEY_ENTRY, g.groupId); + intent.putExtra(KEY_ENTRY, g.groupId); } } else if ( db instanceof PwDatabaseV4 ) { - i = new Intent(act, GroupActivityV4.class); + intent = new Intent(act, GroupActivityV4.class); if ( group != null ) { PwGroupV4 g = (PwGroupV4) group; - i.putExtra(KEY_ENTRY, g.uuid.toString()); + intent.putExtra(KEY_ENTRY, g.uuid.toString()); } } else { // Reached if db is null Log.d(TAG, "Tried to launch with null db"); return; } - - act.startActivityForResult(i,0); + if (extras != null) + intent.putExtras(extras); + act.startActivityForResult(intent,0); } protected abstract PwGroupId retrieveGroupId(Intent i); @@ -103,93 +114,98 @@ public abstract class GroupActivity extends GroupBaseActivity @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if ( isFinishing() ) { - return; - } - - setResult(KeePass.EXIT_NORMAL); - - Log.w(TAG, "Creating tree view"); - Intent intent = getIntent(); - - PwGroupId id = retrieveGroupId(intent); - - Database db = App.getDB(); - readOnly = db.readOnly; - PwGroup root = db.pm.rootGroup; - if ( id == null ) { - mGroup = root; - } else { - mGroup = db.pm.groups.get(id); - } - - Log.w(TAG, "Retrieved tree"); - if ( mGroup == null ) { - Log.w(TAG, "Group was null"); - return; - } - - isRoot = mGroup == root; - - setupButtons(); + super.onCreate(savedInstanceState); - if ( addGroupEnabled && addEntryEnabled ) { - setContentView(new GroupAddEntryView(this)); - } else if ( addGroupEnabled ) { - setContentView(new GroupRootView(this)); - } else if ( addEntryEnabled ) { - setContentView(new GroupAddEntryView(this)); - View addGroup = findViewById(R.id.add_group); - addGroup.setVisibility(View.GONE); - } else { - setContentView(new GroupViewOnlyView(this)); - } + if (isFinishing()) { + return; + } + + setResult(KeePass.EXIT_NORMAL); + + Log.w(TAG, "Creating tree view"); + Intent intent = getIntent(); + + PwGroupId id = retrieveGroupId(intent); + + Database db = App.getDB(); + + // Force readonly if it's autofill + readOnly = AutofillHelper.isIntentContainsAutofillAuthKey(getIntent()) || db.readOnly; + + PwGroup root = db.pm.rootGroup; + if (id == null) { + mGroup = root; + } else { + mGroup = db.pm.groups.get(id); + } + + Log.w(TAG, "Retrieved tree"); + if (mGroup == null) { + Log.w(TAG, "Group was null"); + return; + } + + isRoot = mGroup == root; + + setupButtons(); + + if (addGroupEnabled && addEntryEnabled) { + setContentView(new GroupAddEntryView(this)); + } else if (addGroupEnabled) { + setContentView(new GroupRootView(this)); + View addEntry = findViewById(R.id.add_entry); + addEntry.setVisibility(View.GONE); + } else if (addEntryEnabled) { + setContentView(new GroupAddEntryView(this)); + View addGroup = findViewById(R.id.add_group); + addGroup.setVisibility(View.GONE); + } else { + setContentView(new GroupViewOnlyView(this)); + } Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); toolbar.setTitle(""); setSupportActionBar(toolbar); - if ( mGroup.getParent() != null ) + if (mGroup.getParent() != null) toolbar.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp); - Log.w(TAG, "Set view"); + Log.w(TAG, "Set view"); - if ( addGroupEnabled ) { - // Add Group button - View addGroup = findViewById(R.id.add_group); - addGroup.setOnClickListener(new View.OnClickListener() { + if (addGroupEnabled) { + // Add Group button + View addGroup = findViewById(R.id.add_group); + addGroup.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - GroupEditFragment groupEditFragment = new GroupEditFragment(); - groupEditFragment.show(getSupportFragmentManager(), TAG_CREATE_GROUP); - } - }); - } - - if ( addEntryEnabled ) { - // Add Entry button - View addEntry = findViewById(R.id.add_entry); - addEntry.setOnClickListener(new View.OnClickListener() { - - public void onClick(View v) { - EntryEditActivity.Launch(GroupActivity.this, mGroup); - } - }); - } - - setGroupTitle(); - setGroupIcon(); + public void onClick(View v) { + GroupEditFragment groupEditFragment = new GroupEditFragment(); + groupEditFragment.show(getSupportFragmentManager(), TAG_CREATE_GROUP); + } + }); + } - setListAdapter(new PwGroupListAdapter(this, mGroup)); - registerForContextMenu(getListView()); - Log.w(TAG, "Finished creating tree"); - - if (isRoot) { - showWarnings(); - } - } + if (addEntryEnabled) { + // Add Entry button + View addEntry = findViewById(R.id.add_entry); + addEntry.setOnClickListener(new View.OnClickListener() { + + public void onClick(View v) { + EntryEditActivity.Launch(GroupActivity.this, mGroup); + } + }); + } + + setGroupTitle(); + setGroupIcon(); + + setListAdapter(new PwGroupListAdapter(this, mGroup)); + registerForContextMenu(getListView()); + Log.w(TAG, "Finished creating tree"); + + if (isRoot) { + showWarnings(); + } + } @Override public boolean onOptionsItemSelected(MenuItem item) { diff --git a/app/src/main/java/com/keepassdroid/PasswordActivity.java b/app/src/main/java/com/keepassdroid/PasswordActivity.java index e9b704148..b11d7afdd 100644 --- a/app/src/main/java/com/keepassdroid/PasswordActivity.java +++ b/app/src/main/java/com/keepassdroid/PasswordActivity.java @@ -20,12 +20,9 @@ package com.keepassdroid; import android.app.Activity; -import android.app.PendingIntent; -import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.content.IntentSender; import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; @@ -38,7 +35,6 @@ import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -77,7 +73,6 @@ public class PasswordActivity extends LockingActivity public static final String KEY_DEFAULT_FILENAME = "defaultFileName"; private static final String KEY_PASSWORD = "password"; - public static final String KEY_AUTOFILL_RESPONSE = "KEY_AUTOFILL_RESPONSE"; private static final String KEY_LAUNCH_IMMEDIATELY = "launchImmediately"; private Uri mDbUri = null; @@ -107,20 +102,24 @@ public class PasswordActivity extends LockingActivity private KeyFileHelper keyFileHelper; - private Intent mReplyIntent; + public static void Launch( + Activity act, + String fileName) throws FileNotFoundException { + Launch(act, fileName, "", null); + } public static void Launch( Activity act, String fileName, - boolean autoFillResponse) throws FileNotFoundException { - Launch(act, fileName, "", autoFillResponse); + Bundle extra) throws FileNotFoundException { + Launch(act, fileName, "", extra); } public static void Launch( Activity act, String fileName, String keyFile, - boolean autoFillResponse) throws FileNotFoundException { + Bundle extras) throws FileNotFoundException { if (EmptyUtils.isNullOrEmpty(fileName)) { throw new FileNotFoundException(); } @@ -137,15 +136,12 @@ public class PasswordActivity extends LockingActivity } Intent i = new Intent(act, PasswordActivity.class); - i.putExtra(KEY_AUTOFILL_RESPONSE, autoFillResponse); i.putExtra(UriIntentInitTask.KEY_FILENAME, fileName); i.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile); + if (extras != null) + i.putExtras(extras); act.startActivityForResult(i, 0); - - if(autoFillResponse) - act.finish(); - } @Override @@ -683,15 +679,7 @@ public class PasswordActivity extends LockingActivity App.clearShutdown(); Handler handler = new Handler(); - AfterLoad afterLoad; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && getIntent().getExtras() != null - && getIntent().getExtras().containsKey(KEY_AUTOFILL_RESPONSE) - && getIntent().getBooleanExtra(KEY_AUTOFILL_RESPONSE, false)) { - afterLoad = new AfterLoadAutofill(handler, db); - } else { - afterLoad = new AfterLoad(handler, db); - } + AfterLoad afterLoad = new AfterLoad(handler, db); LoadDB task = new LoadDB(db, PasswordActivity.this, mDbUri, pass, keyfile, afterLoad); ProgressTask pt = new ProgressTask(PasswordActivity.this, task, R.string.loading_database); @@ -742,36 +730,18 @@ public class PasswordActivity extends LockingActivity public void onClick( DialogInterface dialog, int which) { - GroupActivity.Launch(PasswordActivity.this); + GroupActivity.Launch(PasswordActivity.this, getIntent().getExtras()); } }); } else if (mSuccess) { - GroupActivity.Launch(PasswordActivity.this); + GroupActivity.Launch(PasswordActivity.this, getIntent().getExtras()); } else { displayMessage(PasswordActivity.this); } } } - @RequiresApi(api = Build.VERSION_CODES.O) - private class AfterLoadAutofill extends AfterLoad { - - public AfterLoadAutofill(Handler handler, Database db) { - super(handler, db); - } - - @Override - public void run() { - if (mSuccess) { - onAutofillResponseSuccess(); - } else { - onAutofillResponseFailure(); - } - finish(); - } - } - private static class UriIntentInitTask extends AsyncTask { static final String KEY_FILENAME = "fileName"; @@ -848,51 +818,4 @@ public class PasswordActivity extends LockingActivity } } } - - @RequiresApi(api = Build.VERSION_CODES.O) - public static IntentSender getAuthIntentSenderForResponse(Context context) { - final Intent intent = new Intent(context, AutoFillAuthActivity.class); - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) - .getIntentSender(); - } - - - @Override - public void finish() { - if (mReplyIntent != null) { - setResult(RESULT_OK, mReplyIntent); - } else { - setResult(RESULT_CANCELED); - } - super.finish(); - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private void onAutofillResponseFailure() { - Log.w(getClass().getName(), "Failed Autofill auth."); - mReplyIntent = null; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private void onAutofillResponseSuccess() { - /* - Intent intent = getIntent(); - Bundle clientState = intent.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE); - AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); - StructureParser parser = new StructureParser(getApplicationContext(), structure); - parser.parseForFill(); - AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields(); - */ - mReplyIntent = new Intent(); - /* - HashMap clientFormDataMap = - SharedPrefsAutofillRepository.getInstance().getFilledAutofillFieldCollection - (this, autofillFields.getFocusedHints(), autofillFields.getAllHints()); - - // TODO Add success results - mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, AutofillHelper.newResponse - (this, clientState, false, autofillFields, clientFormDataMap)); - */ - mReplyIntent.putExtra(android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT, ""); - } } diff --git a/app/src/main/java/com/keepassdroid/autofill/AutofillHelper.java b/app/src/main/java/com/keepassdroid/autofill/AutofillHelper.java new file mode 100644 index 000000000..faf4cdcb0 --- /dev/null +++ b/app/src/main/java/com/keepassdroid/autofill/AutofillHelper.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017 Brian Pellin, 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 2 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 . + * + */ +package com.keepassdroid.autofill; + +import android.app.Activity; +import android.app.assist.AssistStructure; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.service.autofill.SaveInfo; +import android.support.annotation.RequiresApi; +import android.util.Log; +import android.view.autofill.AutofillId; +import android.widget.RemoteViews; + +import com.keepassdroid.autofill.dataSource.SharedPrefsAutofillRepository; +import com.keepassdroid.model.FilledAutofillFieldCollection; +import com.kunzisoft.keepass.R; + +import java.util.HashMap; +import java.util.Set; + +public class AutofillHelper { + + private static final int AUTOFILL_RESPONSE_REQUEST_CODE = 81653; + + /** + * Define if EXTRA_AUTHENTICATION_RESULT is an extra bundle key present in the Intent + */ + public static boolean isIntentContainsAutofillAuthKey(Intent intent) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && (intent != null + && intent.getExtras() != null + && intent.getExtras().containsKey(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE)); + } + + /** + * Method to hit when right key is selected + */ + @RequiresApi(api = Build.VERSION_CODES.O) + private static void onAutofillResponse(Activity activity) { + // TODO Connect this method in each item in GroupActivity + Intent mReplyIntent = null; + Intent intent = activity.getIntent(); + if (isIntentContainsAutofillAuthKey(intent)) { + AssistStructure structure = intent.getParcelableExtra( + android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE); + StructureParser parser = new StructureParser(activity, structure); + parser.parseForFill(); + AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields(); + mReplyIntent = new Intent(); + HashMap clientFormDataMap = + SharedPrefsAutofillRepository.getInstance().getFilledAutofillFieldCollection + (activity, autofillFields.getFocusedHints(), autofillFields.getAllHints()); + + Log.d(activity.getClass().getName(), "Successed Autofill auth."); + mReplyIntent.putExtra( + android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT, + AutofillHelper.newResponse + (activity, autofillFields, clientFormDataMap)); + activity.setResult(Activity.RESULT_OK, mReplyIntent); + } else { + Log.w(activity.getClass().getName(), "Failed Autofill auth."); + activity.setResult(Activity.RESULT_CANCELED); + } + } + + /** + * Utility method to loop and close each activity with return data + */ + public static void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + if (data != null) { + activity.setResult(Activity.RESULT_OK, data); + } else { + activity.setResult(Activity.RESULT_CANCELED); + } + } else + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + } + } + + /** + * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the + * client View. + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static Dataset newDataset(Context context, + AutofillFieldMetadataCollection autofillFields, + FilledAutofillFieldCollection filledAutofillFieldCollection) { + String datasetName = filledAutofillFieldCollection.getDatasetName(); + if (datasetName != null) { + Dataset.Builder datasetBuilder; + datasetBuilder = new Dataset.Builder + (newRemoteViews(context.getPackageName(), datasetName)); + boolean setValueAtLeastOnce = + filledAutofillFieldCollection.applyToFields(autofillFields, datasetBuilder); + if (setValueAtLeastOnce) { + return datasetBuilder.build(); + } + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static RemoteViews newRemoteViews(String packageName, String remoteViewsText) { + RemoteViews presentation = + new RemoteViews(packageName, R.layout.autofill_service_list_item); + presentation.setTextViewText(R.id.text, remoteViewsText); + return presentation; + } + + /** + * Wraps autofill data in a Response object (essentially a series of Datasets) which can then + * be sent back to the client View. + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public static FillResponse newResponse(Context context, + AutofillFieldMetadataCollection autofillFields, + HashMap clientFormDataMap) { + FillResponse.Builder responseBuilder = new FillResponse.Builder(); + if (clientFormDataMap != null) { + Set datasetNames = clientFormDataMap.keySet(); + for (String datasetName : datasetNames) { + FilledAutofillFieldCollection filledAutofillFieldCollection = + clientFormDataMap.get(datasetName); + if (filledAutofillFieldCollection != null) { + Dataset dataset = newDataset(context, autofillFields, + filledAutofillFieldCollection); + if (dataset != null) { + responseBuilder.addDataset(dataset); + } + } + } + } + int saveType = autofillFields.getSaveType(); + if (saveType != 0) { + setFullSaveInfo(responseBuilder, saveType, autofillFields); + return responseBuilder.build(); + } else { + Log.d(AutofillHelper.class.getName(), "These fields are not meant to be saved by autofill."); + return null; + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static void setFullSaveInfo(FillResponse.Builder responseBuilder, int saveType, + AutofillFieldMetadataCollection autofillFields) { + AutofillId[] autofillIds = autofillFields.getAutofillIds(); + responseBuilder.setSaveInfo(new SaveInfo.Builder(saveType, autofillIds).build()); + } +} diff --git a/app/src/main/java/com/keepassdroid/autofill/dataSource/AutofillDataSource.java b/app/src/main/java/com/keepassdroid/autofill/dataSource/AutofillDataSource.java new file mode 100644 index 000000000..0207fc53a --- /dev/null +++ b/app/src/main/java/com/keepassdroid/autofill/dataSource/AutofillDataSource.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.keepassdroid.autofill.dataSource; + +import android.content.Context; +import com.keepassdroid.model.FilledAutofillFieldCollection; + +import java.util.HashMap; +import java.util.List; + +public interface AutofillDataSource { + + /** + * Gets saved FilledAutofillFieldCollection that contains some objects that can autofill fields + * with these {@code autofillHints}. + */ + HashMap getFilledAutofillFieldCollection(Context context, + List focusedAutofillHints, List allAutofillHints); + + /** + * Stores a collection of Autofill fields. + */ + void saveFilledAutofillFieldCollection(Context context, + FilledAutofillFieldCollection filledAutofillFieldCollection); + + /** + * Clears all data. + */ + void clear(Context context); +} diff --git a/app/src/main/java/com/keepassdroid/autofill/dataSource/SharedPrefsAutofillRepository.java b/app/src/main/java/com/keepassdroid/autofill/dataSource/SharedPrefsAutofillRepository.java new file mode 100644 index 000000000..22fd3c970 --- /dev/null +++ b/app/src/main/java/com/keepassdroid/autofill/dataSource/SharedPrefsAutofillRepository.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.keepassdroid.autofill.dataSource; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.ArraySet; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.keepassdroid.model.FilledAutofillFieldCollection; + +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +/** + * Singleton autofill data repository that stores autofill fields to SharedPreferences. + *

+ *

Disclaimer: you should not store sensitive fields like user data unencrypted. + * This is done here only for simplicity and learning purposes. + */ +@RequiresApi(api = Build.VERSION_CODES.O) +public class SharedPrefsAutofillRepository implements AutofillDataSource { + private static final String SHARED_PREF_KEY = "com.example.android.autofill" + + ".service.datasource.AutofillDataSource"; + private static final String CLIENT_FORM_DATA_KEY = "loginCredentialDatasets"; + private static final String DATASET_NUMBER_KEY = "datasetNumber"; + private static SharedPrefsAutofillRepository sInstance; + + private SharedPrefsAutofillRepository() { + } + + public static SharedPrefsAutofillRepository getInstance() { + if (sInstance == null) { + sInstance = new SharedPrefsAutofillRepository(); + } + return sInstance; + } + + @Override + public HashMap getFilledAutofillFieldCollection( + Context context, List focusedAutofillHints, List allAutofillHints) { + boolean hasDataForFocusedAutofillHints = false; + HashMap clientFormDataMap = new HashMap<>(); + Set clientFormDataStringSet = getAllAutofillDataStringSet(context); + for (String clientFormDataString : clientFormDataStringSet) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + FilledAutofillFieldCollection filledAutofillFieldCollection = + gson.fromJson(clientFormDataString, FilledAutofillFieldCollection.class); + if (filledAutofillFieldCollection != null) { + if (filledAutofillFieldCollection.helpsWithHints(focusedAutofillHints)) { + // Saved data has data relevant to at least 1 of the hints associated with the + // View in focus. + hasDataForFocusedAutofillHints = true; + } + if (filledAutofillFieldCollection.helpsWithHints(allAutofillHints)) { + // Saved data has data relevant to at least 1 of these hints associated with any + // of the Views in the hierarchy. + clientFormDataMap.put(filledAutofillFieldCollection.getDatasetName(), + filledAutofillFieldCollection); + } + } + } + if (hasDataForFocusedAutofillHints) { + return clientFormDataMap; + } else { + return null; + } + } + + @Override + public void saveFilledAutofillFieldCollection(Context context, + FilledAutofillFieldCollection filledAutofillFieldCollection) { + String datasetName = "dataset-" + getDatasetNumber(context); + filledAutofillFieldCollection.setDatasetName(datasetName); + Set allAutofillData = getAllAutofillDataStringSet(context); + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + allAutofillData.add(gson.toJson(filledAutofillFieldCollection)); + saveAllAutofillDataStringSet(context, allAutofillData); + incrementDatasetNumber(context); + } + + @Override + public void clear(Context context) { + context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + .edit() + .remove(CLIENT_FORM_DATA_KEY) + .remove(DATASET_NUMBER_KEY) + .apply(); + } + + private Set getAllAutofillDataStringSet(Context context) { + return context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + .getStringSet(CLIENT_FORM_DATA_KEY, new ArraySet()); + } + + private void saveAllAutofillDataStringSet(Context context, + Set allAutofillDataStringSet) { + context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + .edit() + .putStringSet(CLIENT_FORM_DATA_KEY, allAutofillDataStringSet) + .apply(); + } + + /** + * For simplicity, datasets will be named in the form "dataset-X" where X means + * this was the Xth dataset saved. + */ + private int getDatasetNumber(Context context) { + return context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + .getInt(DATASET_NUMBER_KEY, 0); + } + + /** + * Every time a dataset is saved, this should be called to increment the dataset number. + * (only important for this service's dataset naming scheme). + */ + private void incrementDatasetNumber(Context context) { + context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE) + .edit() + .putInt(DATASET_NUMBER_KEY, getDatasetNumber(context) + 1) + .apply(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java b/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java index de1e5b575..29e38b330 100644 --- a/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java +++ b/app/src/main/java/com/keepassdroid/fileselect/FileSelectActivity.java @@ -20,12 +20,14 @@ package com.keepassdroid.fileselect; import android.Manifest; +import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; @@ -47,6 +49,7 @@ import com.keepassdroid.GroupActivity; import com.keepassdroid.PasswordActivity; import com.keepassdroid.ProgressTask; import com.keepassdroid.app.App; +import com.keepassdroid.autofill.AutofillHelper; import com.keepassdroid.compat.ContentResolverCompat; import com.keepassdroid.compat.StorageAF; import com.keepassdroid.database.edit.CreateDB; @@ -88,25 +91,34 @@ public class FileSelectActivity extends StylishActivity implements private RecentFileHistory fileHistory; private boolean recentMode = false; - private boolean autofillResponse = false; + private boolean consultationMode = false; private EditText openFileNameView; private AssignPasswordHelper assignPasswordHelper; private Uri databaseUri; + public static void launch(Activity act) { + launch(act, null); + } + + public static void launch(Activity act, Bundle extra) { + Intent intent = new Intent(act, FileSelectActivity.class); + if (extra != null) + intent.putExtras(extra); + act.startActivityForResult(intent, 0); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if(getIntent().getExtras() != null) { - autofillResponse = getIntent().getExtras(). - getBoolean(PasswordActivity.KEY_AUTOFILL_RESPONSE, autofillResponse); - } + if (AutofillHelper.isIntentContainsAutofillAuthKey(getIntent())) + consultationMode = true; - fileHistory = App.getFileHistory(); + fileHistory = App.getFileHistory(); - setContentView(R.layout.file_selection); + setContentView(R.layout.file_selection); fileListTitle = findViewById(R.id.file_list_title); if (fileHistory.hasRecentFiles()) { recentMode = true; @@ -127,7 +139,9 @@ public class FileSelectActivity extends StylishActivity implements String fileName = Util.getEditText(FileSelectActivity.this, R.id.file_filename); try { - PasswordActivity.Launch(FileSelectActivity.this, fileName, autofillResponse); + PasswordActivity.Launch(FileSelectActivity.this, + fileName, + getIntent().getExtras()); } catch (ContentFileNotFoundException e) { Toast.makeText(FileSelectActivity.this, @@ -141,13 +155,17 @@ public class FileSelectActivity extends StylishActivity implements }); // Create button - View createButton = findViewById(R.id.create_database); - createButton.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - CreateFileDialog createFileDialog = new CreateFileDialog(); - createFileDialog.show(getSupportFragmentManager(), "createFileDialog"); - } - }); + View createButton = findViewById(R.id.create_database); + if (!consultationMode) { + createButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + CreateFileDialog createFileDialog = new CreateFileDialog(); + createFileDialog.show(getSupportFragmentManager(), "createFileDialog"); + } + }); + } else { + createButton.setVisibility(View.GONE); + } View browseButton = findViewById(R.id.browse_button); browseButton.setOnClickListener(new View.OnClickListener() { @@ -224,7 +242,9 @@ public class FileSelectActivity extends StylishActivity implements if (db.exists()) { try { - PasswordActivity.Launch(FileSelectActivity.this, path, autofillResponse); + PasswordActivity.Launch(FileSelectActivity.this, + path, + getIntent().getExtras()); } catch (Exception e) { // Ignore exception } @@ -232,7 +252,9 @@ public class FileSelectActivity extends StylishActivity implements } else { try { - PasswordActivity.Launch(FileSelectActivity.this, dbUri.toString(), autofillResponse); + PasswordActivity.Launch(FileSelectActivity.this, + dbUri.toString(), + getIntent().getExtras()); } catch (Exception e) { // Ignore exception } @@ -402,7 +424,7 @@ public class FileSelectActivity extends StylishActivity implements public void afterOpenFile(String fileName, String keyFile) { try { PasswordActivity.Launch(FileSelectActivity.this, - fileName, keyFile, autofillResponse); + fileName, keyFile, getIntent().getExtras()); } catch (ContentFileNotFoundException e) { Toast.makeText(FileSelectActivity.this, R.string.file_not_found_content, Toast.LENGTH_LONG) diff --git a/app/src/main/java/com/kunzisoft/keepass/KeePass.java b/app/src/main/java/com/kunzisoft/keepass/KeePass.java index 9f4bd4bfb..92ee0addb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/KeePass.java +++ b/app/src/main/java/com/kunzisoft/keepass/KeePass.java @@ -44,8 +44,7 @@ public class KeePass extends Activity { } protected void startFileSelectActivity() { - Intent intent = new Intent(this, FileSelectActivity.class); - startActivityForResult(intent, 0); + FileSelectActivity.launch(this); } @Override diff --git a/app/src/main/res/layout/autofill_service_list_item.xml b/app/src/main/res/layout/autofill_service_list_item.xml new file mode 100644 index 000000000..2781163cd --- /dev/null +++ b/app/src/main/res/layout/autofill_service_list_item.xml @@ -0,0 +1,44 @@ + + + + + + + \ No newline at end of file