mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-09-02 15:25:39 +00:00
Convert autofill package to Kotlin (#515)
Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
@@ -1,101 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.autofill;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentSender;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import com.zeapo.pwdstore.PasswordStore;
|
|
||||||
import org.eclipse.jgit.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
// blank activity started by service for calling startIntentSenderForResult
|
|
||||||
public class AutofillActivity extends AppCompatActivity {
|
|
||||||
public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
|
|
||||||
public static final int REQUEST_CODE_PICK = 777;
|
|
||||||
public static final int REQUEST_CODE_PICK_MATCH_WITH = 778;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
|
|
||||||
if (extras != null && extras.containsKey("pending_intent")) {
|
|
||||||
try {
|
|
||||||
PendingIntent pi = extras.getParcelable("pending_intent");
|
|
||||||
if (pi == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
startIntentSenderForResult(pi.getIntentSender()
|
|
||||||
, REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0);
|
|
||||||
} catch (IntentSender.SendIntentException e) {
|
|
||||||
Log.e(AutofillService.Constants.TAG, "SendIntentException", e);
|
|
||||||
}
|
|
||||||
} else if (extras != null && extras.containsKey("pick")) {
|
|
||||||
Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
|
|
||||||
intent.putExtra("matchWith", true);
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_PICK);
|
|
||||||
} else if (extras != null && extras.containsKey("pickMatchWith")) {
|
|
||||||
Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
|
|
||||||
intent.putExtra("matchWith", true);
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_PICK_MATCH_WITH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
finish(); // go back to the password field app
|
|
||||||
switch (requestCode) {
|
|
||||||
case REQUEST_CODE_DECRYPT_AND_VERIFY:
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
AutofillService.getInstance().setResultData(data); // report the result to service
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case REQUEST_CODE_PICK:
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case REQUEST_CODE_PICK_MATCH_WITH:
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
// need to not only decrypt the picked password, but also
|
|
||||||
// update the "match with" preference
|
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
String packageName = extras.getString("packageName");
|
|
||||||
boolean isWeb = extras.getBoolean("isWeb");
|
|
||||||
|
|
||||||
String path = data.getStringExtra("path");
|
|
||||||
AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
|
|
||||||
|
|
||||||
SharedPreferences prefs;
|
|
||||||
if (!isWeb) {
|
|
||||||
prefs = getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
|
||||||
} else {
|
|
||||||
prefs = getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
String preference = prefs.getString(packageName, "");
|
|
||||||
switch (preference) {
|
|
||||||
case "":
|
|
||||||
case "/first":
|
|
||||||
case "/never":
|
|
||||||
editor.putString(packageName, path);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
List<String> matches = new ArrayList<>(Arrays.asList(preference.trim().split("\n")));
|
|
||||||
matches.add(path);
|
|
||||||
String paths = StringUtils.join(matches, "\n");
|
|
||||||
editor.putString(packageName, paths);
|
|
||||||
}
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,88 @@
|
|||||||
|
package com.zeapo.pwdstore.autofill
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.zeapo.pwdstore.PasswordStore
|
||||||
|
import com.zeapo.pwdstore.utils.splitLines
|
||||||
|
import org.eclipse.jgit.util.StringUtils
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
// blank activity started by service for calling startIntentSenderForResult
|
||||||
|
class AutofillActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val extras = intent.extras
|
||||||
|
|
||||||
|
if (extras != null && extras.containsKey("pending_intent")) {
|
||||||
|
try {
|
||||||
|
val pi = extras.getParcelable<PendingIntent>("pending_intent") ?: return
|
||||||
|
startIntentSenderForResult(pi.intentSender, REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0)
|
||||||
|
} catch (e: IntentSender.SendIntentException) {
|
||||||
|
Log.e(AutofillService.Constants.TAG, "SendIntentException", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (extras != null && extras.containsKey("pick")) {
|
||||||
|
val intent = Intent(applicationContext, PasswordStore::class.java)
|
||||||
|
intent.putExtra("matchWith", true)
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_PICK)
|
||||||
|
} else if (extras != null && extras.containsKey("pickMatchWith")) {
|
||||||
|
val intent = Intent(applicationContext, PasswordStore::class.java)
|
||||||
|
intent.putExtra("matchWith", true)
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_PICK_MATCH_WITH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
finish() // go back to the password field app
|
||||||
|
when (requestCode) {
|
||||||
|
REQUEST_CODE_DECRYPT_AND_VERIFY -> if (resultCode == RESULT_OK) {
|
||||||
|
AutofillService.instance?.setResultData(data!!) // report the result to service
|
||||||
|
}
|
||||||
|
REQUEST_CODE_PICK -> if (resultCode == RESULT_OK) {
|
||||||
|
AutofillService.instance?.setPickedPassword(data!!.getStringExtra("path"))
|
||||||
|
}
|
||||||
|
REQUEST_CODE_PICK_MATCH_WITH -> if (resultCode == RESULT_OK) {
|
||||||
|
// need to not only decrypt the picked password, but also
|
||||||
|
// update the "match with" preference
|
||||||
|
val extras = intent.extras ?: return
|
||||||
|
val packageName = extras.getString("packageName")
|
||||||
|
val isWeb = extras.getBoolean("isWeb")
|
||||||
|
|
||||||
|
val path = data!!.getStringExtra("path")
|
||||||
|
AutofillService.instance?.setPickedPassword(data.getStringExtra("path"))
|
||||||
|
|
||||||
|
val prefs: SharedPreferences
|
||||||
|
prefs = if (!isWeb) {
|
||||||
|
applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
|
||||||
|
} else {
|
||||||
|
applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
val editor = prefs.edit()
|
||||||
|
when (val preference = prefs.getString(packageName, "")) {
|
||||||
|
"", "/first", "/never" -> editor.putString(packageName, path)
|
||||||
|
else -> {
|
||||||
|
val matches = ArrayList(Arrays.asList(*preference!!.trim { it <= ' ' }.splitLines()))
|
||||||
|
matches.add(path)
|
||||||
|
val paths = StringUtils.join(matches, "\n")
|
||||||
|
editor.putString(packageName, paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913
|
||||||
|
const val REQUEST_CODE_PICK = 777
|
||||||
|
const val REQUEST_CODE_PICK_MATCH_WITH = 778
|
||||||
|
}
|
||||||
|
}
|
@@ -1,235 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.autofill;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import com.zeapo.pwdstore.PasswordStore;
|
|
||||||
import com.zeapo.pwdstore.R;
|
|
||||||
|
|
||||||
public class AutofillFragment extends DialogFragment {
|
|
||||||
private static final int MATCH_WITH = 777;
|
|
||||||
private ArrayAdapter<String> adapter;
|
|
||||||
private boolean isWeb;
|
|
||||||
|
|
||||||
public AutofillFragment() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
|
||||||
// this fragment is only created from the settings page (AutofillPreferenceActivity)
|
|
||||||
// need to interact with the recyclerAdapter which is a member of activity
|
|
||||||
final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
|
|
||||||
LayoutInflater inflater = callingActivity.getLayoutInflater();
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams") final View view = inflater.inflate(R.layout.fragment_autofill, null);
|
|
||||||
|
|
||||||
builder.setView(view);
|
|
||||||
|
|
||||||
final String packageName = getArguments().getString("packageName");
|
|
||||||
final String appName = getArguments().getString("appName");
|
|
||||||
isWeb = getArguments().getBoolean("isWeb");
|
|
||||||
|
|
||||||
// set the dialog icon and title or webURL editText
|
|
||||||
String iconPackageName;
|
|
||||||
if (!isWeb) {
|
|
||||||
iconPackageName = packageName;
|
|
||||||
builder.setTitle(appName);
|
|
||||||
view.findViewById(R.id.webURL).setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
iconPackageName = "com.android.browser";
|
|
||||||
builder.setTitle("Website");
|
|
||||||
((EditText) view.findViewById(R.id.webURL)).setText(packageName);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(iconPackageName));
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up the listview now for items added by button/from preferences
|
|
||||||
adapter = new ArrayAdapter<String>(getActivity().getApplicationContext()
|
|
||||||
, android.R.layout.simple_list_item_1, android.R.id.text1) {
|
|
||||||
// set text color to black because default is white...
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
|
||||||
TextView textView = (TextView) super.getView(position, convertView, parent);
|
|
||||||
textView.setTextColor(ContextCompat.getColor(getContext(), R.color.grey_black_1000));
|
|
||||||
return textView;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
((ListView) view.findViewById(R.id.matched)).setAdapter(adapter);
|
|
||||||
// delete items by clicking them
|
|
||||||
((ListView) view.findViewById(R.id.matched)).setOnItemClickListener(
|
|
||||||
(parent, view1, position, id) -> adapter.remove(adapter.getItem(position)));
|
|
||||||
|
|
||||||
// set the existing preference, if any
|
|
||||||
SharedPreferences prefs;
|
|
||||||
if (!isWeb) {
|
|
||||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
|
||||||
} else {
|
|
||||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
String preference = prefs.getString(packageName, "");
|
|
||||||
switch (preference) {
|
|
||||||
case "":
|
|
||||||
((RadioButton) view.findViewById(R.id.use_default)).toggle();
|
|
||||||
break;
|
|
||||||
case "/first":
|
|
||||||
((RadioButton) view.findViewById(R.id.first)).toggle();
|
|
||||||
break;
|
|
||||||
case "/never":
|
|
||||||
((RadioButton) view.findViewById(R.id.never)).toggle();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
((RadioButton) view.findViewById(R.id.match)).toggle();
|
|
||||||
// trim to remove the last blank element
|
|
||||||
adapter.addAll(preference.trim().split("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add items with the + button
|
|
||||||
View.OnClickListener matchPassword = v -> {
|
|
||||||
((RadioButton) view.findViewById(R.id.match)).toggle();
|
|
||||||
Intent intent = new Intent(getActivity(), PasswordStore.class);
|
|
||||||
intent.putExtra("matchWith", true);
|
|
||||||
startActivityForResult(intent, MATCH_WITH);
|
|
||||||
};
|
|
||||||
view.findViewById(R.id.matchButton).setOnClickListener(matchPassword);
|
|
||||||
|
|
||||||
// write to preferences when OK clicked
|
|
||||||
builder.setPositiveButton(R.string.dialog_ok, (dialog, which) -> {
|
|
||||||
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, null);
|
|
||||||
final SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
if (isWeb) {
|
|
||||||
builder.setNeutralButton(R.string.autofill_apps_delete, (dialog, which) -> {
|
|
||||||
if (callingActivity.recyclerAdapter != null
|
|
||||||
&& packageName != null && !packageName.equals("")) {
|
|
||||||
editor.remove(packageName);
|
|
||||||
callingActivity.recyclerAdapter.removeWebsite(packageName);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to the onClick here for buttons to dismiss dialog only when wanted
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
AlertDialog ad = (AlertDialog) getDialog();
|
|
||||||
if (ad != null) {
|
|
||||||
Button positiveButton = ad.getButton(Dialog.BUTTON_POSITIVE);
|
|
||||||
positiveButton.setOnClickListener(v -> {
|
|
||||||
AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
|
|
||||||
Dialog dialog = getDialog();
|
|
||||||
|
|
||||||
SharedPreferences prefs;
|
|
||||||
if (!isWeb) {
|
|
||||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
|
||||||
} else {
|
|
||||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
|
|
||||||
String packageName = getArguments().getString("packageName", "");
|
|
||||||
if (isWeb) {
|
|
||||||
// handle some errors and don't dismiss the dialog
|
|
||||||
EditText webURL = dialog.findViewById(R.id.webURL);
|
|
||||||
|
|
||||||
packageName = webURL.getText().toString();
|
|
||||||
|
|
||||||
if (packageName.equals("")) {
|
|
||||||
webURL.setError("URL cannot be blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String oldPackageName = getArguments().getString("packageName", "");
|
|
||||||
if (!oldPackageName.equals(packageName) && prefs.getAll().containsKey(packageName)) {
|
|
||||||
webURL.setError("URL already exists");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write to preferences accordingly
|
|
||||||
RadioGroup radioGroup = dialog.findViewById(R.id.autofill_radiogroup);
|
|
||||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
|
||||||
case R.id.use_default:
|
|
||||||
if (!isWeb) {
|
|
||||||
editor.remove(packageName);
|
|
||||||
} else {
|
|
||||||
editor.putString(packageName, "");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case R.id.first:
|
|
||||||
editor.putString(packageName, "/first");
|
|
||||||
break;
|
|
||||||
case R.id.never:
|
|
||||||
editor.putString(packageName, "/never");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
StringBuilder paths = new StringBuilder();
|
|
||||||
for (int i = 0; i < adapter.getCount(); i++) {
|
|
||||||
paths.append(adapter.getItem(i));
|
|
||||||
if (i != adapter.getCount()) {
|
|
||||||
paths.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editor.putString(packageName, paths.toString());
|
|
||||||
}
|
|
||||||
editor.apply();
|
|
||||||
|
|
||||||
// notify the recycler adapter if it is loaded
|
|
||||||
if (callingActivity.recyclerAdapter != null) {
|
|
||||||
int position;
|
|
||||||
if (!isWeb) {
|
|
||||||
String appName = getArguments().getString("appName", "");
|
|
||||||
position = callingActivity.recyclerAdapter.getPosition(appName);
|
|
||||||
callingActivity.recyclerAdapter.notifyItemChanged(position);
|
|
||||||
} else {
|
|
||||||
position = callingActivity.recyclerAdapter.getPosition(packageName);
|
|
||||||
String oldPackageName = getArguments().getString("packageName", "");
|
|
||||||
if (oldPackageName.equals(packageName)) {
|
|
||||||
callingActivity.recyclerAdapter.notifyItemChanged(position);
|
|
||||||
} else if (oldPackageName.equals("")) {
|
|
||||||
callingActivity.recyclerAdapter.addWebsite(packageName);
|
|
||||||
} else {
|
|
||||||
editor.remove(oldPackageName);
|
|
||||||
callingActivity.recyclerAdapter.updateWebsite(oldPackageName, packageName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
adapter.add(data.getStringExtra("path"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,211 @@
|
|||||||
|
package com.zeapo.pwdstore.autofill
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import android.widget.RadioGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.zeapo.pwdstore.PasswordStore
|
||||||
|
import com.zeapo.pwdstore.R
|
||||||
|
import com.zeapo.pwdstore.utils.splitLines
|
||||||
|
|
||||||
|
class AutofillFragment : DialogFragment() {
|
||||||
|
private var adapter: ArrayAdapter<String>? = null
|
||||||
|
private var isWeb: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
// this fragment is only created from the settings page (AutofillPreferenceActivity)
|
||||||
|
// need to interact with the recyclerAdapter which is a member of activity
|
||||||
|
val callingActivity = requireActivity() as AutofillPreferenceActivity
|
||||||
|
val inflater = callingActivity.layoutInflater
|
||||||
|
val args = requireNotNull(arguments)
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams") val view = inflater.inflate(R.layout.fragment_autofill, null)
|
||||||
|
|
||||||
|
builder.setView(view)
|
||||||
|
|
||||||
|
val packageName = args.getString("packageName")
|
||||||
|
val appName = args.getString("appName")
|
||||||
|
isWeb = args.getBoolean("isWeb")
|
||||||
|
|
||||||
|
// set the dialog icon and title or webURL editText
|
||||||
|
val iconPackageName: String?
|
||||||
|
if (!isWeb) {
|
||||||
|
iconPackageName = packageName
|
||||||
|
builder.setTitle(appName)
|
||||||
|
view.findViewById<View>(R.id.webURL).visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
iconPackageName = "com.android.browser"
|
||||||
|
builder.setTitle("Website")
|
||||||
|
(view.findViewById<View>(R.id.webURL) as EditText).setText(packageName)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
builder.setIcon(callingActivity.packageManager.getApplicationIcon(iconPackageName))
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up the listview now for items added by button/from preferences
|
||||||
|
adapter = object : ArrayAdapter<String>(requireContext(), android.R.layout.simple_list_item_1, android.R.id.text1) {
|
||||||
|
// set text color to black because default is white...
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val textView = super.getView(position, convertView, parent) as TextView
|
||||||
|
textView.setTextColor(ContextCompat.getColor(context, R.color.grey_black_1000))
|
||||||
|
return textView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(view.findViewById<View>(R.id.matched) as ListView).adapter = adapter
|
||||||
|
// delete items by clicking them
|
||||||
|
(view.findViewById<View>(R.id.matched) as ListView).setOnItemClickListener { _, _, position, _ -> adapter!!.remove(adapter!!.getItem(position)) }
|
||||||
|
|
||||||
|
// set the existing preference, if any
|
||||||
|
val prefs: SharedPreferences = if (!isWeb) {
|
||||||
|
callingActivity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
|
||||||
|
} else {
|
||||||
|
callingActivity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
when (val preference = prefs.getString(packageName, "")) {
|
||||||
|
"" -> (view.findViewById<View>(R.id.use_default) as RadioButton).toggle()
|
||||||
|
"/first" -> (view.findViewById<View>(R.id.first) as RadioButton).toggle()
|
||||||
|
"/never" -> (view.findViewById<View>(R.id.never) as RadioButton).toggle()
|
||||||
|
else -> {
|
||||||
|
(view.findViewById<View>(R.id.match) as RadioButton).toggle()
|
||||||
|
// trim to remove the last blank element
|
||||||
|
adapter!!.addAll(*preference!!.trim { it <= ' ' }.splitLines())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add items with the + button
|
||||||
|
val matchPassword = { _: View ->
|
||||||
|
(view.findViewById<View>(R.id.match) as RadioButton).toggle()
|
||||||
|
val intent = Intent(activity, PasswordStore::class.java)
|
||||||
|
intent.putExtra("matchWith", true)
|
||||||
|
startActivityForResult(intent, MATCH_WITH)
|
||||||
|
}
|
||||||
|
view.findViewById<View>(R.id.matchButton).setOnClickListener(matchPassword)
|
||||||
|
|
||||||
|
// write to preferences when OK clicked
|
||||||
|
builder.setPositiveButton(R.string.dialog_ok) { _, _ -> }
|
||||||
|
builder.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
val editor = prefs.edit()
|
||||||
|
if (isWeb) {
|
||||||
|
builder.setNeutralButton(R.string.autofill_apps_delete) { _, _ ->
|
||||||
|
if (callingActivity.recyclerAdapter != null
|
||||||
|
&& packageName != null && packageName != "") {
|
||||||
|
editor.remove(packageName)
|
||||||
|
callingActivity.recyclerAdapter?.removeWebsite(packageName)
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to the onClick here for buttons to dismiss dialog only when wanted
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
val ad = dialog as? AlertDialog
|
||||||
|
if (ad != null) {
|
||||||
|
val positiveButton = ad.getButton(Dialog.BUTTON_POSITIVE)
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
val callingActivity = requireActivity() as AutofillPreferenceActivity
|
||||||
|
val dialog = dialog
|
||||||
|
val args = requireNotNull(arguments)
|
||||||
|
|
||||||
|
val prefs: SharedPreferences = if (!isWeb) {
|
||||||
|
callingActivity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
|
||||||
|
} else {
|
||||||
|
callingActivity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
val editor = prefs.edit()
|
||||||
|
|
||||||
|
var packageName = args.getString("packageName", "")
|
||||||
|
if (isWeb) {
|
||||||
|
// handle some errors and don't dismiss the dialog
|
||||||
|
val webURL = dialog.findViewById<EditText>(R.id.webURL)
|
||||||
|
|
||||||
|
packageName = webURL.text.toString()
|
||||||
|
|
||||||
|
if (packageName == "") {
|
||||||
|
webURL.error = "URL cannot be blank"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
val oldPackageName = args.getString("packageName", "")
|
||||||
|
if (oldPackageName != packageName && prefs.all.containsKey(packageName)) {
|
||||||
|
webURL.error = "URL already exists"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write to preferences accordingly
|
||||||
|
val radioGroup = dialog.findViewById<RadioGroup>(R.id.autofill_radiogroup)
|
||||||
|
when (radioGroup.checkedRadioButtonId) {
|
||||||
|
R.id.use_default -> if (!isWeb) {
|
||||||
|
editor.remove(packageName)
|
||||||
|
} else {
|
||||||
|
editor.putString(packageName, "")
|
||||||
|
}
|
||||||
|
R.id.first -> editor.putString(packageName, "/first")
|
||||||
|
R.id.never -> editor.putString(packageName, "/never")
|
||||||
|
else -> {
|
||||||
|
val paths = StringBuilder()
|
||||||
|
for (i in 0 until adapter!!.count) {
|
||||||
|
paths.append(adapter!!.getItem(i))
|
||||||
|
if (i != adapter!!.count) {
|
||||||
|
paths.append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.putString(packageName, paths.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.apply()
|
||||||
|
|
||||||
|
// notify the recycler adapter if it is loaded
|
||||||
|
callingActivity.recyclerAdapter?.apply {
|
||||||
|
val position: Int
|
||||||
|
if (!isWeb) {
|
||||||
|
val appName = args.getString("appName", "")
|
||||||
|
position = getPosition(appName)
|
||||||
|
notifyItemChanged(position)
|
||||||
|
} else {
|
||||||
|
position = getPosition(packageName)
|
||||||
|
when (val oldPackageName = args.getString("packageName", "")) {
|
||||||
|
packageName -> notifyItemChanged(position)
|
||||||
|
"" -> addWebsite(packageName)
|
||||||
|
else -> {
|
||||||
|
editor.remove(oldPackageName)
|
||||||
|
updateWebsite(oldPackageName, packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||||
|
if (resultCode == AppCompatActivity.RESULT_OK) {
|
||||||
|
adapter!!.add(data.getStringExtra("path"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MATCH_WITH = 777
|
||||||
|
}
|
||||||
|
}
|
@@ -1,165 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.autofill;
|
|
||||||
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.SearchView;
|
|
||||||
import androidx.core.app.NavUtils;
|
|
||||||
import androidx.core.app.TaskStackBuilder;
|
|
||||||
import androidx.core.view.MenuItemCompat;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.zeapo.pwdstore.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class AutofillPreferenceActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
AutofillRecyclerAdapter recyclerAdapter; // let fragment have access
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private PackageManager pm;
|
|
||||||
|
|
||||||
private boolean recreate; // flag for action on up press; origin autofill dialog? different act
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.autofill_recycler_view);
|
|
||||||
recyclerView = findViewById(R.id.autofill_recycler);
|
|
||||||
|
|
||||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
|
||||||
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
|
|
||||||
|
|
||||||
pm = getPackageManager();
|
|
||||||
|
|
||||||
new populateTask().execute();
|
|
||||||
|
|
||||||
// if the preference activity was started from the autofill dialog
|
|
||||||
recreate = false;
|
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
if (extras != null) {
|
|
||||||
recreate = true;
|
|
||||||
|
|
||||||
showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"));
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitle("Autofill Apps");
|
|
||||||
|
|
||||||
final FloatingActionButton fab = findViewById(R.id.fab);
|
|
||||||
fab.setOnClickListener(v -> showDialog("", "", true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
|
||||||
getMenuInflater().inflate(R.menu.autofill_preference, menu);
|
|
||||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
|
||||||
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
|
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String s) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String s) {
|
|
||||||
if (recyclerAdapter != null) {
|
|
||||||
recyclerAdapter.filter(s);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
// in service, we CLEAR_TASK. then we set the recreate flag.
|
|
||||||
// something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable
|
|
||||||
case android.R.id.home:
|
|
||||||
Intent upIntent = NavUtils.getParentActivityIntent(this);
|
|
||||||
if (recreate) {
|
|
||||||
TaskStackBuilder.create(this)
|
|
||||||
.addNextIntentWithParentStack(upIntent)
|
|
||||||
.startActivities();
|
|
||||||
} else {
|
|
||||||
NavUtils.navigateUpTo(this, upIntent);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showDialog(String packageName, String appName, boolean isWeb) {
|
|
||||||
DialogFragment df = new AutofillFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString("packageName", packageName);
|
|
||||||
args.putString("appName", appName);
|
|
||||||
args.putBoolean("isWeb", isWeb);
|
|
||||||
df.setArguments(args);
|
|
||||||
df.show(getFragmentManager(), "autofill_dialog");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class populateTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
runOnUiThread(() -> findViewById(R.id.progress_bar).setVisibility(View.VISIBLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
||||||
List<ResolveInfo> allAppsResolveInfo = pm.queryIntentActivities(intent, 0);
|
|
||||||
List<AutofillRecyclerAdapter.AppInfo> allApps = new ArrayList<>();
|
|
||||||
|
|
||||||
for (ResolveInfo app : allAppsResolveInfo) {
|
|
||||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(app.activityInfo.packageName
|
|
||||||
, app.loadLabel(pm).toString(), false, app.loadIcon(pm)));
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedPreferences prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
Map<String, ?> prefsMap = prefs.getAll();
|
|
||||||
for (String key : prefsMap.keySet()) {
|
|
||||||
try {
|
|
||||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, pm.getApplicationIcon("com.android.browser")));
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recyclerAdapter = new AutofillRecyclerAdapter(allApps, pm, AutofillPreferenceActivity.this);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid) {
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
findViewById(R.id.progress_bar).setVisibility(View.GONE);
|
|
||||||
recyclerView.setAdapter(recyclerAdapter);
|
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
if (extras != null) {
|
|
||||||
recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("appName")));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,159 @@
|
|||||||
|
package com.zeapo.pwdstore.autofill
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.app.NavUtils
|
||||||
|
import androidx.core.app.TaskStackBuilder
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import com.zeapo.pwdstore.R
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
class AutofillPreferenceActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
internal var recyclerAdapter: AutofillRecyclerAdapter? = null // let fragment have access
|
||||||
|
private var recyclerView: RecyclerView? = null
|
||||||
|
private var pm: PackageManager? = null
|
||||||
|
|
||||||
|
private var recreate: Boolean = false // flag for action on up press; origin autofill dialog? different act
|
||||||
|
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.autofill_recycler_view)
|
||||||
|
recyclerView = findViewById(R.id.autofill_recycler)
|
||||||
|
|
||||||
|
val layoutManager = LinearLayoutManager(this)
|
||||||
|
recyclerView!!.layoutManager = layoutManager
|
||||||
|
recyclerView!!.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
|
pm = packageManager
|
||||||
|
|
||||||
|
PopulateTask(this).execute()
|
||||||
|
|
||||||
|
// if the preference activity was started from the autofill dialog
|
||||||
|
recreate = false
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
recreate = true
|
||||||
|
|
||||||
|
showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"))
|
||||||
|
}
|
||||||
|
|
||||||
|
title = "Autofill Apps"
|
||||||
|
|
||||||
|
val fab = findViewById<FloatingActionButton>(R.id.fab)
|
||||||
|
fab.setOnClickListener { showDialog("", "", true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
|
menuInflater.inflate(R.menu.autofill_preference, menu)
|
||||||
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(s: String): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(s: String): Boolean {
|
||||||
|
if (recyclerAdapter != null) {
|
||||||
|
recyclerAdapter!!.filter(s)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
// in service, we CLEAR_TASK. then we set the recreate flag.
|
||||||
|
// something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable
|
||||||
|
if (item.itemId == android.R.id.home) {
|
||||||
|
val upIntent = NavUtils.getParentActivityIntent(this)
|
||||||
|
if (recreate) {
|
||||||
|
TaskStackBuilder.create(this)
|
||||||
|
.addNextIntentWithParentStack(upIntent!!)
|
||||||
|
.startActivities()
|
||||||
|
} else {
|
||||||
|
NavUtils.navigateUpTo(this, upIntent!!)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDialog(packageName: String?, appName: String?, isWeb: Boolean) {
|
||||||
|
val df = AutofillFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString("packageName", packageName)
|
||||||
|
args.putString("appName", appName)
|
||||||
|
args.putBoolean("isWeb", isWeb)
|
||||||
|
df.arguments = args
|
||||||
|
df.show(supportFragmentManager, "autofill_dialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private class PopulateTask(activity: AutofillPreferenceActivity) : AsyncTask<Void, Void, Void>() {
|
||||||
|
|
||||||
|
val weakReference = WeakReference<AutofillPreferenceActivity>(activity)
|
||||||
|
|
||||||
|
override fun onPreExecute() {
|
||||||
|
weakReference.get()?.apply {
|
||||||
|
runOnUiThread { findViewById<View>(R.id.progress_bar).visibility = View.VISIBLE }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doInBackground(vararg params: Void): Void? {
|
||||||
|
val pm = weakReference.get()?.pm ?: return null
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN)
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
val allAppsResolveInfo = pm.queryIntentActivities(intent, 0)
|
||||||
|
val allApps = ArrayList<AutofillRecyclerAdapter.AppInfo>()
|
||||||
|
|
||||||
|
for (app in allAppsResolveInfo) {
|
||||||
|
allApps.add(AutofillRecyclerAdapter.AppInfo(app.activityInfo.packageName, app.loadLabel(pm).toString(), false, app.loadIcon(pm)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val prefs = weakReference.get()?.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
val prefsMap = prefs!!.all
|
||||||
|
for (key in prefsMap.keys) {
|
||||||
|
try {
|
||||||
|
allApps.add(AutofillRecyclerAdapter.AppInfo(key, key, true, pm.getApplicationIcon("com.android.browser")))
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
allApps.add(AutofillRecyclerAdapter.AppInfo(key, key, true, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
weakReference.get()?.recyclerAdapter = AutofillRecyclerAdapter(allApps, weakReference.get()!!)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(ignored: Void?) {
|
||||||
|
weakReference.get()?.apply {
|
||||||
|
runOnUiThread {
|
||||||
|
findViewById<View>(R.id.progress_bar).visibility = View.GONE
|
||||||
|
recyclerView!!.adapter = recyclerAdapter
|
||||||
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
recyclerView!!.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,192 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.autofill;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.SortedList;
|
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback;
|
|
||||||
import com.zeapo.pwdstore.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> {
|
|
||||||
|
|
||||||
private SortedList<AppInfo> apps;
|
|
||||||
private ArrayList<AppInfo> allApps; // for filtering, maintain a list of all
|
|
||||||
private AutofillPreferenceActivity activity;
|
|
||||||
private Drawable browserIcon = null;
|
|
||||||
|
|
||||||
AutofillRecyclerAdapter(List<AppInfo> allApps, final PackageManager pm
|
|
||||||
, AutofillPreferenceActivity activity) {
|
|
||||||
SortedList.Callback<AppInfo> callback = new SortedListAdapterCallback<AppInfo>(this) {
|
|
||||||
// don't take into account secondary text. This is good enough
|
|
||||||
// for the limited add/remove usage for websites
|
|
||||||
@Override
|
|
||||||
public int compare(AppInfo o1, AppInfo o2) {
|
|
||||||
return o1.appName.toLowerCase().compareTo(o2.appName.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areContentsTheSame(AppInfo oldItem, AppInfo newItem) {
|
|
||||||
return oldItem.appName.equals(newItem.appName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areItemsTheSame(AppInfo item1, AppInfo item2) {
|
|
||||||
return item1.appName.equals(item2.appName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.apps = new SortedList<>(AppInfo.class, callback);
|
|
||||||
this.apps.addAll(allApps);
|
|
||||||
this.allApps = new ArrayList<>(allApps);
|
|
||||||
this.activity = activity;
|
|
||||||
try {
|
|
||||||
browserIcon = activity.getPackageManager().getApplicationIcon("com.android.browser");
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AutofillRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
View v = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.autofill_row_layout, parent, false);
|
|
||||||
return new ViewHolder(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) {
|
|
||||||
AppInfo app = apps.get(position);
|
|
||||||
holder.packageName = app.packageName;
|
|
||||||
holder.appName = app.appName;
|
|
||||||
holder.isWeb = app.isWeb;
|
|
||||||
|
|
||||||
holder.icon.setImageDrawable(app.icon);
|
|
||||||
holder.name.setText(app.appName);
|
|
||||||
|
|
||||||
holder.secondary.setVisibility(View.VISIBLE);
|
|
||||||
holder.view.setBackgroundResource(R.color.grey_white_1000);
|
|
||||||
|
|
||||||
SharedPreferences prefs;
|
|
||||||
if (!app.appName.equals(app.packageName)) {
|
|
||||||
prefs = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
|
||||||
} else {
|
|
||||||
prefs = activity.getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
String preference = prefs.getString(holder.packageName, "");
|
|
||||||
switch (preference) {
|
|
||||||
case "":
|
|
||||||
holder.secondary.setVisibility(View.GONE);
|
|
||||||
holder.view.setBackgroundResource(0);
|
|
||||||
break;
|
|
||||||
case "/first":
|
|
||||||
holder.secondary.setText(R.string.autofill_apps_first);
|
|
||||||
break;
|
|
||||||
case "/never":
|
|
||||||
holder.secondary.setText(R.string.autofill_apps_never);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
holder.secondary.setText(R.string.autofill_apps_match);
|
|
||||||
holder.secondary.append(" " + preference.split("\n")[0]);
|
|
||||||
if ((preference.trim().split("\n").length - 1) > 0) {
|
|
||||||
holder.secondary.append(" and "
|
|
||||||
+ (preference.trim().split("\n").length - 1) + " more");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return apps.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
int getPosition(String appName) {
|
|
||||||
return apps.indexOf(new AppInfo(null, appName, false, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
// for websites, URL = packageName == appName
|
|
||||||
void addWebsite(String packageName) {
|
|
||||||
apps.add(new AppInfo(packageName, packageName, true, browserIcon));
|
|
||||||
allApps.add(new AppInfo(packageName, packageName, true, browserIcon));
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeWebsite(String packageName) {
|
|
||||||
apps.remove(new AppInfo(null, packageName, false, null));
|
|
||||||
allApps.remove(new AppInfo(null, packageName, false, null)); // compare with equals
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateWebsite(String oldPackageName, String packageName) {
|
|
||||||
apps.updateItemAt(getPosition(oldPackageName), new AppInfo(packageName, packageName, true, browserIcon));
|
|
||||||
allApps.remove(new AppInfo(null, oldPackageName, false, null)); // compare with equals
|
|
||||||
allApps.add(new AppInfo(null, packageName, false, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
void filter(String s) {
|
|
||||||
if (s.isEmpty()) {
|
|
||||||
apps.addAll(allApps);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
apps.beginBatchedUpdates();
|
|
||||||
for (AppInfo app : allApps) {
|
|
||||||
if (app.appName.toLowerCase().contains(s.toLowerCase())) {
|
|
||||||
apps.add(app);
|
|
||||||
} else {
|
|
||||||
apps.remove(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apps.endBatchedUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AppInfo {
|
|
||||||
public Drawable icon;
|
|
||||||
String packageName;
|
|
||||||
String appName;
|
|
||||||
boolean isWeb;
|
|
||||||
|
|
||||||
AppInfo(String packageName, String appName, boolean isWeb, Drawable icon) {
|
|
||||||
this.packageName = packageName;
|
|
||||||
this.appName = appName;
|
|
||||||
this.isWeb = isWeb;
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
return o instanceof AppInfo && this.appName.equals(((AppInfo) o).appName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
|
||||||
public View view;
|
|
||||||
public TextView name;
|
|
||||||
public ImageView icon;
|
|
||||||
TextView secondary;
|
|
||||||
String packageName;
|
|
||||||
String appName;
|
|
||||||
Boolean isWeb;
|
|
||||||
|
|
||||||
ViewHolder(View view) {
|
|
||||||
super(view);
|
|
||||||
this.view = view;
|
|
||||||
name = view.findViewById(R.id.app_name);
|
|
||||||
secondary = view.findViewById(R.id.secondary_text);
|
|
||||||
icon = view.findViewById(R.id.app_icon);
|
|
||||||
view.setOnClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
activity.showDialog(packageName, appName, isWeb);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,170 @@
|
|||||||
|
package com.zeapo.pwdstore.autofill
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
|
import com.zeapo.pwdstore.R
|
||||||
|
import com.zeapo.pwdstore.utils.splitLines
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
internal class AutofillRecyclerAdapter(
|
||||||
|
allApps: List<AppInfo>,
|
||||||
|
private val activity: AutofillPreferenceActivity
|
||||||
|
) : RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val apps: SortedList<AppInfo>
|
||||||
|
private val allApps: ArrayList<AppInfo> // for filtering, maintain a list of all
|
||||||
|
private var browserIcon: Drawable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
val callback = object : SortedListAdapterCallback<AppInfo>(this) {
|
||||||
|
// don't take into account secondary text. This is good enough
|
||||||
|
// for the limited add/remove usage for websites
|
||||||
|
override fun compare(o1: AppInfo, o2: AppInfo): Int {
|
||||||
|
return o1.appName.toLowerCase(Locale.ROOT).compareTo(o2.appName.toLowerCase(Locale.ROOT))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: AppInfo, newItem: AppInfo): Boolean {
|
||||||
|
return oldItem.appName == newItem.appName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(item1: AppInfo, item2: AppInfo): Boolean {
|
||||||
|
return item1.appName == item2.appName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apps = SortedList(AppInfo::class.java, callback)
|
||||||
|
apps.addAll(allApps)
|
||||||
|
this.allApps = ArrayList(allApps)
|
||||||
|
try {
|
||||||
|
browserIcon = activity.packageManager.getApplicationIcon("com.android.browser")
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.autofill_row_layout, parent, false)
|
||||||
|
return ViewHolder(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val app = apps.get(position)
|
||||||
|
holder.packageName = app.packageName
|
||||||
|
holder.appName = app.appName
|
||||||
|
holder.isWeb = app.isWeb
|
||||||
|
|
||||||
|
holder.icon.setImageDrawable(app.icon)
|
||||||
|
holder.name.text = app.appName
|
||||||
|
|
||||||
|
holder.secondary.visibility = View.VISIBLE
|
||||||
|
holder.view.setBackgroundResource(R.color.grey_white_1000)
|
||||||
|
|
||||||
|
val prefs: SharedPreferences
|
||||||
|
prefs = if (app.appName != app.packageName) {
|
||||||
|
activity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
|
||||||
|
} else {
|
||||||
|
activity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
when (val preference = prefs.getString(holder.packageName, "")) {
|
||||||
|
"" -> {
|
||||||
|
holder.secondary.visibility = View.GONE
|
||||||
|
holder.view.setBackgroundResource(0)
|
||||||
|
}
|
||||||
|
"/first" -> holder.secondary.setText(R.string.autofill_apps_first)
|
||||||
|
"/never" -> holder.secondary.setText(R.string.autofill_apps_never)
|
||||||
|
else -> {
|
||||||
|
holder.secondary.setText(R.string.autofill_apps_match)
|
||||||
|
holder.secondary.append(" " + preference!!.splitLines()[0])
|
||||||
|
if (preference.trim { it <= ' ' }.splitLines().size - 1 > 0) {
|
||||||
|
holder.secondary.append(" and "
|
||||||
|
+ (preference.trim { it <= ' ' }.splitLines().size - 1) + " more")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return apps.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPosition(appName: String): Int {
|
||||||
|
return apps.indexOf(AppInfo(null, appName, false, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
// for websites, URL = packageName == appName
|
||||||
|
fun addWebsite(packageName: String) {
|
||||||
|
apps.add(AppInfo(packageName, packageName, true, browserIcon))
|
||||||
|
allApps.add(AppInfo(packageName, packageName, true, browserIcon))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeWebsite(packageName: String) {
|
||||||
|
apps.remove(AppInfo(null, packageName, false, null))
|
||||||
|
allApps.remove(AppInfo(null, packageName, false, null)) // compare with equals
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWebsite(oldPackageName: String, packageName: String) {
|
||||||
|
apps.updateItemAt(getPosition(oldPackageName), AppInfo(packageName, packageName, true, browserIcon))
|
||||||
|
allApps.remove(AppInfo(null, oldPackageName, false, null)) // compare with equals
|
||||||
|
allApps.add(AppInfo(null, packageName, false, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filter(s: String) {
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
apps.addAll(allApps)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apps.beginBatchedUpdates()
|
||||||
|
for (app in allApps) {
|
||||||
|
if (app.appName.toLowerCase(Locale.ROOT).contains(s.toLowerCase(Locale.ROOT))) {
|
||||||
|
apps.add(app)
|
||||||
|
} else {
|
||||||
|
apps.remove(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apps.endBatchedUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AppInfo(var packageName: String?, var appName: String, var isWeb: Boolean, var icon: Drawable?) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is AppInfo && this.appName == other.appName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = packageName?.hashCode() ?: 0
|
||||||
|
result = 31 * result + appName.hashCode()
|
||||||
|
result = 31 * result + isWeb.hashCode()
|
||||||
|
result = 31 * result + (icon?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class ViewHolder(var view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||||
|
var name: TextView = view.findViewById(R.id.app_name)
|
||||||
|
var icon: ImageView = view.findViewById(R.id.app_icon)
|
||||||
|
var secondary: TextView = view.findViewById(R.id.secondary_text)
|
||||||
|
var packageName: String? = null
|
||||||
|
var appName: String? = null
|
||||||
|
var isWeb: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
activity.showDialog(packageName, appName, isWeb)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -1,606 +0,0 @@
|
|||||||
package com.zeapo.pwdstore.autofill;
|
|
||||||
|
|
||||||
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.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo;
|
|
||||||
import android.view.accessibility.AccessibilityWindowInfo;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import com.zeapo.pwdstore.PasswordEntry;
|
|
||||||
import com.zeapo.pwdstore.R;
|
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.openintents.openpgp.IOpenPgpService2;
|
|
||||||
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.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class AutofillService extends AccessibilityService {
|
|
||||||
private static AutofillService instance;
|
|
||||||
private OpenPgpServiceConnection serviceConnection;
|
|
||||||
private SharedPreferences settings;
|
|
||||||
private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
|
|
||||||
private ArrayList<File> items; // password choices
|
|
||||||
private int lastWhichItem;
|
|
||||||
private AlertDialog dialog;
|
|
||||||
private AccessibilityWindowInfo window;
|
|
||||||
private Intent resultData = null; // need the intent which contains results from user interaction
|
|
||||||
private CharSequence packageName;
|
|
||||||
private boolean ignoreActionFocus = false;
|
|
||||||
private String webViewTitle = null;
|
|
||||||
private String webViewURL = null;
|
|
||||||
private PasswordEntry lastPassword;
|
|
||||||
private long lastPasswordMaxDate;
|
|
||||||
|
|
||||||
public static AutofillService getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResultData(Intent data) {
|
|
||||||
resultData = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPickedPassword(String path) {
|
|
||||||
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
|
|
||||||
bindDecryptAndVerify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onServiceConnected() {
|
|
||||||
super.onServiceConnected();
|
|
||||||
serviceConnection = new OpenPgpServiceConnection(AutofillService.this
|
|
||||||
, "org.sufficientlysecure.keychain");
|
|
||||||
serviceConnection.bindToService();
|
|
||||||
settings = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
|
||||||
// remove stored password from cache
|
|
||||||
if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
|
|
||||||
lastPassword = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if returning to the source app from a successful AutofillActivity
|
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
|
||||||
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
|
|
||||||
&& resultData != null) {
|
|
||||||
bindDecryptAndVerify();
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for webView and trigger accessibility events if window changes
|
|
||||||
// or if page changes in chrome
|
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
|
||||||
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
||||||
&& event.getPackageName() != null
|
|
||||||
&& (event.getPackageName().equals("com.android.chrome")
|
|
||||||
|| event.getPackageName().equals("com.android.browser")))) {
|
|
||||||
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
|
||||||
try {
|
|
||||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
|
||||||
webViewTitle = searchWebView(root);
|
|
||||||
webViewURL = null;
|
|
||||||
if (webViewTitle != null) {
|
|
||||||
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar");
|
|
||||||
if (nodes.isEmpty()) {
|
|
||||||
nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url");
|
|
||||||
}
|
|
||||||
for (AccessibilityNodeInfo node : nodes)
|
|
||||||
if (node.getText() != null) {
|
|
||||||
try {
|
|
||||||
webViewURL = new URL(node.getText().toString()).getHost();
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
if (e.toString().contains("Protocol not found")) {
|
|
||||||
try {
|
|
||||||
webViewURL = new URL("http://" + node.getText().toString()).getHost();
|
|
||||||
} catch (MalformedURLException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// sadly we were unable to access the data we wanted
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nothing to do if field is keychain app or system ui
|
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
||||||
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
|
|
||||||
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
|
|
||||||
dismissDialog(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!event.isPassword()) {
|
|
||||||
if (lastPassword != null && event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.getSource().isEditable()) {
|
|
||||||
showPasteUsernameDialog(event.getSource(), lastPassword);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// nothing to do if not password field focus
|
|
||||||
dismissDialog(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialog != null && dialog.isShowing()) {
|
|
||||||
// the current dialog must belong to this window; ignore clicks on this password field
|
|
||||||
// why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
|
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if it was not a click, the field was refocused or another field was focused; recreate
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
|
|
||||||
if (ignoreActionFocus) {
|
|
||||||
ignoreActionFocus = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to request permission before attempting to draw dialog
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
&& !Settings.canDrawOverlays(this)) {
|
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
|
||||||
Uri.parse("package:" + getPackageName()));
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we are now going to attempt to fill, save AccessibilityNodeInfo for later in decryptAndVerify
|
|
||||||
// (there should be a proper way to do this, although this seems to work 90% of the time)
|
|
||||||
info = event.getSource();
|
|
||||||
if (info == null) return;
|
|
||||||
|
|
||||||
// save the dialog's corresponding window so we can use getWindows() in dismissDialog
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
window = info.getWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
String packageName;
|
|
||||||
String appName;
|
|
||||||
boolean isWeb;
|
|
||||||
|
|
||||||
// Match with the app if a webview was not found or one was found but
|
|
||||||
// there's no title or url to go by
|
|
||||||
if (webViewTitle == null || (webViewTitle.equals("") && webViewURL == null)) {
|
|
||||||
if (info.getPackageName() == null) return;
|
|
||||||
packageName = info.getPackageName().toString();
|
|
||||||
|
|
||||||
// get the app name and find a corresponding password
|
|
||||||
PackageManager packageManager = getPackageManager();
|
|
||||||
ApplicationInfo applicationInfo;
|
|
||||||
try {
|
|
||||||
applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
applicationInfo = null;
|
|
||||||
}
|
|
||||||
appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
|
|
||||||
|
|
||||||
isWeb = false;
|
|
||||||
|
|
||||||
setAppMatchingPasswords(appName, packageName);
|
|
||||||
} else {
|
|
||||||
// now we may have found a title but webViewURL could be null
|
|
||||||
// we set packagename so that we can find the website setting entry
|
|
||||||
packageName = setWebMatchingPasswords(webViewTitle, webViewURL);
|
|
||||||
appName = packageName;
|
|
||||||
isWeb = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if autofill_always checked, show dialog even if no matches (automatic
|
|
||||||
// or otherwise)
|
|
||||||
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showSelectPasswordDialog(packageName, appName, isWeb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String searchWebView(AccessibilityNodeInfo source) {
|
|
||||||
return searchWebView(source, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String searchWebView(AccessibilityNodeInfo source, int depth) {
|
|
||||||
if (source == null || depth == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < source.getChildCount(); i++) {
|
|
||||||
AccessibilityNodeInfo u = source.getChild(i);
|
|
||||||
if (u == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (u.getClassName() != null && u.getClassName().equals("android.webkit.WebView")) {
|
|
||||||
if (u.getContentDescription() != null) {
|
|
||||||
return u.getContentDescription().toString();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
String webView = searchWebView(u, depth - 1);
|
|
||||||
if (webView != null) {
|
|
||||||
return webView;
|
|
||||||
}
|
|
||||||
u.recycle();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// dismiss the dialog if the window has changed
|
|
||||||
private void dismissDialog(AccessibilityEvent event) {
|
|
||||||
// the default keyboard showing/hiding is a window state changed event
|
|
||||||
// on Android 5+ we can use getWindows() to determine when the original window is not visible
|
|
||||||
// on Android 4.3 we have to use window state changed events and filter out the keyboard ones
|
|
||||||
// there may be other exceptions...
|
|
||||||
boolean dismiss;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
dismiss = !getWindows().contains(window);
|
|
||||||
} else {
|
|
||||||
dismiss = !(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
|
||||||
event.getPackageName() != null &&
|
|
||||||
event.getPackageName().toString().contains("inputmethod"));
|
|
||||||
}
|
|
||||||
if (dismiss && dialog != null && dialog.isShowing()) {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String setWebMatchingPasswords(String webViewTitle, String webViewURL) {
|
|
||||||
// Return the URL needed to open the corresponding Settings.
|
|
||||||
String settingsURL = webViewURL;
|
|
||||||
|
|
||||||
// if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
|
|
||||||
String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
|
|
||||||
SharedPreferences prefs;
|
|
||||||
String preference;
|
|
||||||
|
|
||||||
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
|
||||||
preference = defValue;
|
|
||||||
if (webViewURL != null) {
|
|
||||||
final String webViewUrlLowerCase = webViewURL.toLowerCase();
|
|
||||||
Map<String, ?> prefsMap = prefs.getAll();
|
|
||||||
for (String key : prefsMap.keySet()) {
|
|
||||||
// for websites unlike apps there can be blank preference of "" which
|
|
||||||
// means use default, so ignore it.
|
|
||||||
final String value = prefs.getString(key, null);
|
|
||||||
final String keyLowerCase = key.toLowerCase();
|
|
||||||
if (value != null && !value.equals("")
|
|
||||||
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
|
|
||||||
preference = value;
|
|
||||||
settingsURL = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (preference) {
|
|
||||||
case "/first":
|
|
||||||
if (!PasswordRepository.isInitialized()) {
|
|
||||||
PasswordRepository.initialize(this);
|
|
||||||
}
|
|
||||||
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle);
|
|
||||||
break;
|
|
||||||
case "/never":
|
|
||||||
items = new ArrayList<>();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
getPreferredPasswords(preference);
|
|
||||||
}
|
|
||||||
|
|
||||||
return settingsURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setAppMatchingPasswords(String appName, String packageName) {
|
|
||||||
// if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
|
|
||||||
String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
|
|
||||||
SharedPreferences prefs;
|
|
||||||
String preference;
|
|
||||||
|
|
||||||
prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
|
||||||
preference = prefs.getString(packageName, defValue);
|
|
||||||
|
|
||||||
switch (preference) {
|
|
||||||
case "/first":
|
|
||||||
if (!PasswordRepository.isInitialized()) {
|
|
||||||
PasswordRepository.initialize(this);
|
|
||||||
}
|
|
||||||
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName);
|
|
||||||
break;
|
|
||||||
case "/never":
|
|
||||||
items = new ArrayList<>();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
getPreferredPasswords(preference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the newline separated list of passwords from the SharedPreferences
|
|
||||||
// file into the items list.
|
|
||||||
private void getPreferredPasswords(String preference) {
|
|
||||||
if (!PasswordRepository.isInitialized()) {
|
|
||||||
PasswordRepository.initialize(this);
|
|
||||||
}
|
|
||||||
String preferredPasswords[] = preference.split("\n");
|
|
||||||
items = new ArrayList<>();
|
|
||||||
for (String password : preferredPasswords) {
|
|
||||||
String path = PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + password + ".gpg";
|
|
||||||
if (new File(path).exists()) {
|
|
||||||
items.add(new File(path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<File> searchPasswords(File path, String appName) {
|
|
||||||
ArrayList<File> passList = PasswordRepository.getFilesList(path);
|
|
||||||
|
|
||||||
if (passList.size() == 0) return new ArrayList<>();
|
|
||||||
|
|
||||||
ArrayList<File> items = new ArrayList<>();
|
|
||||||
|
|
||||||
for (File file : passList) {
|
|
||||||
if (file.isFile()) {
|
|
||||||
if (!file.isHidden() && appName.toLowerCase().contains(file.getName().toLowerCase().replace(".gpg", ""))) {
|
|
||||||
items.add(file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!file.isHidden()) {
|
|
||||||
items.addAll(searchPasswords(file, appName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showPasteUsernameDialog(final AccessibilityNodeInfo node, final PasswordEntry password) {
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
|
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, (d, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
});
|
|
||||||
builder.setPositiveButton(R.string.autofill_paste, (d, which) -> {
|
|
||||||
pasteText(node, password.getUsername());
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
});
|
|
||||||
builder.setMessage(getString(R.string.autofill_paste_username, password.getUsername()));
|
|
||||||
|
|
||||||
dialog = builder.create();
|
|
||||||
this.setDialogType(dialog);
|
|
||||||
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
|
||||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSelectPasswordDialog(final String packageName, final String appName, final boolean isWeb) {
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
|
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, (d, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
});
|
|
||||||
builder.setNeutralButton("Settings", (dialog, which) -> {
|
|
||||||
//TODO make icon? gear?
|
|
||||||
// the user will have to return to the app themselves.
|
|
||||||
Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
intent.putExtra("packageName", packageName);
|
|
||||||
intent.putExtra("appName", appName);
|
|
||||||
intent.putExtra("isWeb", isWeb);
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// populate the dialog items, always with pick + pick and match. Could
|
|
||||||
// make it optional (or make height a setting for the same effect)
|
|
||||||
CharSequence itemNames[] = new CharSequence[items.size() + 2];
|
|
||||||
for (int i = 0; i < items.size(); i++) {
|
|
||||||
itemNames[i] = items.get(i).getName().replace(".gpg", "");
|
|
||||||
}
|
|
||||||
itemNames[items.size()] = getString(R.string.autofill_pick);
|
|
||||||
itemNames[items.size() + 1] = getString(R.string.autofill_pick_and_match);
|
|
||||||
builder.setItems(itemNames, (dialog, which) -> {
|
|
||||||
lastWhichItem = which;
|
|
||||||
if (which < items.size()) {
|
|
||||||
bindDecryptAndVerify();
|
|
||||||
} else if (which == items.size()) {
|
|
||||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
intent.putExtra("pick", true);
|
|
||||||
startActivity(intent);
|
|
||||||
} else {
|
|
||||||
lastWhichItem--; // will add one element to items, so lastWhichItem=items.size()+1
|
|
||||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
intent.putExtra("pickMatchWith", true);
|
|
||||||
intent.putExtra("packageName", packageName);
|
|
||||||
intent.putExtra("isWeb", isWeb);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog = builder.create();
|
|
||||||
this.setDialogType(dialog);
|
|
||||||
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
|
||||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
|
||||||
// arbitrary non-annoying size
|
|
||||||
int height = 154;
|
|
||||||
if (itemNames.length > 1) {
|
|
||||||
height += 46;
|
|
||||||
}
|
|
||||||
dialog.getWindow().setLayout((int) (240 * getApplicationContext().getResources().getDisplayMetrics().density)
|
|
||||||
, (int) (height * getApplicationContext().getResources().getDisplayMetrics().density));
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDialogType(AlertDialog dialog) {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
|
||||||
} else {
|
|
||||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInterrupt() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bindDecryptAndVerify() {
|
|
||||||
if (serviceConnection.getService() == null) {
|
|
||||||
// the service was disconnected, need to bind again
|
|
||||||
// give it a listener and in the callback we will decryptAndVerify
|
|
||||||
serviceConnection = new OpenPgpServiceConnection(AutofillService.this
|
|
||||||
, "org.sufficientlysecure.keychain", new onBoundListener());
|
|
||||||
serviceConnection.bindToService();
|
|
||||||
} else {
|
|
||||||
decryptAndVerify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void decryptAndVerify() {
|
|
||||||
packageName = info.getPackageName();
|
|
||||||
Intent data;
|
|
||||||
if (resultData == null) {
|
|
||||||
data = new Intent();
|
|
||||||
data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
|
||||||
} else {
|
|
||||||
data = resultData;
|
|
||||||
resultData = null;
|
|
||||||
}
|
|
||||||
InputStream is = null;
|
|
||||||
try {
|
|
||||||
is = FileUtils.openInputStream(items.get(lastWhichItem));
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
|
|
||||||
// TODO we are dropping frames, (did we before??) find out why and maybe make this async
|
|
||||||
Intent result = api.executeApi(data, is, os);
|
|
||||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
|
||||||
case OpenPgpApi.RESULT_CODE_SUCCESS: {
|
|
||||||
try {
|
|
||||||
final PasswordEntry entry = new PasswordEntry(os);
|
|
||||||
pasteText(info, entry.getPassword());
|
|
||||||
|
|
||||||
// save password entry for pasting the username as well
|
|
||||||
if (entry.hasUsername()) {
|
|
||||||
lastPassword = entry;
|
|
||||||
final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
|
|
||||||
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
|
|
||||||
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
|
|
||||||
}
|
|
||||||
} 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.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pasteText(final AccessibilityNodeInfo node, final String text) {
|
|
||||||
// if the user focused on something else, take focus back
|
|
||||||
// but this will open another dialog...hack to ignore this
|
|
||||||
// & need to ensure performAction correct (i.e. what is info now?)
|
|
||||||
ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
|
|
||||||
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
|
|
||||||
} else {
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText("autofill_pm", text);
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
node.performAction(AccessibilityNodeInfo.ACTION_PASTE);
|
|
||||||
|
|
||||||
clip = ClipData.newPlainText("autofill_pm", "");
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
if (settings.getBoolean("clear_clipboard_20x", false)) {
|
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.recycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
final class Constants {
|
|
||||||
static final String TAG = "Keychain";
|
|
||||||
}
|
|
||||||
|
|
||||||
private class onBoundListener implements OpenPgpServiceConnection.OnBound {
|
|
||||||
@Override
|
|
||||||
public void onBound(IOpenPgpService2 service) {
|
|
||||||
decryptAndVerify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
582
app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
Normal file
582
app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
package com.zeapo.pwdstore.autofill
|
||||||
|
|
||||||
|
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.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import android.view.accessibility.AccessibilityWindowInfo
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.zeapo.pwdstore.PasswordEntry
|
||||||
|
import com.zeapo.pwdstore.R
|
||||||
|
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||||
|
import com.zeapo.pwdstore.utils.splitLines
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.openintents.openpgp.IOpenPgpService2
|
||||||
|
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.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AutofillService : AccessibilityService() {
|
||||||
|
private var serviceConnection: OpenPgpServiceConnection? = null
|
||||||
|
private var settings: SharedPreferences? = null
|
||||||
|
private var info: AccessibilityNodeInfo? = null // the original source of the event (the edittext field)
|
||||||
|
private var items: ArrayList<File> = arrayListOf() // password choices
|
||||||
|
private var lastWhichItem: Int = 0
|
||||||
|
private var dialog: AlertDialog? = null
|
||||||
|
private var window: AccessibilityWindowInfo? = null
|
||||||
|
private var resultData: Intent? = null // need the intent which contains results from user interaction
|
||||||
|
private var packageName: CharSequence? = null
|
||||||
|
private var ignoreActionFocus = false
|
||||||
|
private var webViewTitle: String? = null
|
||||||
|
private var webViewURL: String? = null
|
||||||
|
private var lastPassword: PasswordEntry? = null
|
||||||
|
private var lastPasswordMaxDate: Long = 0
|
||||||
|
|
||||||
|
fun setResultData(data: Intent) {
|
||||||
|
resultData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPickedPassword(path: String) {
|
||||||
|
items.add(File("${PasswordRepository.getRepositoryDirectory(applicationContext)}/$path.gpg"))
|
||||||
|
bindDecryptAndVerify()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected() {
|
||||||
|
super.onServiceConnected()
|
||||||
|
serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain")
|
||||||
|
serviceConnection!!.bindToService()
|
||||||
|
settings = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||||
|
// remove stored password from cache
|
||||||
|
if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
|
||||||
|
lastPassword = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// if returning to the source app from a successful AutofillActivity
|
||||||
|
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||||
|
&& event.packageName != null && event.packageName == packageName
|
||||||
|
&& resultData != null) {
|
||||||
|
bindDecryptAndVerify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for webView and trigger accessibility events if window changes
|
||||||
|
// or if page changes in chrome
|
||||||
|
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||||
|
&& event.packageName != null
|
||||||
|
&& (event.packageName == "com.android.chrome" || event.packageName == "com.android.browser"))) {
|
||||||
|
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
||||||
|
try {
|
||||||
|
val root = rootInActiveWindow
|
||||||
|
webViewTitle = searchWebView(root)
|
||||||
|
webViewURL = null
|
||||||
|
if (webViewTitle != null) {
|
||||||
|
var nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
|
||||||
|
if (nodes.isEmpty()) {
|
||||||
|
nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url")
|
||||||
|
}
|
||||||
|
for (node in nodes)
|
||||||
|
if (node.text != null) {
|
||||||
|
try {
|
||||||
|
webViewURL = URL(node.text.toString()).host
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
if (e.toString().contains("Protocol not found")) {
|
||||||
|
try {
|
||||||
|
webViewURL = URL("http://" + node.text.toString()).host
|
||||||
|
} catch (ignored: MalformedURLException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// sadly we were unable to access the data we wanted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to do if field is keychain app or system ui
|
||||||
|
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||||
|
|| event.packageName != null && event.packageName == "org.sufficientlysecure.keychain"
|
||||||
|
|| event.packageName != null && event.packageName == "com.android.systemui") {
|
||||||
|
dismissDialog(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.isPassword) {
|
||||||
|
if (lastPassword != null && event.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.source.isEditable) {
|
||||||
|
showPasteUsernameDialog(event.source, lastPassword!!)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// nothing to do if not password field focus
|
||||||
|
dismissDialog(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog != null && dialog!!.isShowing) {
|
||||||
|
// the current dialog must belong to this window; ignore clicks on this password field
|
||||||
|
// why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
|
||||||
|
if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if it was not a click, the field was refocused or another field was focused; recreate
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
|
||||||
|
if (ignoreActionFocus) {
|
||||||
|
ignoreActionFocus = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to request permission before attempting to draw dialog
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||||
|
Uri.parse("package:" + getPackageName()))
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivity(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are now going to attempt to fill, save AccessibilityNodeInfo for later in decryptAndVerify
|
||||||
|
// (there should be a proper way to do this, although this seems to work 90% of the time)
|
||||||
|
info = event.source
|
||||||
|
if (info == null) return
|
||||||
|
|
||||||
|
// save the dialog's corresponding window so we can use getWindows() in dismissDialog
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
window = info!!.window
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageName: String
|
||||||
|
val appName: String
|
||||||
|
val isWeb: Boolean
|
||||||
|
|
||||||
|
// Match with the app if a webview was not found or one was found but
|
||||||
|
// there's no title or url to go by
|
||||||
|
if (webViewTitle == null || webViewTitle == "" && webViewURL == null) {
|
||||||
|
if (info!!.packageName == null) return
|
||||||
|
packageName = info!!.packageName.toString()
|
||||||
|
|
||||||
|
// get the app name and find a corresponding password
|
||||||
|
val packageManager = packageManager
|
||||||
|
var applicationInfo: ApplicationInfo?
|
||||||
|
try {
|
||||||
|
applicationInfo = packageManager.getApplicationInfo(event.packageName.toString(), 0)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
applicationInfo = null
|
||||||
|
}
|
||||||
|
|
||||||
|
appName = (if (applicationInfo != null) packageManager.getApplicationLabel(applicationInfo) else "").toString()
|
||||||
|
|
||||||
|
isWeb = false
|
||||||
|
|
||||||
|
setAppMatchingPasswords(appName, packageName)
|
||||||
|
} else {
|
||||||
|
// now we may have found a title but webViewURL could be null
|
||||||
|
// we set packagename so that we can find the website setting entry
|
||||||
|
packageName = setWebMatchingPasswords(webViewTitle!!, webViewURL)
|
||||||
|
appName = packageName
|
||||||
|
isWeb = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if autofill_always checked, show dialog even if no matches (automatic
|
||||||
|
// or otherwise)
|
||||||
|
if (items.isEmpty() && !settings!!.getBoolean("autofill_always", false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showSelectPasswordDialog(packageName, appName, isWeb)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchWebView(source: AccessibilityNodeInfo?, depth: Int = 10): String? {
|
||||||
|
if (source == null || depth == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (i in 0 until source.childCount) {
|
||||||
|
val u = source.getChild(i) ?: continue
|
||||||
|
if (u.className != null && u.className == "android.webkit.WebView") {
|
||||||
|
return if (u.contentDescription != null) {
|
||||||
|
u.contentDescription.toString()
|
||||||
|
} else ""
|
||||||
|
}
|
||||||
|
val webView = searchWebView(u, depth - 1)
|
||||||
|
if (webView != null) {
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
u.recycle()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// dismiss the dialog if the window has changed
|
||||||
|
private fun dismissDialog(event: AccessibilityEvent) {
|
||||||
|
// the default keyboard showing/hiding is a window state changed event
|
||||||
|
// on Android 5+ we can use getWindows() to determine when the original window is not visible
|
||||||
|
// on Android 4.3 we have to use window state changed events and filter out the keyboard ones
|
||||||
|
// there may be other exceptions...
|
||||||
|
val dismiss: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
!windows.contains(window)
|
||||||
|
} else {
|
||||||
|
!(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
||||||
|
event.packageName != null &&
|
||||||
|
event.packageName.toString().contains("inputmethod"))
|
||||||
|
}
|
||||||
|
if (dismiss && dialog != null && dialog!!.isShowing) {
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWebMatchingPasswords(webViewTitle: String, webViewURL: String?): String {
|
||||||
|
// Return the URL needed to open the corresponding Settings.
|
||||||
|
var settingsURL = webViewURL
|
||||||
|
|
||||||
|
// if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
|
||||||
|
val defValue = if (settings!!.getBoolean("autofill_default", true)) "/first" else "/never"
|
||||||
|
val prefs: SharedPreferences = getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
|
||||||
|
var preference: String
|
||||||
|
|
||||||
|
preference = defValue
|
||||||
|
if (webViewURL != null) {
|
||||||
|
val webViewUrlLowerCase = webViewURL.toLowerCase(Locale.ROOT)
|
||||||
|
val prefsMap = prefs.all
|
||||||
|
for (key in prefsMap.keys) {
|
||||||
|
// for websites unlike apps there can be blank preference of "" which
|
||||||
|
// means use default, so ignore it.
|
||||||
|
val value = prefs.getString(key, null)
|
||||||
|
val keyLowerCase = key.toLowerCase(Locale.ROOT)
|
||||||
|
if (value != null && value != ""
|
||||||
|
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
|
||||||
|
preference = value
|
||||||
|
settingsURL = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (preference) {
|
||||||
|
"/first" -> {
|
||||||
|
if (!PasswordRepository.isInitialized()) {
|
||||||
|
PasswordRepository.initialize(this)
|
||||||
|
}
|
||||||
|
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle)
|
||||||
|
}
|
||||||
|
"/never" -> items = ArrayList()
|
||||||
|
else -> getPreferredPasswords(preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsURL!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAppMatchingPasswords(appName: String, packageName: String) {
|
||||||
|
// if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
|
||||||
|
val defValue = if (settings!!.getBoolean("autofill_default", true)) "/first" else "/never"
|
||||||
|
val prefs: SharedPreferences = getSharedPreferences("autofill", Context.MODE_PRIVATE)
|
||||||
|
val preference: String?
|
||||||
|
|
||||||
|
preference = prefs.getString(packageName, defValue)
|
||||||
|
|
||||||
|
when (preference) {
|
||||||
|
"/first" -> {
|
||||||
|
if (!PasswordRepository.isInitialized()) {
|
||||||
|
PasswordRepository.initialize(this)
|
||||||
|
}
|
||||||
|
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName)
|
||||||
|
}
|
||||||
|
"/never" -> items = ArrayList()
|
||||||
|
else -> getPreferredPasswords(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the newline separated list of passwords from the SharedPreferences
|
||||||
|
// file into the items list.
|
||||||
|
private fun getPreferredPasswords(preference: String) {
|
||||||
|
if (!PasswordRepository.isInitialized()) {
|
||||||
|
PasswordRepository.initialize(this)
|
||||||
|
}
|
||||||
|
val preferredPasswords = preference.splitLines()
|
||||||
|
items = ArrayList()
|
||||||
|
for (password in preferredPasswords) {
|
||||||
|
val path = PasswordRepository.getRepositoryDirectory(applicationContext).toString() + "/" + password + ".gpg"
|
||||||
|
if (File(path).exists()) {
|
||||||
|
items.add(File(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchPasswords(path: File?, appName: String): ArrayList<File> {
|
||||||
|
val passList = PasswordRepository.getFilesList(path)
|
||||||
|
|
||||||
|
if (passList.size == 0) return ArrayList()
|
||||||
|
|
||||||
|
val items = ArrayList<File>()
|
||||||
|
|
||||||
|
for (file in passList) {
|
||||||
|
if (file.isFile) {
|
||||||
|
if (!file.isHidden && appName.toLowerCase(Locale.ROOT).contains(file.name.toLowerCase(Locale.ROOT).replace(".gpg", ""))) {
|
||||||
|
items.add(file)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!file.isHidden) {
|
||||||
|
items.addAll(searchPasswords(file, appName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPasteUsernameDialog(node: AccessibilityNodeInfo, password: PasswordEntry) {
|
||||||
|
if (dialog != null) {
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog)
|
||||||
|
builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
builder.setPositiveButton(R.string.autofill_paste) { _, _ ->
|
||||||
|
pasteText(node, password.username)
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
builder.setMessage(getString(R.string.autofill_paste_username, password.username))
|
||||||
|
|
||||||
|
dialog = builder.create()
|
||||||
|
this.setDialogType(dialog)
|
||||||
|
dialog!!.window!!.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
|
||||||
|
dialog!!.window!!.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
dialog!!.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSelectPasswordDialog(packageName: String, appName: String, isWeb: Boolean) {
|
||||||
|
if (dialog != null) {
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog)
|
||||||
|
builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
|
||||||
|
dialog!!.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
builder.setNeutralButton("Settings") { _, _ ->
|
||||||
|
//TODO make icon? gear?
|
||||||
|
// the user will have to return to the app themselves.
|
||||||
|
val intent = Intent(this@AutofillService, AutofillPreferenceActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
intent.putExtra("packageName", packageName)
|
||||||
|
intent.putExtra("appName", appName)
|
||||||
|
intent.putExtra("isWeb", isWeb)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate the dialog items, always with pick + pick and match. Could
|
||||||
|
// make it optional (or make height a setting for the same effect)
|
||||||
|
val itemNames = arrayOfNulls<CharSequence>(items.size + 2)
|
||||||
|
for (i in items.indices) {
|
||||||
|
itemNames[i] = items[i].name.replace(".gpg", "")
|
||||||
|
}
|
||||||
|
itemNames[items.size] = getString(R.string.autofill_pick)
|
||||||
|
itemNames[items.size + 1] = getString(R.string.autofill_pick_and_match)
|
||||||
|
builder.setItems(itemNames) { _, which ->
|
||||||
|
lastWhichItem = which
|
||||||
|
when {
|
||||||
|
which < items.size -> bindDecryptAndVerify()
|
||||||
|
which == items.size -> {
|
||||||
|
val intent = Intent(this@AutofillService, AutofillActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
intent.putExtra("pick", true)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
lastWhichItem-- // will add one element to items, so lastWhichItem=items.size()+1
|
||||||
|
val intent = Intent(this@AutofillService, AutofillActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
intent.putExtra("pickMatchWith", true)
|
||||||
|
intent.putExtra("packageName", packageName)
|
||||||
|
intent.putExtra("isWeb", isWeb)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog = builder.create()
|
||||||
|
setDialogType(dialog)
|
||||||
|
dialog?.window?.apply {
|
||||||
|
val height = 200
|
||||||
|
val density = context.resources.displayMetrics.density
|
||||||
|
addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
|
||||||
|
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
// arbitrary non-annoying size
|
||||||
|
setLayout((240 * density).toInt(), (height * density).toInt())
|
||||||
|
}
|
||||||
|
dialog?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDialogType(dialog: AlertDialog?) {
|
||||||
|
dialog?.window?.apply {
|
||||||
|
setType(
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||||
|
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
|
||||||
|
else
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterrupt() {}
|
||||||
|
|
||||||
|
private fun bindDecryptAndVerify() {
|
||||||
|
if (serviceConnection!!.service == null) {
|
||||||
|
// the service was disconnected, need to bind again
|
||||||
|
// give it a listener and in the callback we will decryptAndVerify
|
||||||
|
serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain", OnBoundListener())
|
||||||
|
serviceConnection!!.bindToService()
|
||||||
|
} else {
|
||||||
|
decryptAndVerify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptAndVerify() {
|
||||||
|
packageName = info!!.packageName
|
||||||
|
val data: Intent
|
||||||
|
if (resultData == null) {
|
||||||
|
data = Intent()
|
||||||
|
data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY
|
||||||
|
} else {
|
||||||
|
data = resultData!!
|
||||||
|
resultData = null
|
||||||
|
}
|
||||||
|
var `is`: InputStream? = null
|
||||||
|
try {
|
||||||
|
`is` = FileUtils.openInputStream(items[lastWhichItem])
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
val os = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service)
|
||||||
|
// TODO we are dropping frames, (did we before??) find out why and maybe make this async
|
||||||
|
val result = api.executeApi(data, `is`, os)
|
||||||
|
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||||
|
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
||||||
|
try {
|
||||||
|
val entry = PasswordEntry(os)
|
||||||
|
pasteText(info!!, entry.password)
|
||||||
|
|
||||||
|
// save password entry for pasting the username as well
|
||||||
|
if (entry.hasUsername()) {
|
||||||
|
lastPassword = entry
|
||||||
|
val ttl = Integer.parseInt(settings!!.getString("general_show_time", "45")!!)
|
||||||
|
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show()
|
||||||
|
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L
|
||||||
|
}
|
||||||
|
} catch (e: UnsupportedEncodingException) {
|
||||||
|
Log.e(Constants.TAG, "UnsupportedEncodingException", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
|
||||||
|
Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED")
|
||||||
|
val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)
|
||||||
|
// need to start a blank activity to call startIntentSenderForResult
|
||||||
|
val intent = Intent(this@AutofillService, AutofillActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
intent.putExtra("pending_intent", pi)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
OpenPgpApi.RESULT_CODE_ERROR -> {
|
||||||
|
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
|
||||||
|
Toast.makeText(this@AutofillService,
|
||||||
|
"Error from OpenKeyChain : " + error.message,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(Constants.TAG, "onError getErrorId:" + error.errorId)
|
||||||
|
Log.e(Constants.TAG, "onError getMessage:" + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pasteText(node: AccessibilityNodeInfo, text: String?) {
|
||||||
|
// if the user focused on something else, take focus back
|
||||||
|
// but this will open another dialog...hack to ignore this
|
||||||
|
// & need to ensure performAction correct (i.e. what is info now?)
|
||||||
|
ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
|
||||||
|
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
|
||||||
|
} else {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
var clip = ClipData.newPlainText("autofill_pm", text)
|
||||||
|
clipboard.primaryClip = clip
|
||||||
|
node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
||||||
|
|
||||||
|
clip = ClipData.newPlainText("autofill_pm", "")
|
||||||
|
clipboard.primaryClip = clip
|
||||||
|
if (settings!!.getBoolean("clear_clipboard_20x", false)) {
|
||||||
|
for (i in 0..19) {
|
||||||
|
clip = ClipData.newPlainText(i.toString(), i.toString())
|
||||||
|
clipboard.primaryClip = clip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object Constants {
|
||||||
|
const val TAG = "Keychain"
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class OnBoundListener : OpenPgpServiceConnection.OnBound {
|
||||||
|
override fun onBound(service: IOpenPgpService2) {
|
||||||
|
decryptAndVerify()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var instance: AutofillService? = null
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
@@ -435,8 +435,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg"
|
val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg"
|
||||||
|
|
||||||
api?.executeApiAsync(data, iStream, oStream) { result: Intent? ->
|
api?.executeApiAsync(data, iStream, oStream) { result: Intent? ->
|
||||||
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
RESULT_CODE_SUCCESS -> {
|
||||||
try {
|
try {
|
||||||
// TODO This might fail, we should check that the write is successful
|
// TODO This might fail, we should check that the write is successful
|
||||||
val outputStream = FileUtils.openOutputStream(File(path))
|
val outputStream = FileUtils.openOutputStream(File(path))
|
||||||
@@ -459,7 +459,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
Log.e(TAG, "An Exception occurred", e)
|
Log.e(TAG, "An Exception occurred", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
|
RESULT_CODE_ERROR -> handleError(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -516,7 +516,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
|
|
||||||
private fun calculateHotp(entry: PasswordEntry) {
|
private fun calculateHotp(entry: PasswordEntry) {
|
||||||
copyOtpToClipBoard(Otp.calculateCode(entry.hotpSecret, entry.hotpCounter!! + 1, "sha1", entry.digits))
|
copyOtpToClipBoard(Otp.calculateCode(entry.hotpSecret, entry.hotpCounter!! + 1, "sha1", entry.digits))
|
||||||
crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter!! + 1, "sha1", entry.digits)
|
crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1, "sha1", entry.digits)
|
||||||
crypto_extra_show.text = entry.extraContent
|
crypto_extra_show.text = entry.extraContent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,8 +539,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
val data = receivedIntent ?: Intent()
|
val data = receivedIntent ?: Intent()
|
||||||
data.action = OpenPgpApi.ACTION_GET_KEY_IDS
|
data.action = OpenPgpApi.ACTION_GET_KEY_IDS
|
||||||
api?.executeApiAsync(data, null, null) { result: Intent? ->
|
api?.executeApiAsync(data, null, null) { result: Intent? ->
|
||||||
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
|
||||||
OpenPgpApi.RESULT_CODE_SUCCESS -> {
|
RESULT_CODE_SUCCESS -> {
|
||||||
try {
|
try {
|
||||||
val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
|
val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
|
||||||
val keys = ids.map { it.toString() }.toSet()
|
val keys = ids.map { it.toString() }.toSet()
|
||||||
@@ -557,7 +557,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_KEY_ID)
|
RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_KEY_ID)
|
||||||
OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
|
RESULT_CODE_ERROR -> handleError(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,23 +580,23 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
Log.d(TAG, "onActivityResult resultCode: $resultCode")
|
Log.d(TAG, "onActivityResult resultCode: $resultCode")
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
setResult(AppCompatActivity.RESULT_CANCELED, null)
|
setResult(RESULT_CANCELED, null)
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// try again after user interaction
|
// try again after user interaction
|
||||||
if (resultCode == AppCompatActivity.RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
REQUEST_DECRYPT -> decryptAndVerify(data)
|
REQUEST_DECRYPT -> decryptAndVerify(data)
|
||||||
REQUEST_KEY_ID -> getKeyIds(data)
|
REQUEST_KEY_ID -> getKeyIds(data)
|
||||||
else -> {
|
else -> {
|
||||||
setResult(AppCompatActivity.RESULT_OK)
|
setResult(RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (resultCode == AppCompatActivity.RESULT_CANCELED) {
|
} else if (resultCode == RESULT_CANCELED) {
|
||||||
setResult(AppCompatActivity.RESULT_CANCELED, data)
|
setResult(RESULT_CANCELED, data)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,7 +786,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
|||||||
if (crypto_password_show != null) {
|
if (crypto_password_show != null) {
|
||||||
// clear password; if decrypt changed to encrypt layout via edit button, no need
|
// clear password; if decrypt changed to encrypt layout via edit button, no need
|
||||||
if (passwordEntry?.hotpIsIncremented() == false) {
|
if (passwordEntry?.hotpIsIncremented() == false) {
|
||||||
setResult(AppCompatActivity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
passwordEntry = null
|
passwordEntry = null
|
||||||
crypto_password_show.text = ""
|
crypto_password_show.text = ""
|
||||||
|
5
app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
Normal file
5
app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.zeapo.pwdstore.utils
|
||||||
|
|
||||||
|
fun String.splitLines(): Array<String> {
|
||||||
|
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
}
|
Reference in New Issue
Block a user