mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-09-01 14:55:19 +00:00
Create app autofill service: a dialog pops up for all password fields & has a button to paste/set a password found in the store with name matching app's
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<application android:allowBackup="true" android:icon="@drawable/ic_launcher"
|
<application android:allowBackup="true" android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name" android:theme="@style/AppTheme">
|
android:label="@string/app_name" android:theme="@style/AppTheme">
|
||||||
<activity android:name=".PasswordStore" android:label="@string/app_name"
|
<activity android:name=".PasswordStore" android:label="@string/app_name"
|
||||||
@@ -42,6 +45,19 @@
|
|||||||
android:value="com.zeapo.pwdstore.PasswordStore" />
|
android:value="com.zeapo.pwdstore.PasswordStore" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name=".AutofillService"
|
||||||
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.accessibilityservice"
|
||||||
|
android:resource="@xml/autofill_config" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity android:name=".AutofillActivity">
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
33
app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java
Normal file
33
app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package com.zeapo.pwdstore;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentSender;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
|
||||||
|
public class AutofillActivity extends AppCompatActivity {
|
||||||
|
public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
|
||||||
|
private boolean bound = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Intent intent = getIntent();
|
||||||
|
PendingIntent pi = intent.getExtras().getParcelable("pending_intent");
|
||||||
|
try {
|
||||||
|
startIntentSenderForResult(pi.getIntentSender()
|
||||||
|
, REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0);
|
||||||
|
} catch (IntentSender.SendIntentException e) {
|
||||||
|
Log.e(AutofillService.Constants.TAG, "SendIntentException", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
finish();
|
||||||
|
AutofillService.getService().decryptAndVerify();
|
||||||
|
}
|
||||||
|
}
|
200
app/src/main/java/com/zeapo/pwdstore/AutofillService.java
Normal file
200
app/src/main/java/com/zeapo/pwdstore/AutofillService.java
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package com.zeapo.pwdstore;
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityService;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.zeapo.pwdstore.utils.PasswordItem;
|
||||||
|
import com.zeapo.pwdstore.utils.PasswordRepository;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.openintents.openpgp.OpenPgpError;
|
||||||
|
import org.openintents.openpgp.util.OpenPgpApi;
|
||||||
|
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class AutofillService extends AccessibilityService {
|
||||||
|
private OpenPgpServiceConnection serviceConnection;
|
||||||
|
private SharedPreferences settings;
|
||||||
|
private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
|
||||||
|
private ArrayList<PasswordItem> items;
|
||||||
|
private static AutofillService service;
|
||||||
|
|
||||||
|
public final class Constants {
|
||||||
|
public static final String TAG = "Keychain";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onServiceConnected() {
|
||||||
|
super.onServiceConnected();
|
||||||
|
serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain");
|
||||||
|
serviceConnection.bindToService();
|
||||||
|
settings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
service = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AutofillService getService() {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||||
|
if (!event.isPassword() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info = event.getSource();
|
||||||
|
PackageManager packageManager = getPackageManager();
|
||||||
|
ApplicationInfo applicationInfo;
|
||||||
|
try {
|
||||||
|
applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
applicationInfo = null;
|
||||||
|
}
|
||||||
|
String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
|
||||||
|
if (appName.equals("OpenKeychain")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items = recursiveFilter(appName, null);
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ArrayList<CharSequence> itemNames = new ArrayList<>();
|
||||||
|
for (PasswordItem item : items) {
|
||||||
|
itemNames.add(item.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Light_Dialog_Alert);
|
||||||
|
builder.setNegativeButton("Cancel", null);
|
||||||
|
builder.setView(R.layout.autofill_layout);
|
||||||
|
final AlertDialog dialog = builder.create();
|
||||||
|
|
||||||
|
dialog.setTitle("Fill with Password Store");
|
||||||
|
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||||
|
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||||
|
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||||
|
dialog.show();
|
||||||
|
((Button) dialog.findViewById(R.id.button)).setText(itemNames.get(0).toString());
|
||||||
|
dialog.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
decryptAndVerify();
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<PasswordItem> recursiveFilter(String filter, File dir) {
|
||||||
|
ArrayList<PasswordItem> items = new ArrayList<>();
|
||||||
|
if (!PasswordRepository.isInitialized()) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
ArrayList<PasswordItem> passwordItems = dir == null ?
|
||||||
|
PasswordRepository.getPasswords() :
|
||||||
|
PasswordRepository.getPasswords(dir);
|
||||||
|
for (PasswordItem item : passwordItems) {
|
||||||
|
if (item.getType() == PasswordItem.TYPE_CATEGORY) {
|
||||||
|
recursiveFilter(filter, item.getFile());
|
||||||
|
}
|
||||||
|
if (item.toString().toLowerCase().contains(filter.toLowerCase())) {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInterrupt() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decryptAndVerify() {
|
||||||
|
Intent data = new Intent();
|
||||||
|
data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||||
|
InputStream is = null;
|
||||||
|
try {
|
||||||
|
is = FileUtils.openInputStream(items.get(0).getFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
|
||||||
|
Intent result = api.executeApi(data, is, os);
|
||||||
|
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||||
|
case OpenPgpApi.RESULT_CODE_SUCCESS: {
|
||||||
|
try {
|
||||||
|
String[] passContent = os.toString("UTF-8").split("\n");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// if the user focused on something else, take focus back
|
||||||
|
// but this will open another dialog...
|
||||||
|
info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
|
||||||
|
passContent[0]);
|
||||||
|
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
|
||||||
|
} else {
|
||||||
|
info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]);
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
|
||||||
|
clip = ClipData.newPlainText("autofill_pm", "MyPasswordIsDaBest!");
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
if (settings.getBoolean("clear_clipboard_20x", false)) {
|
||||||
|
for (int i = 0; i < 19; i++) {
|
||||||
|
clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
|
||||||
|
Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED");
|
||||||
|
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||||
|
// need to start a blank activity to call startIntentSenderForResult
|
||||||
|
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||||
|
intent.putExtra("pending_intent", pi);
|
||||||
|
startActivity(intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OpenPgpApi.RESULT_CODE_ERROR: {
|
||||||
|
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||||
|
Toast.makeText(AutofillService.this,
|
||||||
|
"Error from OpenKeyChain : " + error.getMessage(),
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId());
|
||||||
|
Log.e(Constants.TAG, "onError getMessage:" + error.getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
app/src/main/res/layout/autofill_layout.xml
Normal file
12
app/src/main/res/layout/autofill_layout.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/button"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
</LinearLayout>
|
@@ -154,4 +154,6 @@
|
|||||||
<string name="pwd_generate_button">Generate</string>
|
<string name="pwd_generate_button">Generate</string>
|
||||||
<string name="category_string">"Category: "</string>
|
<string name="category_string">"Category: "</string>
|
||||||
|
|
||||||
|
<!-- Autofill -->
|
||||||
|
<string name="autofill_description">Auto-fills login fields.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
8
app/src/main/res/xml/autofill_config.xml
Normal file
8
app/src/main/res/xml/autofill_config.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:description="@string/autofill_description"
|
||||||
|
android:accessibilityEventTypes="typeViewFocused"
|
||||||
|
android:accessibilityFlags="flagDefault"
|
||||||
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
|
android:notificationTimeout="100"
|
||||||
|
android:canRetrieveWindowContent="true"
|
||||||
|
/>
|
Reference in New Issue
Block a user