diff options
author | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-31 00:30:47 -0700 |
---|---|---|
committer | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-31 11:45:41 -0700 |
commit | 389ba153ecf015cbff36764e9c07f12ac3a0353d (patch) | |
tree | 284aa0a3355b15801ab5a1e171ff541e1ee850ba | |
parent | da2273924314181bb5e7e50fc52effc2f00aff1b (diff) | |
download | android-389ba153ecf015cbff36764e9c07f12ac3a0353d.tar.gz |
Changes to get Kotlin and Java autofill samples in sync.
Also fixed a crash when using datasetAuth. Impacted both samples.
Bug: 38182790
Test: manual
Change-Id: I1bfb00e3a22708d7bcbaade2aff3b00789499fe7
38 files changed, 451 insertions, 791 deletions
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml index 28d9c0b5..eb1f43c2 100644 --- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml @@ -32,7 +32,6 @@ android:taskAffinity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @@ -40,32 +39,17 @@ android:name=".app.LoginActivity" android:label="AF StandardLogin" android:taskAffinity=".LoginActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <activity android:name=".app.VirtualLoginActivity" android:label="AF VirtualLogin" android:taskAffinity=".VirtualLoginActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <activity android:name=".app.WelcomeActivity" /> <activity android:name=".app.CreditCardActivity" android:label="AF CreditCard" android:taskAffinity=".CreditCardActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <!-- Including launcher icon for Autofill Settings to convenience. @@ -78,7 +62,6 @@ android:taskAffinity=".SettingsActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.java index a82de4c8..7feb3936 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.java @@ -21,19 +21,12 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.Spinner; import com.example.android.autofillframework.R; public class CreditCardActivity extends AppCompatActivity { - private Spinner mCcExpirationDaySpinner; - private Spinner mCcExpirationMonthSpinner; - private Spinner mCcExpirationYearSpinner; - private Button mSubmitButton; - private Button mClearButton; - public static Intent getStartActivityIntent(Context context) { Intent intent = new Intent(context, CreditCardActivity.class); return intent; @@ -45,11 +38,9 @@ public class CreditCardActivity extends AppCompatActivity { setContentView(R.layout.credit_card_activity); - mSubmitButton = (Button) findViewById(R.id.submit); - mClearButton = (Button) findViewById(R.id.clear); - mCcExpirationDaySpinner = (Spinner) findViewById(R.id.expirationDay); - mCcExpirationMonthSpinner = (Spinner) findViewById(R.id.expirationMonth); - mCcExpirationYearSpinner = (Spinner) findViewById(R.id.expirationYear); + Spinner ccExpirationDaySpinner = findViewById(R.id.expirationDay); + Spinner ccExpirationMonthSpinner = findViewById(R.id.expirationMonth); + Spinner ccExpirationYearSpinner = findViewById(R.id.expirationYear); // Create an ArrayAdapter using the string array and a default spinner layout ArrayAdapter<CharSequence> dayAdapter = ArrayAdapter.createFromResource @@ -57,25 +48,25 @@ public class CreditCardActivity extends AppCompatActivity { // Specify the layout to use when the list of choices appears dayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // Apply the adapter to the spinner - mCcExpirationDaySpinner.setAdapter(dayAdapter); + ccExpirationDaySpinner.setAdapter(dayAdapter); ArrayAdapter<CharSequence> monthAdapter = ArrayAdapter.createFromResource (this, R.array.month_array, android.R.layout.simple_spinner_item); monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mCcExpirationMonthSpinner.setAdapter(monthAdapter); + ccExpirationMonthSpinner.setAdapter(monthAdapter); ArrayAdapter<CharSequence> yearAdapter = ArrayAdapter.createFromResource (this, R.array.year_array, android.R.layout.simple_spinner_item); yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mCcExpirationYearSpinner.setAdapter(yearAdapter); + ccExpirationYearSpinner.setAdapter(yearAdapter); - mSubmitButton.setOnClickListener(new View.OnClickListener() { + findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { submit(); } }); - mClearButton.setOnClickListener(new View.OnClickListener() { + findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { resetFields(); diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java index dc09de53..e2140c4f 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.java @@ -47,48 +47,35 @@ public class CustomVirtualView extends View { private static final String TAG = "CustomView"; - private static int nextId; - - private final ArrayList<Line> mLines = new ArrayList<>(); - private final SparseArray<Item> mItems = new SparseArray<>(); - private final AutofillManager mAfm; + private static final int TOP_MARGIN = 100; + private static final int LEFT_MARGIN = 100; + private static final int TEXT_HEIGHT = 90; + private static final int VERTICAL_GAP = 10; + private static final int LINE_HEIGHT = TEXT_HEIGHT + VERTICAL_GAP; + private static final int UNFOCUSED_COLOR = Color.BLACK; + private static final int FOCUSED_COLOR = Color.RED; + private static int sNextId; + + private final ArrayList<Line> mVirtualViewGroups = new ArrayList<>(); + private final SparseArray<Item> mVirtualViews = new SparseArray<>(); + private final AutofillManager mAutofillManager; private Line mFocusedLine; private Paint mTextPaint; - private int mTextHeight; - private int mTopMargin; - private int mLeftMargin; - private int mVerticalGap; - private int mLineLength; - private int mFocusedColor; - private int mUnfocusedColor; private Line mUsernameLine; private Line mPasswordLine; public CustomVirtualView(Context context, AttributeSet attrs) { super(context, attrs); - - mAfm = context.getSystemService(AutofillManager.class); - + mAutofillManager = context.getSystemService(AutofillManager.class); mTextPaint = new Paint(); - - mUnfocusedColor = Color.BLACK; - mFocusedColor = Color.RED; mTextPaint.setStyle(Style.FILL); - mTopMargin = 100; - mLeftMargin = 100; - mTextHeight = 90; - mVerticalGap = 10; - - mLineLength = mTextHeight + mVerticalGap; - mTextPaint.setTextSize(mTextHeight); + mTextPaint.setTextSize(TEXT_HEIGHT); mUsernameLine = addLine("usernameField", context.getString(R.string.username_label), new String[]{View.AUTOFILL_HINT_USERNAME}, " ", true); mPasswordLine = addLine("passwordField", context.getString(R.string.password_label), new String[]{View.AUTOFILL_HINT_PASSWORD}, " ", false); - - Log.d(TAG, "Text height: " + mTextHeight); } @Override @@ -99,19 +86,17 @@ public class CustomVirtualView extends View { // AutofillValues in the list. Log.d(TAG, "autoFill(): " + values); for (int i = 0; i < values.size(); i++) { - final int id = values.keyAt(i); - final AutofillValue value = values.valueAt(i); - final Item item = mItems.get(id); - if (item == null) { + int id = values.keyAt(i); + AutofillValue value = values.valueAt(i); + Item item = mVirtualViews.get(id); + if (item != null && item.editable) { + // Set the item's text to the text wrapped in the AutofillValue. + item.text = value.getTextValue(); + } else if (item == null) { Log.w(TAG, "No item for id " + id); - return; - } - if (!item.editable) { + } else { Log.w(TAG, "Item for id " + id + " is not editable: " + item); - return; } - // Set the item's text to the text wrapped in the AutofillValue. - item.text = value.getTextValue(); } postInvalidate(); } @@ -122,14 +107,14 @@ public class CustomVirtualView extends View { // Build a ViewStructure that will get passed to the AutofillService by the framework // when it is time to find autofill suggestions. structure.setClassName(getClass().getName()); - int childrenSize = mItems.size(); + int childrenSize = mVirtualViews.size(); Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = " + childrenSize + ", extras: " + bundleToString(structure.getExtras())); int index = structure.addChildCount(childrenSize); // Traverse through the view hierarchy, including virtual child views. For each view, we // need to set the relevant autofill metadata and add it to the ViewStructure. for (int i = 0; i < childrenSize; i++) { - Item item = mItems.valueAt(i); + Item item = mVirtualViews.valueAt(i); Log.d(TAG, "Adding new child at index " + index + ": " + item); ViewStructure child = structure.newChild(index); child.setAutofillId(structure, item.id); @@ -149,14 +134,14 @@ public class CustomVirtualView extends View { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - Log.d(TAG, "onDraw: " + mLines.size() + " lines; canvas:" + canvas); + Log.d(TAG, "onDraw: " + mVirtualViewGroups.size() + " lines; canvas:" + canvas); float x; - float y = mTopMargin + mLineLength; - for (int i = 0; i < mLines.size(); i++) { - x = mLeftMargin; - Line line = mLines.get(i); + float y = TOP_MARGIN + LINE_HEIGHT; + for (int i = 0; i < mVirtualViewGroups.size(); i++) { + x = LEFT_MARGIN; + Line line = mVirtualViewGroups.get(i); Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y); - mTextPaint.setColor(line.fieldTextItem.focused ? mFocusedColor : mUnfocusedColor); + mTextPaint.setColor(line.fieldTextItem.focused ? FOCUSED_COLOR : UNFOCUSED_COLOR); String readOnlyText = line.labelItem.text + ": ["; String writeText = line.fieldTextItem.text + "]"; // Paints the label first... @@ -164,23 +149,23 @@ public class CustomVirtualView extends View { // ...then paints the edit text and sets the proper boundary float deltaX = mTextPaint.measureText(readOnlyText); x += deltaX; - line.bounds.set((int) x, (int) (y - mLineLength), + line.bounds.set((int) x, (int) (y - LINE_HEIGHT), (int) (x + mTextPaint.measureText(writeText)), (int) y); Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds); canvas.drawText(writeText, x, y, mTextPaint); - y += mLineLength; + y += LINE_HEIGHT; } } @Override public boolean onTouchEvent(MotionEvent event) { int y = (int) event.getY(); - Log.d(TAG, "Touched: y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin); - int lowerY = mTopMargin; + Log.d(TAG, "Touched: y=" + y + ", range=" + LINE_HEIGHT + ", top=" + TOP_MARGIN); + int lowerY = TOP_MARGIN; int upperY = -1; - for (int i = 0; i < mLines.size(); i++) { - upperY = lowerY + mLineLength; - Line line = mLines.get(i); + for (int i = 0; i < mVirtualViewGroups.size(); i++) { + upperY = lowerY + LINE_HEIGHT; + Line line = mVirtualViewGroups.get(i); Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY); if (lowerY <= y && y <= upperY) { if (mFocusedLine != null) { @@ -193,7 +178,7 @@ public class CustomVirtualView extends View { invalidate(); break; } - lowerY += mLineLength; + lowerY += LINE_HEIGHT; } return super.onTouchEvent(event); } @@ -212,11 +197,12 @@ public class CustomVirtualView extends View { postInvalidate(); } - private Line addLine(String idEntry, String label, String[] hints, String text, boolean sanitized) { + private Line addLine(String idEntry, String label, String[] hints, String text, + boolean sanitized) { Line line = new Line(idEntry, label, hints, text, sanitized); - mLines.add(line); - mItems.put(line.labelItem.id, line.labelItem); - mItems.put(line.fieldTextItem.id, line.fieldTextItem); + mVirtualViewGroups.add(line); + mVirtualViews.put(line.labelItem.id, line.labelItem); + mVirtualViews.put(line.fieldTextItem.id, line.fieldTextItem); return line; } @@ -230,7 +216,8 @@ public class CustomVirtualView extends View { private CharSequence text; private boolean focused = false; - Item(Line line, int id, String[] hints, int type, CharSequence text, boolean editable, boolean sanitized) { + Item(Line line, int id, String[] hints, int type, CharSequence text, boolean editable, + boolean sanitized) { this.line = line; this.id = id; this.text = text; @@ -261,27 +248,30 @@ public class CustomVirtualView extends View { private Line(String idEntry, String label, String[] hints, String text, boolean sanitized) { this.idEntry = idEntry; - this.labelItem = new Item(this, ++nextId, null, AUTOFILL_TYPE_NONE, label, false, true); - this.fieldTextItem = new Item(this, ++nextId, hints, AUTOFILL_TYPE_TEXT, text, true, sanitized); + this.labelItem = new Item(this, ++sNextId, null, AUTOFILL_TYPE_NONE, label, + false, true); + this.fieldTextItem = new Item(this, ++sNextId, hints, AUTOFILL_TYPE_TEXT, text, + true, sanitized); } void changeFocus(boolean focused) { fieldTextItem.focused = focused; if (focused) { - final Rect absBounds = getAbsCoordinates(); + Rect absBounds = getAbsCoordinates(); Log.d(TAG, "focus gained on " + fieldTextItem.id + "; absBounds=" + absBounds); - mAfm.notifyViewEntered(CustomVirtualView.this, fieldTextItem.id, absBounds); + mAutofillManager.notifyViewEntered(CustomVirtualView.this, fieldTextItem.id, + absBounds); } else { Log.d(TAG, "focus lost on " + fieldTextItem.id); - mAfm.notifyViewExited(CustomVirtualView.this, fieldTextItem.id); + mAutofillManager.notifyViewExited(CustomVirtualView.this, fieldTextItem.id); } } private Rect getAbsCoordinates() { // Must offset the boundaries so they're relative to the CustomView. - final int offset[] = new int[2]; + int offset[] = new int[2]; getLocationOnScreen(offset); - final Rect absBounds = new Rect(bounds.left + offset[0], + Rect absBounds = new Rect(bounds.left + offset[0], bounds.top + offset[1], bounds.right + offset[0], bounds.bottom + offset[1]); Log.v(TAG, "getAbsCoordinates() for " + fieldTextItem.id + ": bounds=" + bounds diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java index 3ec87da0..ec7ab581 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.java @@ -20,7 +20,6 @@ import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; -import android.widget.Button; import android.widget.EditText; import android.widget.Toast; @@ -30,8 +29,6 @@ public class LoginActivity extends AppCompatActivity { private EditText mUsernameEditText; private EditText mPasswordEditText; - private Button mLoginButton; - private Button mClearButton; public static Intent getStartActivityIntent(Context context) { Intent intent = new Intent(context, LoginActivity.class); @@ -43,18 +40,15 @@ public class LoginActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.login_activity); - - mLoginButton = (Button) findViewById(R.id.login); - mClearButton = (Button) findViewById(R.id.clear); - mUsernameEditText = (EditText) findViewById(R.id.usernameField); - mPasswordEditText = (EditText) findViewById(R.id.passwordField); - mLoginButton.setOnClickListener(new View.OnClickListener() { + mUsernameEditText = findViewById(R.id.usernameField); + mPasswordEditText = findViewById(R.id.passwordField); + findViewById(R.id.login).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { login(); } }); - mClearButton.setOnClickListener(new View.OnClickListener() { + findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { resetFields(); diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.java index 4b27010b..4e7a6caa 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.java @@ -15,7 +15,6 @@ */ package com.example.android.autofillframework.app; -import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.java index 5a4a4f4e..0c787090 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.java @@ -15,7 +15,6 @@ */ package com.example.android.autofillframework.app; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -70,6 +69,7 @@ public class VirtualLoginActivity extends AppCompatActivity { if (valid) { Intent intent = WelcomeActivity.getStartActivityIntent(VirtualLoginActivity.this); startActivity(intent); + finish(); } else { Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show(); } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java index 768b2ee3..d170c9e3 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java @@ -29,12 +29,11 @@ import android.text.Editable; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; -import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import com.example.android.autofillframework.R; -import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; +import com.example.android.autofillframework.service.datasource.SharedPrefsAutofillRepository; import com.example.android.autofillframework.service.model.AutofillFieldsCollection; import com.example.android.autofillframework.service.model.ClientFormData; import com.example.android.autofillframework.service.settings.MyPreferences; @@ -58,8 +57,6 @@ public class AuthActivity extends Activity { private static int sDatasetPendingIntentId = 0; private EditText mMasterPassword; - private Button mCancel; - private Button mLogin; private Intent mReplyIntent; static IntentSender getAuthIntentSenderForResponse(Context context) { @@ -79,20 +76,16 @@ public class AuthActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.auth_activity); - mCancel = findViewById(R.id.cancel); - mLogin = findViewById(R.id.login); mMasterPassword = findViewById(R.id.master_password); - mLogin.setOnClickListener(new OnClickListener() { + findViewById(R.id.login).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { login(); } }); - - mCancel.setOnClickListener(new OnClickListener() { + findViewById(R.id.cancel).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onFailure(); @@ -133,20 +126,20 @@ public class AuthActivity extends Activity { boolean forResponse = intent.getBooleanExtra(EXTRA_FOR_RESPONSE, true); AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); StructureParser parser = new StructureParser(structure); - parser.parse(); + parser.parseForFill(); AutofillFieldsCollection autofillFields = parser.getAutofillFields(); - int saveTypes = parser.getSaveTypes(); + int saveTypes = autofillFields.getSaveType(); mReplyIntent = new Intent(); HashMap<String, ClientFormData> clientFormDataMap = - LocalAutofillRepository.getInstance(this).getClientFormData + SharedPrefsAutofillRepository.getInstance(this).getClientFormData (autofillFields.getFocusedHints(), autofillFields.getAllHints()); if (forResponse) { setResponseIntent(AutofillHelper.newResponse - (this, false, autofillFields, saveTypes, clientFormDataMap)); + (this, false, autofillFields, clientFormDataMap)); } else { String datasetName = intent.getStringExtra(EXTRA_DATASET_NAME); setDatasetIntent(AutofillHelper.newDataset - (this, autofillFields, clientFormDataMap.get(datasetName))); + (this, autofillFields, clientFormDataMap.get(datasetName), false)); } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java index 460729e6..f460538a 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java @@ -43,15 +43,21 @@ public final class AutofillHelper { * client View. */ public static Dataset newDataset(Context context, - AutofillFieldsCollection autofillFields, ClientFormData clientFormData) { - Dataset.Builder datasetBuilder = new Dataset.Builder - (newRemoteViews(context.getPackageName(), clientFormData.getDatasetName())); - boolean setValueAtLeastOnce = clientFormData.applyToFields(autofillFields, datasetBuilder); - if (setValueAtLeastOnce) { - return datasetBuilder.build(); - } else { - return null; + AutofillFieldsCollection autofillFields, ClientFormData clientFormData, boolean datasetAuth) { + String datasetName = clientFormData.getDatasetName(); + if (datasetName != null) { + Dataset.Builder datasetBuilder = new Dataset.Builder + (newRemoteViews(context.getPackageName(), datasetName)); + if (datasetAuth) { + IntentSender sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName); + datasetBuilder.setAuthentication(sender); + } + boolean setValueAtLeastOnce = clientFormData.applyToFields(autofillFields, datasetBuilder); + if (setValueAtLeastOnce) { + return datasetBuilder.build(); + } } + return null; } public static RemoteViews newRemoteViews(String packageName, String remoteViewsText) { @@ -65,32 +71,25 @@ public final class AutofillHelper { * be sent back to the client View. */ public static FillResponse newResponse(Context context, - boolean datasetAuth, AutofillFieldsCollection autofillFields, int saveType, + boolean datasetAuth, AutofillFieldsCollection autofillFields, HashMap<String, ClientFormData> clientFormDataMap) { FillResponse.Builder responseBuilder = new FillResponse.Builder(); if (clientFormDataMap != null) { Set<String> datasetNames = clientFormDataMap.keySet(); for (String datasetName : datasetNames) { ClientFormData clientFormData = clientFormDataMap.get(datasetName); - if (datasetAuth) { - Dataset.Builder datasetBuilder = - new Dataset.Builder(newRemoteViews - (context.getPackageName(), clientFormData.getDatasetName())); - IntentSender sender = AuthActivity - .getAuthIntentSenderForDataset(context, clientFormData.getDatasetName()); - datasetBuilder.setAuthentication(sender); - responseBuilder.addDataset(datasetBuilder.build()); - } else { - Dataset dataset = newDataset(context, autofillFields, clientFormData); + if (clientFormData != null) { + Dataset dataset = newDataset(context, autofillFields, clientFormData, datasetAuth); if (dataset != null) { responseBuilder.addDataset(dataset); } } } } - if (saveType != 0) { + if (autofillFields.getSaveType() != 0) { AutofillId[] autofillIds = autofillFields.getAutofillIds(); - responseBuilder.setSaveInfo(new SaveInfo.Builder(saveType, autofillIds).build()); + responseBuilder.setSaveInfo + (new SaveInfo.Builder(autofillFields.getSaveType(), autofillIds).build()); return responseBuilder.build(); } else { Log.d(TAG, "These fields are not meant to be saved by autofill."); diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java index 61e42050..ead8b963 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java @@ -30,7 +30,7 @@ import android.util.Log; import android.widget.RemoteViews; import com.example.android.autofillframework.R; -import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; +import com.example.android.autofillframework.service.datasource.SharedPrefsAutofillRepository; import com.example.android.autofillframework.service.model.AutofillFieldsCollection; import com.example.android.autofillframework.service.model.ClientFormData; import com.example.android.autofillframework.service.settings.MyPreferences; @@ -77,10 +77,8 @@ public class MyAutofillService extends AutofillService { }); // Parse AutoFill data in Activity StructureParser parser = new StructureParser(structure); - parser.parse(); + parser.parseForFill(); AutofillFieldsCollection autofillFields = parser.getAutofillFields(); - int saveTypes = parser.getSaveTypes(); - FillResponse.Builder responseBuilder = new FillResponse.Builder(); // Check user's settings for authenticating Responses and Datasets. boolean responseAuth = MyPreferences.getInstance(this).isResponseAuth(); @@ -96,10 +94,10 @@ public class MyAutofillService extends AutofillService { } else { boolean datasetAuth = MyPreferences.getInstance(this).isDatasetAuth(); HashMap<String, ClientFormData> clientFormDataMap = - LocalAutofillRepository.getInstance(this).getClientFormData + SharedPrefsAutofillRepository.getInstance(this).getClientFormData (autofillFields.getFocusedHints(), autofillFields.getAllHints()); FillResponse response = AutofillHelper.newResponse - (this, datasetAuth, autofillFields, saveTypes, clientFormDataMap); + (this, datasetAuth, autofillFields, clientFormDataMap); callback.onSuccess(response); } } @@ -111,9 +109,9 @@ public class MyAutofillService extends AutofillService { final Bundle data = request.getClientState(); Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data)); StructureParser parser = new StructureParser(structure); - parser.parse(); + parser.parseForSave(); ClientFormData clientFormData = parser.getClientFormData(); - LocalAutofillRepository.getInstance(this).saveClientFormData(clientFormData); + SharedPrefsAutofillRepository.getInstance(this).saveClientFormData(clientFormData); } @Override diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java index b6294449..6d81a59b 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java @@ -23,14 +23,14 @@ import android.util.Log; import com.example.android.autofillframework.service.model.AutofillField; import com.example.android.autofillframework.service.model.AutofillFieldsCollection; import com.example.android.autofillframework.service.model.ClientFormData; -import com.example.android.autofillframework.service.model.SavedAutofillValue; +import com.example.android.autofillframework.service.model.SavableAutofillData; import static com.example.android.autofillframework.CommonUtil.TAG; /** * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an - * AssistStructure from the client Activity, representing its View hierarchy. In this - * sample, it parses the hierarchy and records + * AssistStructure from the client Activity, representing its View hierarchy. In this sample, it + * parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. */ final class StructureParser { private final AutofillFieldsCollection mAutofillFields = new AutofillFieldsCollection(); @@ -42,31 +42,42 @@ final class StructureParser { } + public void parseForFill() { + parse(true); + } + + public void parseForSave() { + parse(false); + } + /** * Traverse AssistStructure and add ViewNode metadata to a flat list. */ - void parse() { + private void parse(boolean forFill) { Log.d(TAG, "Parsing structure for " + mStructure.getActivityComponent()); int nodes = mStructure.getWindowNodeCount(); mClientFormData = new ClientFormData(); for (int i = 0; i < nodes; i++) { WindowNode node = mStructure.getWindowNodeAt(i); ViewNode view = node.getRootViewNode(); - parseLocked(view); + parseLocked(forFill, view); } } - private void parseLocked(ViewNode viewNode) { + private void parseLocked(boolean forFill, ViewNode viewNode) { if (viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) { //TODO check to make sure hints are supported by service. - mAutofillFields.add(new AutofillField(viewNode)); - mClientFormData - .set(viewNode.getAutofillHints(), SavedAutofillValue.fromViewNode(viewNode)); + if (forFill) { + mAutofillFields.add(new AutofillField(viewNode)); + } else { + mClientFormData.setAutofillValuesForHints + (viewNode.getAutofillHints(), new SavableAutofillData(viewNode)); + } } int childrenSize = viewNode.getChildCount(); if (childrenSize > 0) { for (int i = 0; i < childrenSize; i++) { - parseLocked(viewNode.getChildAt(i)); + parseLocked(forFill, viewNode.getChildAt(i)); } } } @@ -75,10 +86,6 @@ final class StructureParser { return mAutofillFields; } - public int getSaveTypes() { - return mAutofillFields.getSaveType(); - } - public ClientFormData getClientFormData() { return mClientFormData; } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.java index 8336fe1e..44758fcb 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.java @@ -20,37 +20,34 @@ import android.content.SharedPreferences; import android.util.ArraySet; import com.example.android.autofillframework.service.model.ClientFormData; - -import org.json.JSONException; -import org.json.JSONObject; +import com.google.gson.Gson; import java.util.HashMap; import java.util.List; import java.util.Set; /** - * Singleton autofill data repository, that stores autofill fields to SharedPreferences. - * DISCLAIMER, you should not store sensitive fields like user data unencrypted. This is only done - * here for simplicity and learning purposes. + * Singleton autofill data repository that stores autofill fields to SharedPreferences. + * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done + * here only for simplicity and learning purposes. */ -public class LocalAutofillRepository implements AutofillRepository { +public class SharedPrefsAutofillRepository implements AutofillRepository { private static final String SHARED_PREF_KEY = "com.example.android.autofillframework.service"; private static final String CLIENT_FORM_DATA_KEY = "loginCredentialDatasets"; private static final String DATASET_NUMBER_KEY = "datasetNumber"; - private static LocalAutofillRepository sInstance; + private static SharedPrefsAutofillRepository sInstance; private final SharedPreferences mPrefs; - // TODO prepend with autofill data set in Settings. - private LocalAutofillRepository(Context context) { + private SharedPrefsAutofillRepository(Context context) { mPrefs = context.getApplicationContext() .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); } - public static LocalAutofillRepository getInstance(Context context) { + public static SharedPrefsAutofillRepository getInstance(Context context) { if (sInstance == null) { - sInstance = new LocalAutofillRepository(context); + sInstance = new SharedPrefsAutofillRepository(context); } return sInstance; } @@ -58,40 +55,37 @@ public class LocalAutofillRepository implements AutofillRepository { @Override public HashMap<String, ClientFormData> getClientFormData(List<String> focusedAutofillHints, List<String> allAutofillHints) { - try { - // TODO use sqlite instead. - boolean hasDataForFocusedAutofillHints = false; - HashMap<String, ClientFormData> clientFormDataMap = new HashMap<>(); - Set<String> clientFormDataStringSet = getAllAutofillDataStringSet(); - for (String clientFormDataString : clientFormDataStringSet) { - ClientFormData clientFormData = ClientFormData - .fromJson(new JSONObject(clientFormDataString)); - if (clientFormData != null) { - if (clientFormData.helpsWithHints(focusedAutofillHints)) { - hasDataForFocusedAutofillHints = true; - } - if (clientFormData.helpsWithHints(allAutofillHints)) { - clientFormDataMap.put(clientFormData.getDatasetName(), clientFormData); - } + boolean hasDataForFocusedAutofillHints = false; + HashMap<String, ClientFormData> clientFormDataMap = new HashMap<>(); + Set<String> clientFormDataStringSet = getAllAutofillDataStringSet(); + for (String clientFormDataString : clientFormDataStringSet) { + ClientFormData clientFormData = new Gson().fromJson(clientFormDataString, ClientFormData.class); + if (clientFormData != null) { + if (clientFormData.helpsWithHints(focusedAutofillHints)) { + // Saved data has data relevant to at least 1 of the hints associated with the + // View in focus. + hasDataForFocusedAutofillHints = true; + } + if (clientFormData.helpsWithHints(allAutofillHints)) { + // Saved data has data relevant to at least 1 of these hints associated with any + // of the Views in the hierarchy. + clientFormDataMap.put(clientFormData.getDatasetName(), clientFormData); } } - if (hasDataForFocusedAutofillHints) { - return clientFormDataMap; - } else { - return null; - } - } catch (JSONException e) { + } + if (hasDataForFocusedAutofillHints) { + return clientFormDataMap; + } else { return null; } } @Override public void saveClientFormData(ClientFormData clientFormData) { - //TODO use sqlite instead. String datasetName = "dataset-" + getDatasetNumber(); clientFormData.setDatasetName(datasetName); Set<String> allAutofillData = getAllAutofillDataStringSet(); - allAutofillData.add(clientFormData.toJson().toString()); + allAutofillData.add(new Gson().toJson(clientFormData)); saveAllAutofillDataStringSet(allAutofillData); incrementDatasetNumber(); } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java index 4d4de2bc..710112e1 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java @@ -15,39 +15,38 @@ */ package com.example.android.autofillframework.service.model; -import android.app.assist.AssistStructure; +import android.app.assist.AssistStructure.ViewNode; import android.service.autofill.SaveInfo; import android.view.View; import android.view.autofill.AutofillId; -import android.view.autofill.AutofillValue; /** - * Class that represents a field that can be autofilled. It will contain a description - * (what type data the field holds), an AutoFillId (an ID unique to the rest of the ViewStructure), - * and a value (what data is currently in the field). + * A stripped down version of a {@link ViewNode} that contains only autofill-relevant metadata. It + * also contains a {@code mSaveType} flag that is calculated based on the {@link ViewNode}]'s + * autofill hints. */ public class AutofillField { private int mSaveType = 0; - private String[] mHints; - private AutofillId mId; + private String[] mAutofillHints; + private AutofillId mAutofillId; private int mAutofillType; private String[] mAutofillOptions; private boolean mFocused; - public AutofillField(AssistStructure.ViewNode view) { - mId = view.getAutofillId(); - setHints(view.getAutofillHints()); + public AutofillField(ViewNode view) { + mAutofillId = view.getAutofillId(); mAutofillType = view.getAutofillType(); mAutofillOptions = view.getAutofillOptions(); mFocused = view.isFocused(); + setHints(view.getAutofillHints()); } public String[] getHints() { - return mHints; + return mAutofillHints; } public void setHints(String[] hints) { - mHints = hints; + mAutofillHints = hints; updateSaveTypeFromHints(); } @@ -56,17 +55,17 @@ public class AutofillField { } public AutofillId getId() { - return mId; - } - - public void setId(AutofillId id) { - mId = id; + return mAutofillId; } public int getAutofillType() { return mAutofillType; } + /** + * When the {@link ViewNode} is a list that the user needs to choose a string from (i.e. a + * spinner), this is called to return the index of a specific item in the list. + */ public int getAutofillOptionIndex(String value) { for (int i = 0; i < mAutofillOptions.length; i++) { if (mAutofillOptions[i].equals(value)) { @@ -82,10 +81,10 @@ public class AutofillField { private void updateSaveTypeFromHints() { mSaveType = 0; - if (mHints == null) { + if (mAutofillHints == null) { return; } - for (String hint : mHints) { + for (String hint : mAutofillHints) { switch (hint) { case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE: case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY: diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java index 0354b989..a346f350 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java @@ -22,18 +22,22 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +/** + * Data structure that stores a collection of {@code AutofillField}s. Contains all of the client's + * {@code View} hierarchy autofill-relevant metadata. + */ public final class AutofillFieldsCollection { private final List<AutofillId> mAutofillIds = new ArrayList<>(); private final HashMap<String, List<AutofillField>> mAutofillHintsToFieldsMap = new HashMap<>(); private final List<String> mAllAutofillHints = new ArrayList<>(); private final List<String> mFocusedAutofillHints = new ArrayList<>(); - private int size = 0; + private int mSize = 0; private int mSaveType = 0; public void add(AutofillField autofillField) { mSaveType |= autofillField.getSaveType(); - size++; + mSize++; mAutofillIds.add(autofillField.getId()); List<String> hintsList = Arrays.asList(autofillField.getHints()); mAllAutofillHints.addAll(hintsList); @@ -53,7 +57,7 @@ public final class AutofillFieldsCollection { } public AutofillId[] getAutofillIds() { - return mAutofillIds.toArray(new AutofillId[size]); + return mAutofillIds.toArray(new AutofillId[mSize]); } public List<AutofillField> getFieldsForHint(String hint) { diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java index 3658519e..15b5bea3 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java @@ -22,78 +22,48 @@ import android.view.View; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; -import org.json.JSONException; -import org.json.JSONObject; - import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Set; + +import static com.example.android.autofillframework.CommonUtil.TAG; /** * ClientFormData is the model that holds all of the data on a client app's page, plus the dataset * name associated with it. */ public final class ClientFormData { - private static final String TAG = "ClientFormData"; - private final HashMap<String, SavedAutofillValue> hintMap; - private String datasetName; + private final HashMap<String, SavableAutofillData> mHintMap; + private String mDatasetName; public ClientFormData() { - this(null, new HashMap<String, SavedAutofillValue>()); - } - - public ClientFormData(String datasetName, HashMap<String, SavedAutofillValue> hintMap) { - this.hintMap = hintMap; - this.datasetName = datasetName; + this(null, new HashMap<String, SavableAutofillData>()); } - public static ClientFormData fromJson(JSONObject jsonObject) { - HashMap<String, SavedAutofillValue> hintMap = new HashMap<>(); - try { - String datasetName = jsonObject.has("datasetName") ? - jsonObject.getString("datasetName") : null; - JSONObject valuesJson = jsonObject.getJSONObject("values"); - Iterator<String> hints = valuesJson.keys(); - while (hints.hasNext()) { - String hint = hints.next(); - JSONObject valueAsJson = valuesJson - .getJSONObject(hint); - if (valueAsJson != null) { - SavedAutofillValue savedAutofillValue = SavedAutofillValue.fromJson(valueAsJson); - hintMap.put(hint, savedAutofillValue); - } - } - return new ClientFormData(datasetName, hintMap); - } catch (JSONException e) { - Log.d(TAG, e.getMessage()); - return null; - } + public ClientFormData(String datasetName, HashMap<String, SavableAutofillData> hintMap) { + mHintMap = hintMap; + mDatasetName = datasetName; } /** * Returns the name of the {@link Dataset}. */ public String getDatasetName() { - return this.datasetName; + return mDatasetName; } /** * Sets the {@link Dataset} name. */ public void setDatasetName(String datasetName) { - this.datasetName = datasetName; + mDatasetName = datasetName; } /** * Sets values for a list of hints. */ - public void set(@NonNull String[] autofillHints, @NonNull SavedAutofillValue autofillValue) { - if (autofillHints.length < 1) { - return; - } + public void setAutofillValuesForHints(@NonNull String[] autofillHints, @NonNull SavableAutofillData autofillValue) { for (int i = 0; i < autofillHints.length; i++) { - hintMap.put(autofillHints[i], autofillValue); + mHintMap.put(autofillHints[i], autofillValue); } } @@ -112,8 +82,8 @@ public final class ClientFormData { continue; } for (int autofillFieldIndex = 0; autofillFieldIndex < autofillFields.size(); autofillFieldIndex++) { - SavedAutofillValue savedAutofillValue = hintMap.get(hint); - if (savedAutofillValue == null) { + SavableAutofillData savableAutofillData = mHintMap.get(hint); + if (savableAutofillData == null) { continue; } AutofillField autofillField = autofillFields.get(autofillFieldIndex); @@ -121,29 +91,29 @@ public final class ClientFormData { int autofillType = autofillField.getAutofillType(); switch (autofillType) { case View.AUTOFILL_TYPE_LIST: - int listValue = autofillField.getAutofillOptionIndex(savedAutofillValue.getTextValue()); + int listValue = autofillField.getAutofillOptionIndex(savableAutofillData.getTextValue()); if (listValue != -1) { datasetBuilder.setValue(autofillId, AutofillValue.forList(listValue)); setValueAtLeastOnce = true; } break; case View.AUTOFILL_TYPE_DATE: - long dateValue = savedAutofillValue.getDateValue(); - if (dateValue != -1) { + Long dateValue = savableAutofillData.getDateValue(); + if (dateValue != null) { datasetBuilder.setValue(autofillId, AutofillValue.forDate(dateValue)); setValueAtLeastOnce = true; } break; case View.AUTOFILL_TYPE_TEXT: - String textValue = savedAutofillValue.getTextValue(); + String textValue = savableAutofillData.getTextValue(); if (textValue != null) { datasetBuilder.setValue(autofillId, AutofillValue.forText(textValue)); setValueAtLeastOnce = true; } break; case View.AUTOFILL_TYPE_TOGGLE: - if (savedAutofillValue.hasToggleValue()) { - boolean toggleValue = savedAutofillValue.getToggleValue(); + Boolean toggleValue = savableAutofillData.getToggleValue(); + if (toggleValue != null) { datasetBuilder.setValue(autofillId, AutofillValue.forToggle(toggleValue)); setValueAtLeastOnce = true; } @@ -158,27 +128,10 @@ public final class ClientFormData { return setValueAtLeastOnce; } - public JSONObject toJson() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("datasetName", datasetName != null ? datasetName : JSONObject.NULL); - JSONObject jsonValues = new JSONObject(); - Set<String> hints = hintMap.keySet(); - for (String hint : hints) { - SavedAutofillValue value = hintMap.get(hint); - jsonValues.put(hint, value != null ? value.toJson() : JSONObject.NULL); - } - jsonObject.put("values", jsonValues); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - } - return jsonObject; - } - public boolean helpsWithHints(List<String> autofillHints) { for (int i = 0; i < autofillHints.size(); i++) { String autofillHint = autofillHints.get(i); - if (hintMap.get(autofillHint) != null && !hintMap.get(autofillHint).isNull()) { + if (mHintMap.get(autofillHint) != null && !mHintMap.get(autofillHint).isNull()) { return true; } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavableAutofillData.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavableAutofillData.java new file mode 100644 index 00000000..e706511e --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavableAutofillData.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.autofillframework.service.model; + +import android.app.assist.AssistStructure; +import android.view.autofill.AutofillValue; + +/** + * JSON serializable data class containing the same data as an {@link AutofillValue}. + */ +public class SavableAutofillData { + private String mTextValue = null; + private Long mDateValue = null; + private Boolean mToggleValue = null; + + public SavableAutofillData(AssistStructure.ViewNode viewNode) { + AutofillValue autofillValue = viewNode.getAutofillValue(); + if (autofillValue != null) { + if (autofillValue.isList()) { + String[] autofillOptions = viewNode.getAutofillOptions(); + int index = autofillValue.getListValue(); + if (autofillOptions != null && autofillOptions.length > 0) { + mTextValue = autofillOptions[index]; + } + } else if (autofillValue.isDate()) { + mDateValue = autofillValue.getDateValue(); + } else if (autofillValue.isText()) { + // Using toString of AutofillValue.getTextValue in order to save it to + // SharedPreferences. + mTextValue = autofillValue.getTextValue().toString(); + } + } + } + + public String getTextValue() { + return mTextValue; + } + + public Long getDateValue() { + return mDateValue; + } + + public Boolean getToggleValue() { + return mToggleValue; + } + + public boolean isNull() { + return mTextValue == null && mDateValue == null && mToggleValue == null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SavableAutofillData that = (SavableAutofillData) o; + + if (mTextValue != null ? !mTextValue.equals(that.mTextValue) : that.mTextValue != null) + return false; + if (mDateValue != null ? !mDateValue.equals(that.mDateValue) : that.mDateValue != null) + return false; + return mToggleValue != null ? mToggleValue.equals(that.mToggleValue) : that.mToggleValue == null; + } + + @Override + public int hashCode() { + int result = mTextValue != null ? mTextValue.hashCode() : 0; + result = 31 * result + (mDateValue != null ? mDateValue.hashCode() : 0); + result = 31 * result + (mToggleValue != null ? mToggleValue.hashCode() : 0); + return result; + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java deleted file mode 100644 index 73e0c81e..00000000 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.android.autofillframework.service.model; - -import android.app.assist.AssistStructure; -import android.util.Log; -import android.view.autofill.AutofillValue; - -import org.json.JSONException; -import org.json.JSONObject; - -public class SavedAutofillValue { - private static final String TAG = "SavedAutofillValue"; - private String textValue = null; - private Long dateValue = -1L; - private Boolean toggleValue = false; - private boolean hasToggleValue = false; - - public static SavedAutofillValue fromJson(JSONObject jsonObject) { - if (jsonObject == null) { - return null; - } - try { - SavedAutofillValue savedAutofillValue = new SavedAutofillValue(); - - savedAutofillValue.textValue = - !jsonObject.isNull("textValue") ? jsonObject.getString("textValue") : null; - savedAutofillValue.dateValue = - !jsonObject.isNull("dateValue") ? jsonObject.getLong("dateValue") : null; - savedAutofillValue.setToggleValue - (!jsonObject.isNull("toggleValue") ? jsonObject.getBoolean("toggleValue") : null); - return savedAutofillValue; - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return null; - } - } - - public static SavedAutofillValue fromViewNode(AssistStructure.ViewNode viewNode) { - SavedAutofillValue savedAutofillValue = new SavedAutofillValue(); - AutofillValue autofillValue = viewNode.getAutofillValue(); - if (autofillValue != null) { - if (autofillValue.isList()) { - String[] autofillOptions = viewNode.getAutofillOptions(); - int index = autofillValue.getListValue(); - if (autofillOptions != null && autofillOptions.length > 0) { - savedAutofillValue.textValue = autofillOptions[index]; - } - } else if (autofillValue.isDate()) { - savedAutofillValue.dateValue = autofillValue.getDateValue(); - } else if (autofillValue.isText()) { - // Using toString of AutofillValue.getTextValue in order to save it to - // SharedPreferences. - savedAutofillValue.textValue = autofillValue.getTextValue().toString(); - } - } - return savedAutofillValue; - } - - public JSONObject toJson() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("textValue", textValue != null ? textValue : JSONObject.NULL); - jsonObject.put("dateValue", dateValue != null ? dateValue : JSONObject.NULL); - jsonObject.put("toggleValue", toggleValue != null ? toggleValue : JSONObject.NULL); - return jsonObject; - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); - return null; - } - } - - public String getTextValue() { - return textValue; - } - - public long getDateValue() { - return dateValue; - } - - - public boolean getToggleValue() { - return toggleValue; - } - - public void setToggleValue(Boolean toggleValue) { - this.toggleValue = toggleValue; - hasToggleValue = toggleValue != null; - } - - - public boolean isNull() { - return textValue == null && dateValue == -1L && !hasToggleValue; - } - - public boolean hasToggleValue() { - return hasToggleValue; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SavedAutofillValue that = (SavedAutofillValue) o; - - if (textValue != null ? !textValue.equals(that.textValue) : that.textValue != null) - return false; - if (dateValue != null ? !dateValue.equals(that.dateValue) : that.dateValue != null) - return false; - return toggleValue != null ? toggleValue.equals(that.toggleValue) : that.toggleValue == null; - - } - - @Override - public int hashCode() { - int result = textValue != null ? textValue.hashCode() : 0; - result = 31 * result + (dateValue != null ? dateValue.hashCode() : 0); - result = 31 * result + (toggleValue != null ? toggleValue.hashCode() : 0); - return result; - } -} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java index 6387d36b..ee461b0e 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java @@ -29,14 +29,13 @@ import android.widget.Switch; import android.widget.TextView; import com.example.android.autofillframework.R; -import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; +import com.example.android.autofillframework.service.datasource.SharedPrefsAutofillRepository; public class SettingsActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.settings_activity); final MyPreferences preferences = MyPreferences.getInstance(this); setupSettingsSwitch(R.id.settings_auth_responses_container, @@ -68,7 +67,6 @@ public class SettingsActivity extends AppCompatActivity { buildClearDataDialog().show(); } }); - setupSettingsButton(R.id.settings_auth_credentials_container, R.id.settings_auth_credentials_label, R.id.settings_auth_credentials_icon, @@ -92,7 +90,7 @@ public class SettingsActivity extends AppCompatActivity { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - LocalAutofillRepository.getInstance + SharedPrefsAutofillRepository.getInstance (SettingsActivity.this).clear(); MyPreferences.getInstance(SettingsActivity.this) .clearCredentials(); diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/AndroidManifest.xml index 28d9c0b5..eb1f43c2 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/AndroidManifest.xml @@ -32,7 +32,6 @@ android:taskAffinity=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @@ -40,32 +39,17 @@ android:name=".app.LoginActivity" android:label="AF StandardLogin" android:taskAffinity=".LoginActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <activity android:name=".app.VirtualLoginActivity" android:label="AF VirtualLogin" android:taskAffinity=".VirtualLoginActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <activity android:name=".app.WelcomeActivity" /> <activity android:name=".app.CreditCardActivity" android:label="AF CreditCard" android:taskAffinity=".CreditCardActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> </activity> <!-- Including launcher icon for Autofill Settings to convenience. @@ -78,7 +62,6 @@ android:taskAffinity=".SettingsActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.kt index 86dc9b25..bef16afe 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.kt @@ -70,20 +70,21 @@ class CustomVirtualView(context: Context, attrs: AttributeSet) : View(context, a } override fun autofill(values: SparseArray<AutofillValue>) { - // User has just selected a Dataset from the list of Autofill suggestions and the Dataset's - // AutofillValue gets passed into this method. + // User has just selected a Dataset from the list of autofill suggestions. + // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant + // to fill a specific autofillable view. Now we have to update the UI based on the + // AutofillValues in the list. Log.d(TAG, "autofill(): " + values) for (i in 0..values.size() - 1) { val id = values.keyAt(i) val value = values.valueAt(i) - - mItems[id]?.let { - if (!it.editable) { - Log.w(TAG, "Item for autofillId $id is not editable: $it") - return@autofill + mItems[id]?.let { item -> + if (item.editable) { + // Set the item's text to the text wrapped in the AutofillValue. + item.text = value.textValue + } else { + Log.w(TAG, "Item for autofillId $id is not editable: $item") } - // Set the item's text to the text wrapped in the AutofillValue. - it.text = value.textValue } } postInvalidate() diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.kt index c6eb721f..52081afd 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.kt @@ -51,6 +51,7 @@ class VirtualLoginActivity : AppCompatActivity() { if (valid) { val intent = WelcomeActivity.getStartActivityIntent(this@VirtualLoginActivity) startActivity(intent) + finish() } else { Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show() } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.kt index 12be45bb..d102a7e9 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.kt @@ -97,8 +97,8 @@ class AuthActivity : Activity() { } else { val datasetName = intent.getStringExtra(EXTRA_DATASET_NAME) clientFormDataMap?.let { - it[datasetName]?.let { - AutofillHelper.newDataset(this, autofillFields, it)?.let(this::setDatasetIntent) + it[datasetName]?.let { clientFormData -> + AutofillHelper.newDataset(this, autofillFields, clientFormData, false)?.let(this::setDatasetIntent) } } } @@ -115,7 +115,7 @@ class AuthActivity : Activity() { companion object { // Unique autofillId for dataset intents. - private var sDatasetPendingIntentId = 0 + private var datasetPendingIntentId = 0 internal fun getAuthIntentSenderForResponse(context: Context): IntentSender { val intent = Intent(context, AuthActivity::class.java) @@ -127,7 +127,7 @@ class AuthActivity : Activity() { val intent = Intent(context, AuthActivity::class.java) intent.putExtra(EXTRA_DATASET_NAME, datasetName) intent.putExtra(EXTRA_FOR_RESPONSE, false) - return PendingIntent.getActivity(context, ++sDatasetPendingIntentId, intent, + return PendingIntent.getActivity(context, ++datasetPendingIntentId, intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender } } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.kt index b7470d7b..58666ef4 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.kt @@ -37,10 +37,14 @@ object AutofillHelper { * client View. */ fun newDataset(context: Context, autofillFields: AutofillFieldsCollection, - clientFormData: ClientFormData): Dataset? { + clientFormData: ClientFormData, datasetAuth: Boolean): Dataset? { clientFormData.datasetName?.let { datasetName -> val datasetBuilder = Dataset.Builder(newRemoteViews(context.packageName, datasetName)) val setValueAtLeastOnce = clientFormData.applyToFields(autofillFields, datasetBuilder) + if (datasetAuth) { + val sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName) + datasetBuilder.setAuthentication(sender) + } if (setValueAtLeastOnce) { return datasetBuilder.build() } @@ -65,18 +69,8 @@ object AutofillHelper { clientFormDataMap?.keys?.let { datasetNames -> for (datasetName in datasetNames) { clientFormDataMap[datasetName]?.let { clientFormData -> - if (datasetAuth) { - clientFormData.datasetName?.let { - val datasetBuilder = Dataset.Builder(newRemoteViews(context.packageName, it)) - val sender = AuthActivity - .getAuthIntentSenderForDataset(context, it) - datasetBuilder.setAuthentication(sender) - responseBuilder.addDataset(datasetBuilder.build()) - } - } else { - val dataset = newDataset(context, autofillFields, clientFormData) - dataset?.let(responseBuilder::addDataset) - } + val dataset = newDataset(context, autofillFields, clientFormData, datasetAuth) + dataset?.let(responseBuilder::addDataset) } } } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.kt index 9bc2a8c2..41712bd1 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.kt @@ -76,7 +76,8 @@ class MyAutofillService : AutofillService() { callback.onSuccess(responseBuilder.build()) } else { val datasetAuth = MyPreferences.isDatasetAuth(this) - val clientFormDataMap = SharedPrefsAutofillRepository.getClientFormData(this, autofillFields.focusedAutofillHints, autofillFields.allAutofillHints) + val clientFormDataMap = SharedPrefsAutofillRepository.getClientFormData(this, + autofillFields.focusedAutofillHints, autofillFields.allAutofillHints) val response = AutofillHelper.newResponse(this, datasetAuth, autofillFields, clientFormDataMap) callback.onSuccess(response) } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.kt index e05a2d51..2cb37eea 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.kt @@ -22,12 +22,12 @@ import com.example.android.autofillframework.CommonUtil.TAG import com.example.android.autofillframework.service.model.AutofillField import com.example.android.autofillframework.service.model.AutofillFieldsCollection import com.example.android.autofillframework.service.model.ClientFormData -import com.example.android.autofillframework.service.model.MutableAutofillValue +import com.example.android.autofillframework.service.model.SavableAutofillData /** * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an - * AssistStructure from the client Activity, representing its View hierarchy. In this - * sample, it parses the hierarchy and records + * AssistStructure from the client Activity, representing its View hierarchy. In this sample, it + * parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. */ internal class StructureParser(private val mStructure: AssistStructure) { val autofillFields = AutofillFieldsCollection() @@ -64,7 +64,7 @@ internal class StructureParser(private val mStructure: AssistStructure) { autofillFields.add(AutofillField(viewNode)) } else { clientFormData.setAutofillValuesForHints(viewNode.autofillHints, - MutableAutofillValue(viewNode)) + SavableAutofillData(viewNode)) } } } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.kt index a7eadc96..e2fb8708 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/SharedPrefsAutofillRepository.kt @@ -24,9 +24,9 @@ import com.google.gson.reflect.TypeToken /** - * Singleton autofill data repository, that stores autofill fields to SharedPreferences. - * DISCLAIMER, you should not store sensitive fields like user data unencrypted. This is only done - * here for simplicity and learning purposes. + * Singleton autofill data repository that stores autofill fields to SharedPreferences. + * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done + * here only for simplicity and learning purposes. */ object SharedPrefsAutofillRepository : AutofillRepository { private val SHARED_PREF_KEY = "com.example.android.autofillframework.service" @@ -46,9 +46,13 @@ object SharedPrefsAutofillRepository : AutofillRepository { val type = object : TypeToken<ClientFormData>() {}.type Gson().fromJson<ClientFormData>(clientFormDataString, type)?.let { clientFormData -> if (clientFormData.helpsWithHints(focusedAutofillHints)) { + // Saved data has data relevant to at least 1 of the hints associated with the + // View in focus. hasDataForFocusedAutofillHints = true clientFormData.datasetName?.let { datasetName -> if (clientFormData.helpsWithHints(allAutofillHints)) { + // Saved data has data relevant to at least 1 of these hints associated with any + // of the Views in the hierarchy. clientFormDataMap.put(datasetName, clientFormData) } } diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.kt index 78980aaa..2dc844ae 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.kt @@ -28,16 +28,16 @@ import java.util.HashMap * dataset name associated with it. */ class ClientFormData constructor(var datasetName: String? = null, - private val hintMap: HashMap<String, MutableAutofillValue> = HashMap<String, MutableAutofillValue>()) { + private val hintMap: HashMap<String, SavableAutofillData> = HashMap<String, SavableAutofillData>()) { private val TAG = "ClientFormData" /** * Sets values for a list of autofillHints. */ - fun setAutofillValuesForHints(autofillHints: Array<String>, autofillValue: MutableAutofillValue) { + fun setAutofillValuesForHints(autofillHints: Array<String>, autofillData: SavableAutofillData) { autofillHints.forEach { hint -> - hintMap[hint] = autofillValue + hintMap[hint] = autofillData } } @@ -86,6 +86,10 @@ class ClientFormData constructor(var datasetName: String? = null, return setValueAtLeastOnce } + /** + * Returns whether this model contains autofill data that is relevant to any of the + * autofillHints that are passed in. + */ fun helpsWithHints(autofillHints: List<String>): Boolean { for (autofillHint in autofillHints) { hintMap[autofillHint]?.let { savedAutofillValue -> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/MutableAutofillValue.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/SavableAutofillData.kt index 702a1358..44baebfa 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/MutableAutofillValue.kt +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/SavableAutofillData.kt @@ -19,9 +19,9 @@ import android.app.assist.AssistStructure import android.view.autofill.AutofillValue /** - * Mutable, JSON serializable data class containing the same data as an [AutofillValue]. + * JSON serializable data class containing the same data as an [AutofillValue]. */ -class MutableAutofillValue(viewNode: AssistStructure.ViewNode) { +class SavableAutofillData(viewNode: AssistStructure.ViewNode) { var textValue: String? = null var dateValue: Long? = null var toggleValue: Boolean? = null diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-dimens.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-dimens.xml deleted file mode 100644 index 22074a2b..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-dimens.xml +++ /dev/null @@ -1,24 +0,0 @@ -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --> - -<resources> - - <!-- Semantic definitions --> - - <dimen name="horizontal_page_margin">@dimen/margin_huge</dimen> - <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-styles.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-styles.xml deleted file mode 100644 index 03d19741..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-sw600dp/template-styles.xml +++ /dev/null @@ -1,25 +0,0 @@ -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --> - -<resources> - - <style name="Widget.SampleMessage"> - <item name="android:textAppearance">?android:textAppearanceLarge</item> - <item name="android:lineSpacingMultiplier">1.2</item> - <item name="android:shadowDy">-6.5</item> - </style> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v11/template-styles.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v11/template-styles.xml deleted file mode 100644 index 8c1ea66f..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v11/template-styles.xml +++ /dev/null @@ -1,22 +0,0 @@ -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --> - -<resources> - - <!-- Activity themes --> - <style name="Theme.Base" parent="android:Theme.Holo.Light" /> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-colors.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-colors.xml deleted file mode 100644 index 8b6ec3f8..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-colors.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources> - - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-template-styles.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-template-styles.xml deleted file mode 100644 index c778e4f9..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values-v21/base-template-styles.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources> - - <!-- Activity themes --> - <style name="Theme.Base" parent="android:Theme.Material.Light"> - </style> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/base-strings.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/base-strings.xml deleted file mode 100644 index ddf8b07d..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/base-strings.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources> - <string name="app_name">AutofillFramework</string> - <string name="intro_message"> - <![CDATA[ - - - This sample app demos the Autofill feature introduced in Android O. - - - ]]> - </string> -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/strings.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/strings.xml index b5611ae1..f9448a74 100644 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/strings.xml +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ --> <resources> + <string name="app_name">Autofill Sample</string> <string name="settings_name">Autofill Settings</string> <string name="username_label">Username</string> <string name="password_label">Password</string> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-dimens.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-dimens.xml deleted file mode 100644 index 39e710b5..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-dimens.xml +++ /dev/null @@ -1,32 +0,0 @@ -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --> - -<resources> - - <!-- Define standard dimensions to comply with Holo-style grids and rhythm. --> - - <dimen name="margin_tiny">4dp</dimen> - <dimen name="margin_small">8dp</dimen> - <dimen name="margin_medium">16dp</dimen> - <dimen name="margin_large">32dp</dimen> - <dimen name="margin_huge">64dp</dimen> - - <!-- Semantic definitions --> - - <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen> - <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-styles.xml b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-styles.xml deleted file mode 100644 index 6e7d593d..00000000 --- a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/res/values/template-styles.xml +++ /dev/null @@ -1,42 +0,0 @@ -<!-- - Copyright 2013 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - --> - -<resources> - - <!-- Activity themes --> - - <style name="Theme.Base" parent="android:Theme.Light" /> - - <style name="Theme.Sample" parent="Theme.Base" /> - - <style name="AppTheme" parent="Theme.Sample" /> - <!-- Widget styling --> - - <style name="Widget" /> - - <style name="Widget.SampleMessage"> - <item name="android:textAppearance">?android:textAppearanceMedium</item> - <item name="android:lineSpacingMultiplier">1.1</item> - </style> - - <style name="Widget.SampleMessageTile"> - <item name="android:background">@drawable/tile</item> - <item name="android:shadowColor">#7F000000</item> - <item name="android:shadowDy">-3.5</item> - <item name="android:shadowRadius">2</item> - </style> - -</resources> diff --git a/input/autofill/AutofillFramework/kotlinApp/README.md b/input/autofill/AutofillFramework/kotlinApp/README.md index 53e4a7f0..9b060561 100644 --- a/input/autofill/AutofillFramework/kotlinApp/README.md +++ b/input/autofill/AutofillFramework/kotlinApp/README.md @@ -1,63 +1,98 @@ -Android AutofillFramework Sample (Kotlin) + +Android AutofillFramework Sample =================================== This sample demonstrates the use of the Autofill Framework. It includes implementations of client -Activities that want to be autofilled, and a Service that can provide autofill data to client -Activities. For simplicity, this sample's service uses mock data to autofill what it thinks are -username and password text fields. +Activities with views that should be autofilled, and a Service that can provide autofill data to +client Activities. Introduction ------------ This sample demonstrates the use of the Autofill framework from the service side and the client -side. In practice, only a small handful of apps will develop Autofill services because a device will -only have one service as default at a time. However, all apps targeting O with any autofillable -fields should follow the necessary steps to ensure their Views can be autofilled. Most of the time, -there is little to no extra code involved, but the use of custom views and views with virtual child -views requires more work. - -The sample's Autofill service is implemented to parse the client's view hierarchy in search of text -fields that it has data for. If such text fields exist in the hierarchy, the service sends data -suggestions to the client to fill in those text fields. In this basic sample, it will only look for -views whose resource IDs are "usernameField" and "passwordField" and will send mock credential data -accordingly. A real Autofill service would attempt to autofill more than just login credentials and -would be able to fill in other view types in addition to text fields (e.g. spinners, checkboxes, -etc.). It would also use more advanced heuristics to determine what data to send to which views. - -To set the device's default Autofill service to the one in the sample, edit -**Settings** > **Apps & Notifications** > **Default Apps** > **Auto-fill app** and select the -sample app. To edit the service's settings, open the **Autofill Settings** launcher icon. Here, you -can set whether you want to enable authentication on the entire Autofill Response or just on -individual datasets. You can also set the number of mock datasets that are sent to the client app. - -The client side of the app has two Activities that each have a username field and a password field. -One of the Activities uses standard views and the other Activity uses a custom view with virtual -children. The standard views do not require any extra code to allow autofill. The following code -example shows the `View` method you have to override in order to provide view hierarchy data to the -Autofill service. This is triggered when the `View` goes into focus and Android invokes an Autofill -request. - -```java -/* -This method is responsible for building the ViewStructure that gets passed to the AutoFillService -by the framework when it is time to find Autofill suggestions. To do this, it should traverse -through its view hierarchy and add views to the ViewStructure on the way. -*/ -@Override -public void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags) { - structure.setClassName(getClass().getName()); - int childrenSize = mItems.size(); - int index = structure.addChildCount(childrenSize); - for (int i = 0; i < childrenSize; i++) { - Item item = mItems.valueAt(i); - ViewStructure child = structure.newChild(index, item.id, flags); - child.setSanitized(item.sanitized); - child.setText(item.text); - child.setAutoFillValue(AutoFillValue.forText(item.text)); - child.setFocused(item.line.focused); - child.setId(item.id, getContext().getPackageName(), null, item.line.idEntry); - index++; - } +side. In practice, only a small handful of apps will develop Autofill services because a device +will only have one service as default at a time, and there is just a small number of 3rd-party apps +providing these services (typically password managers). However, all apps targeting O with any +autofillable fields should follow the necessary steps to 1) ensure their views can be autofilled +and 2) optimize their autofill performance. Most of the time, there is little to no extra code +involved, but the use of custom views and views with virtual child views requires more work. + +The sample's Autofill service is implemented to parse the client's view hierarchy in search of +autofillable fields that it has data for. If such fields exist in the hierarchy, the service sends +data suggestions to the client to autofill those fields. The client uses the following attributes +to specify autofill properties: `importantForAutofill`, `autofillHints`, and `autofillType`. +`importantForAutofill` specifies whether the view is autofillable. `autofillHints` is a list of +strings that hint to the service **what** data to fill the view with. This sample service only +supports the hints listed [here](https://developer.android.com/reference/android/view/View.html#AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE) +with the prefix AUTOFILL_HINT_*. `autofillType` tells the service the type of data it expects to +receive (i.e. a list index, a date, or a string). Specifying `autofillType` is only necessary +when implementing a custom view since all of the provided widgets in the UI toolkit do this for you. + +To set the device's default Autofill service to the one in the sample, edit **Settings** > +**System** > **Languages & Input** > **Advanced** > **Auto-fill service** and select the sample +app. To edit the service's settings, tap the settings icon next to the **Auto-fill service** list +item or open the **Autofill Settings** launcher icon.. Here, you can set whether you want to enable +authentication on the entire autofill Response or just on individual autofill datasets. You should +also set the master password to “unlock” authenticated autofill data with. + +**Note:** This sample service stores all autofill data in SharedPreferences and thus is not secure. +Be careful about what you store when experimenting with the sample because anyone with root access +to your device will be able to view your autofill data. + +The client side of the app has three Activities that each have autofillable fields. The first +Activity uses standard views to comprise a login form. Very little needs to be done by the client +app to ensure the views get autofilled properly. The second Activity uses a custom view with +virtual children, meaning some autofillable child views are not known to the View hierarchy to be +child views. Supporting autofill on these child views is a little more involved. + +The following code snippet shows how to signal to the autofill service that a specific +autofillable virtual view has come into focus: + +```kotlin +class CustomVirtualView(context: Context, attrs: AttributeSet) : View(context, attrs) { +... + // Cache AutofillManager system service + private val autofillManager: AutofillManager = context.getSystemService(AutofillManager::class.java) +... + // Notify service which virtual view has come into focus. + autofillManager.notifyViewEntered(this@CustomVirtualView, id, absBounds) +... + // Notify service that a virtual view has left focus. + autofillManager.notifyViewExited(this@CustomVirtualView, id) +} +``` + +Now that the autofillable view has signaled to the service that it has been autofilled, it needs +to provide the virtual view hierarchy to the Autofill service. This is done out of the box for +views part of the UI toolkit, but you need to implement this yourself if you have the view has +virtual child views. The following code example shows the `View` method you have to override in +order to provide this view hierarchy data to the Autofill service. + +```kotlin +override fun onProvideAutofillVirtualStructure(structure: ViewStructure, flags: Int) { + // Build a ViewStructure to pack in AutoFillService requests. + structure.setClassName(javaClass.name) + val childrenSize = mItems.size() + Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = " + + childrenSize + ", extras: " + bundleToString(structure.extras)) + var index = structure.addChildCount(childrenSize) + // Traverse through the view hierarchy, including virtual child views. For each view, we + // need to set the relevant autofill metadata and add it to the ViewStructure. + for (i in 0..childrenSize - 1) { + val item = mItems.valueAt(i) + Log.d(TAG, "Adding new child at index $index: $item") + val child = structure.newChild(index) + child.setAutofillId(structure, item.id) + child.setAutofillHints(item.hints) + child.setAutofillType(item.type) + child.setDataIsSensitive(!item.sanitized) + child.text = item.text + child.setAutofillValue(AutofillValue.forText(item.text)) + child.setFocused(item.focused) + child.setId(item.id, context.packageName, null, item.line.idEntry) + child.setClassName(item.className) + index++ + } } ``` @@ -65,30 +100,29 @@ After the service processes the Autofill request and sends back a series of Auto (wrapped in a `Response` object), the user can pick which `Dataset` they want to autofill their views with. When a `Dataset` is selected, this method is invoked for all of the views that were associated with that `Dataset` by the service. For example, the `Dataset` might contain Autofill -values for username, password, birthday, and address. This method would then be invoked on all four -of those fields. The following code example shows how the sample app implements the method to -deliver a UI update to the appropriate child view after the user makes their selection. - -```java -/* -User has just selected a Dataset from the list of Autofill suggestions and the Dataset's -AutoFillValue gets passed into this method. This method updates the UI based on the data -in the AutoFillValue. -*/ -@Override -public void autoFillVirtual(int id, AutoFillValue value) { - Item item = mItems.get(id); - if (item == null) { - // ID not recognized so no-op. - return; - } - if (!item.editable) { - // Component is not editable so no-op. - return; - } - // Set the virtual child view's text to the text wrapped in the AutoFillValue. - item.text = value.getTextValue(); - postInvalidate(); +values for username, password, birthday, and address. This method would then be invoked on all +four of those fields. The following code example shows how the sample app implements the method +to deliver a UI update to the appropriate child view after the user makes their selection. + +```kotlin +override fun autofill(values: SparseArray<AutofillValue>) { + // User has just selected a Dataset from the list of autofill suggestions. + // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant + // to fill a specific autofillable view. Now we have to update the UI based on the + // AutofillValues in the list. + for (i in 0..values.size() - 1) { + val id = values.keyAt(i) + val value = values.valueAt(i) + mItems[id]?.let { item -> + if (item.editable) { + // Set the item's text to the text wrapped in the AutofillValue. + item.text = value.textValue + } else { + // Component not editable, so no-op. + } + } + } + postInvalidate() } ``` @@ -96,8 +130,10 @@ Pre-requisites -------------- - Android SDK Preview O -- Android Build Tools v25.0.3 -- Android Support Repository +- Android Studio 3.0+ +- Android Build Tools v26+ +- Android Support Repository v26+ +- Gradle v3.0+ Screenshots ------------- diff --git a/input/autofill/AutofillFramework/template-params.xml b/input/autofill/AutofillFramework/template-params.xml index 5bac84df..e1cc3252 100644 --- a/input/autofill/AutofillFramework/template-params.xml +++ b/input/autofill/AutofillFramework/template-params.xml @@ -212,12 +212,14 @@ public void autofill(SparseArray<AutofillValue> values) { final int id = values.keyAt(i); final AutofillValue value = values.valueAt(i); final Item item = mItems.get(id); - if (item == null || !item.editable) { - // Component either not found or is not editable, so no-op. - return; + if (item != null && item.editable) { + // Set the item's text to the text wrapped in the AutofillValue. + item.text = value.getTextValue(); + } else if (item == null) { + // Component not found, so no-op. + } else { + // Component not editable, so no-op. } - // Set the item's text to the text wrapped in the AutofillValue. - item.text = value.getTextValue(); } postInvalidate(); } |