diff options
author | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-16 22:31:36 -0700 |
---|---|---|
committer | Douglas Sigelbaum <sigelbaum@google.com> | 2017-05-17 00:35:21 -0700 |
commit | 08a3592720a23e56c9546cf89abf707372f5abfb (patch) | |
tree | 603afca306120848ce00f4cbbc5be26859cfa358 /input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example | |
parent | 8fa30fc0cbc6a7e2da9749e66373d70a593fd1ce (diff) | |
download | android-08a3592720a23e56c9546cf89abf707372f5abfb.tar.gz |
Initial kotlin commit for client app in the autofill sample.
Bug: 38182790
Test: manual
Change-Id: Ib76173768461528f52297a7acd2f40d532b14d0a
Diffstat (limited to 'input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example')
19 files changed, 2085 insertions, 0 deletions
diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java new file mode 100644 index 00000000..54049902 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java @@ -0,0 +1,54 @@ +/* + * 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; + +import android.os.Bundle; + +import java.util.Arrays; +import java.util.Set; + +public final class CommonUtil { + + public static final String TAG = "AutofillSample"; + + public static final String EXTRA_DATASET_NAME = "dataset_name"; + public static final String EXTRA_FOR_RESPONSE = "for_response"; + + private static void bundleToString(StringBuilder builder, Bundle data) { + final Set<String> keySet = data.keySet(); + builder.append("[Bundle with ").append(keySet.size()).append(" keys:"); + for (String key : keySet) { + builder.append(' ').append(key).append('='); + Object value = data.get(key); + if ((value instanceof Bundle)) { + bundleToString(builder, (Bundle) value); + } else { + builder.append((value instanceof Object[]) + ? Arrays.toString((Object[]) value) : value); + } + } + builder.append(']'); + } + + public static String bundleToString(Bundle data) { + if (data == null) { + return "N/A"; + } + final StringBuilder builder = new StringBuilder(); + bundleToString(builder, data); + return builder.toString(); + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.kt new file mode 100644 index 00000000..f2d9a8b4 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CreditCardActivity.kt @@ -0,0 +1,88 @@ +/* + * 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.app + +import android.content.Context +import android.content.Intent +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 + +class CreditCardActivity : AppCompatActivity() { + + private var mCcExpirationDaySpinner: Spinner? = null + private var mCcExpirationMonthSpinner: Spinner? = null + private var mCcExpirationYearSpinner: Spinner? = null + private var mSubmitButton: Button? = null + private var mClearButton: Button? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.credit_card_activity) + + mSubmitButton = findViewById(R.id.submit) as Button + mClearButton = findViewById(R.id.clear) as Button + mCcExpirationDaySpinner = findViewById(R.id.expirationDay) as Spinner + mCcExpirationMonthSpinner = findViewById(R.id.expirationMonth) as Spinner + mCcExpirationYearSpinner = findViewById(R.id.expirationYear) as Spinner + + // Create an ArrayAdapter using the string array and a default spinner layout + val dayAdapter = ArrayAdapter.createFromResource(this, R.array.day_array, android.R.layout.simple_spinner_item) + // 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!!.adapter = dayAdapter + + val monthAdapter = ArrayAdapter.createFromResource(this, R.array.month_array, android.R.layout.simple_spinner_item) + monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + mCcExpirationMonthSpinner!!.adapter = monthAdapter + + val yearAdapter = ArrayAdapter.createFromResource(this, R.array.year_array, android.R.layout.simple_spinner_item) + yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + mCcExpirationYearSpinner!!.adapter = yearAdapter + + mSubmitButton!!.setOnClickListener { submit() } + mClearButton!!.setOnClickListener { resetFields() } + } + + private fun resetFields() { + //TODO + } + + /** + * Launches new Activity and finishes, triggering an autofill save request if the user entered + * any new data. + */ + private fun submit() { + val intent = WelcomeActivity.getStartActivityIntent(this@CreditCardActivity) + startActivity(intent) + finish() + } + + companion object { + + fun getStartActivityIntent(context: Context): Intent { + val intent = Intent(context, CreditCardActivity::class.java) + return intent + } + } +} 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 new file mode 100644 index 00000000..c5a7989e --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/CustomVirtualView.kt @@ -0,0 +1,273 @@ +/* + * 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.app + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.Style +import android.graphics.Rect +import android.util.AttributeSet +import android.util.Log +import android.util.SparseArray +import android.view.MotionEvent +import android.view.View +import android.view.ViewStructure +import android.view.autofill.AutofillManager +import android.view.autofill.AutofillValue +import android.widget.EditText +import android.widget.TextView + +import com.example.android.autofillframework.R + +import java.util.ArrayList +import java.util.Arrays + +import com.example.android.autofillframework.CommonUtil.bundleToString + + +/** + * Custom View with virtual child views for Username/Password text fields. + */ +class CustomVirtualView(context: Context, attrs: AttributeSet) : View(context, attrs) { + + private val mLines = ArrayList<Line>() + private val mItems = SparseArray<Item>() + private val mAfm: AutofillManager + + private var mFocusedLine: Line? = null + private val mTextPaint: Paint + private val mTextHeight: Int + private val mTopMargin: Int + private val mLeftMargin: Int + private val mVerticalGap: Int + private val mLineLength: Int + private val mFocusedColor: Int + private val mUnfocusedColor: Int + + private val mUsernameLine: Line + private val mPasswordLine: Line + + init { + + mAfm = context.getSystemService(AutofillManager::class.java) + + mTextPaint = Paint() + + mUnfocusedColor = Color.BLACK + mFocusedColor = Color.RED + mTextPaint.style = Style.FILL + mTopMargin = 100 + mLeftMargin = 100 + mTextHeight = 90 + mVerticalGap = 10 + + mLineLength = mTextHeight + mVerticalGap + mTextPaint.textSize = mTextHeight.toFloat() + mUsernameLine = addLine("usernameField", context.getString(R.string.username_label), + arrayOf(View.AUTOFILL_HINT_USERNAME), " ", true) + mPasswordLine = addLine("passwordField", context.getString(R.string.password_label), + arrayOf(View.AUTOFILL_HINT_PASSWORD), " ", false) + + Log.d(TAG, "Text height: " + mTextHeight) + } + + 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. + Log.d(TAG, "autoFill(): " + values) + for (i in 0..values.size() - 1) { + val id = values.keyAt(i) + val value = values.valueAt(i) + val item = mItems.get(id) + if (item == null) { + Log.w(TAG, "No item for id " + id) + return + } + if (!item.editable) { + 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.textValue + } + postInvalidate() + } + + 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) + 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++ + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + Log.d(TAG, "onDraw: " + mLines.size + " lines; canvas:" + canvas) + var x: Float + var y = (mTopMargin + mLineLength).toFloat() + for (i in mLines.indices) { + x = mLeftMargin.toFloat() + val line = mLines[i] + Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y) + mTextPaint.color = if (line.fieldTextItem.focused) mFocusedColor else mUnfocusedColor + val readOnlyText = line.labelItem.text.toString() + ": [" + val writeText = line.fieldTextItem.text.toString() + "]" + // Paints the label first... + canvas.drawText(readOnlyText, x, y, mTextPaint) + // ...then paints the edit text and sets the proper boundary + val deltaX = mTextPaint.measureText(readOnlyText) + x += deltaX + line.bounds.set(x.toInt(), (y - mLineLength).toInt(), + (x + mTextPaint.measureText(writeText)).toInt(), y.toInt()) + Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds) + canvas.drawText(writeText, x, y, mTextPaint) + y += mLineLength.toFloat() + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val y = event.y.toInt() + Log.d(TAG, "Touched: y=$y, range=$mLineLength, top=$mTopMargin") + var lowerY = mTopMargin + var upperY = -1 + for (i in mLines.indices) { + upperY = lowerY + mLineLength + val line = mLines[i] + Log.d(TAG, "Line $i ranges from $lowerY to $upperY") + if (lowerY <= y && y <= upperY) { + if (mFocusedLine != null) { + Log.d(TAG, "Removing focus from " + mFocusedLine!!) + mFocusedLine!!.changeFocus(false) + } + Log.d(TAG, "Changing focus to " + line) + mFocusedLine = line + mFocusedLine!!.changeFocus(true) + invalidate() + break + } + lowerY += mLineLength + } + return super.onTouchEvent(event) + } + + val usernameText: CharSequence + get() = mUsernameLine.fieldTextItem.text + + val passwordText: CharSequence + get() = mPasswordLine.fieldTextItem.text + + fun resetFields() { + mUsernameLine.reset() + mPasswordLine.reset() + postInvalidate() + } + + private fun addLine(idEntry: String, label: String, hints: Array<String>, text: String, sanitized: Boolean): Line { + val line = Line(idEntry, label, hints, text, sanitized) + mLines.add(line) + mItems.put(line.labelItem.id, line.labelItem) + mItems.put(line.fieldTextItem.id, line.fieldTextItem) + return line + } + + private class Item internal constructor(val line: Line, val id: Int, val hints: Array<String>?, val type: Int, var text: CharSequence, val editable: Boolean, val sanitized: Boolean) { + var focused = false + + override fun toString(): String { + return id.toString() + ": " + text + if (editable) + " (editable)" + else + " (read-only)" + if (sanitized) " (sanitized)" else " (sensitive" + } + + val className: String + get() = if (editable) EditText::class.java.name else TextView::class.java.name + } + + private inner class Line constructor(val idEntry: String, label: String, hints: Array<String>, text: String, sanitized: Boolean) { + + // Boundaries of the text field, relative to the CustomView + internal val bounds = Rect() + var labelItem: Item + var fieldTextItem: Item + + init { + this.labelItem = Item(this, ++nextId, null, View.AUTOFILL_TYPE_NONE, label, false, true) + this.fieldTextItem = Item(this, ++nextId, hints, View.AUTOFILL_TYPE_TEXT, text, true, sanitized) + } + + internal fun changeFocus(focused: Boolean) { + fieldTextItem.focused = focused + if (focused) { + val absBounds = absCoordinates + Log.d(TAG, "focus gained on " + fieldTextItem.id + "; absBounds=" + absBounds) + mAfm.notifyViewEntered(this@CustomVirtualView, fieldTextItem.id, absBounds) + } else { + Log.d(TAG, "focus lost on " + fieldTextItem.id) + mAfm.notifyViewExited(this@CustomVirtualView, fieldTextItem.id) + } + } + + private // Must offset the boundaries so they're relative to the CustomView. + val absCoordinates: Rect + get() { + val offset = IntArray(2) + getLocationOnScreen(offset) + val absBounds = 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 + + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds) + return absBounds + } + + fun reset() { + fieldTextItem.text = " " + } + + override fun toString(): String { + return "Label: " + labelItem + " Text: " + fieldTextItem + " Focused: " + + fieldTextItem.focused + } + } + + companion object { + + private val TAG = "CustomView" + + private var nextId: Int = 0 + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.kt new file mode 100644 index 00000000..8a739606 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/LoginActivity.kt @@ -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.app + +import android.content.Context +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 + +import com.example.android.autofillframework.R + +class LoginActivity : AppCompatActivity() { + + private var mUsernameEditText: EditText? = null + private var mPasswordEditText: EditText? = null + private var mLoginButton: Button? = null + private var mClearButton: Button? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.login_activity) + + mLoginButton = findViewById(R.id.login) as Button + mClearButton = findViewById(R.id.clear) as Button + mUsernameEditText = findViewById(R.id.usernameField) as EditText + mPasswordEditText = findViewById(R.id.passwordField) as EditText + mLoginButton!!.setOnClickListener { login() } + mClearButton!!.setOnClickListener { resetFields() } + } + + private fun resetFields() { + mUsernameEditText!!.setText("") + mPasswordEditText!!.setText("") + } + + /** + * Emulates a login action. + */ + private fun login() { + val username = mUsernameEditText!!.text.toString() + val password = mPasswordEditText!!.text.toString() + val valid = isValidCredentials(username, password) + if (valid) { + val intent = WelcomeActivity.getStartActivityIntent(this@LoginActivity) + startActivity(intent) + finish() + } else { + Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show() + } + } + + /** + * Dummy implementation for demo purposes. A real service should use secure mechanisms to + * authenticate users. + */ + fun isValidCredentials(username: String?, password: String?): Boolean { + return username != null && password != null && username == password + } + + companion object { + + fun getStartActivityIntent(context: Context): Intent { + val intent = Intent(context, LoginActivity::class.java) + return intent + } + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.kt new file mode 100644 index 00000000..b47daa18 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/MainActivity.kt @@ -0,0 +1,53 @@ +/* + * 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.app + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View + +import com.example.android.autofillframework.R + +/** + * This is used to launch sample activities that showcase autofill. + */ +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + findViewById(R.id.standardViewSignInButton).setOnClickListener { standardViewSignIn() } + findViewById(R.id.virtualViewSignInButton).setOnClickListener { virtualViewSignIn() } + findViewById(R.id.creditCardCheckoutButton).setOnClickListener { creditCardCheckout() } + } + + private fun creditCardCheckout() { + val intent = CreditCardActivity.getStartActivityIntent(this) + startActivity(intent) + } + + private fun standardViewSignIn() { + val intent = LoginActivity.getStartActivityIntent(this) + startActivity(intent) + } + + private fun virtualViewSignIn() { + val intent = VirtualLoginActivity.getStartActivityIntent(this) + startActivity(intent) + } +}
\ No newline at end of file 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 new file mode 100644 index 00000000..198ff9d5 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/VirtualLoginActivity.kt @@ -0,0 +1,77 @@ +/* + * 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.app + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.widget.Toast + +import com.example.android.autofillframework.R + + +class VirtualLoginActivity : AppCompatActivity() { + + private var mCustomVirtualView: CustomVirtualView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.virtual_login_activity) + + mCustomVirtualView = findViewById(R.id.custom_view) as CustomVirtualView + findViewById(R.id.login).setOnClickListener { login() } + findViewById(R.id.clear).setOnClickListener { resetFields() } + } + + private fun resetFields() { + mCustomVirtualView!!.resetFields() + } + + /** + * Emulates a login action. + */ + private fun login() { + val username = mCustomVirtualView!!.usernameText.toString() + val password = mCustomVirtualView!!.passwordText.toString() + val valid = isValidCredentials(username, password) + if (valid) { + val intent = WelcomeActivity.getStartActivityIntent(this@VirtualLoginActivity) + startActivity(intent) + } else { + Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show() + } + } + + /** + * Dummy implementation for demo purposes. A real service should use secure mechanisms to + * authenticate users. + */ + fun isValidCredentials(username: String?, password: String?): Boolean { + return username != null && password != null && username == password + } + + companion object { + + fun getStartActivityIntent(context: Context): Intent { + val intent = Intent(context, VirtualLoginActivity::class.java) + return intent + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/WelcomeActivity.kt b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/WelcomeActivity.kt new file mode 100644 index 00000000..01475180 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/app/WelcomeActivity.kt @@ -0,0 +1,38 @@ +/* + * 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.app + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle + +import com.example.android.autofillframework.R + +class WelcomeActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.welcome_activity) + } + + companion object { + + fun getStartActivityIntent(context: Context): Intent { + return Intent(context, WelcomeActivity::class.java) + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java new file mode 100644 index 00000000..768b2ee3 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AuthActivity.java @@ -0,0 +1,160 @@ +/* + * 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; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.assist.AssistStructure; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.support.annotation.Nullable; +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.model.AutofillFieldsCollection; +import com.example.android.autofillframework.service.model.ClientFormData; +import com.example.android.autofillframework.service.settings.MyPreferences; + +import java.util.HashMap; + +import static android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE; +import static android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT; +import static com.example.android.autofillframework.CommonUtil.EXTRA_DATASET_NAME; +import static com.example.android.autofillframework.CommonUtil.EXTRA_FOR_RESPONSE; +import static com.example.android.autofillframework.CommonUtil.TAG; + +/** + * This Activity controls the UI for logging in to the Autofill service. + * It is launched when an Autofill Response or specific Dataset within the Response requires + * authentication to access. It bundles the result in an Intent. + */ +public class AuthActivity extends Activity { + + // Unique id for dataset intents. + private static int sDatasetPendingIntentId = 0; + + private EditText mMasterPassword; + private Button mCancel; + private Button mLogin; + private Intent mReplyIntent; + + static IntentSender getAuthIntentSenderForResponse(Context context) { + final Intent intent = new Intent(context, AuthActivity.class); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) + .getIntentSender(); + } + + static IntentSender getAuthIntentSenderForDataset(Context context, String datasetName) { + final Intent intent = new Intent(context, AuthActivity.class); + intent.putExtra(EXTRA_DATASET_NAME, datasetName); + intent.putExtra(EXTRA_FOR_RESPONSE, false); + return PendingIntent.getActivity(context, ++sDatasetPendingIntentId, intent, + PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender(); + } + + @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() { + @Override + public void onClick(View v) { + login(); + } + + }); + + mCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onFailure(); + AuthActivity.this.finish(); + } + }); + } + + private void login() { + Editable password = mMasterPassword.getText(); + if (password.toString() + .equals(MyPreferences.getInstance(AuthActivity.this).getMasterPassword())) { + onSuccess(); + } else { + Toast.makeText(this, "Password incorrect", Toast.LENGTH_SHORT).show(); + onFailure(); + } + finish(); + } + + @Override + public void finish() { + if (mReplyIntent != null) { + setResult(RESULT_OK, mReplyIntent); + } else { + setResult(RESULT_CANCELED); + } + super.finish(); + } + + private void onFailure() { + Log.w(TAG, "Failed auth."); + mReplyIntent = null; + } + + private void onSuccess() { + Intent intent = getIntent(); + boolean forResponse = intent.getBooleanExtra(EXTRA_FOR_RESPONSE, true); + AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); + StructureParser parser = new StructureParser(structure); + parser.parse(); + AutofillFieldsCollection autofillFields = parser.getAutofillFields(); + int saveTypes = parser.getSaveTypes(); + mReplyIntent = new Intent(); + HashMap<String, ClientFormData> clientFormDataMap = + LocalAutofillRepository.getInstance(this).getClientFormData + (autofillFields.getFocusedHints(), autofillFields.getAllHints()); + if (forResponse) { + setResponseIntent(AutofillHelper.newResponse + (this, false, autofillFields, saveTypes, clientFormDataMap)); + } else { + String datasetName = intent.getStringExtra(EXTRA_DATASET_NAME); + setDatasetIntent(AutofillHelper.newDataset + (this, autofillFields, clientFormDataMap.get(datasetName))); + } + } + + private void setResponseIntent(FillResponse fillResponse) { + mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse); + } + + private void setDatasetIntent(Dataset dataset) { + mReplyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset); + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java new file mode 100644 index 00000000..460729e6 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/AutofillHelper.java @@ -0,0 +1,100 @@ +/* + * 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; + +import android.content.Context; +import android.content.IntentSender; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.service.autofill.SaveInfo; +import android.util.Log; +import android.view.autofill.AutofillId; +import android.widget.RemoteViews; + +import com.example.android.autofillframework.R; +import com.example.android.autofillframework.service.model.AutofillFieldsCollection; +import com.example.android.autofillframework.service.model.ClientFormData; + +import java.util.HashMap; +import java.util.Set; + +import static com.example.android.autofillframework.CommonUtil.TAG; + +/** + * This is a class containing helper methods for building Autofill Datasets and Responses. + */ +public final class AutofillHelper { + + /** + * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the + * 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; + } + } + + public static RemoteViews newRemoteViews(String packageName, String remoteViewsText) { + RemoteViews presentation = new RemoteViews(packageName, R.layout.list_item); + presentation.setTextViewText(R.id.text1, remoteViewsText); + return presentation; + } + + /** + * Wraps autofill data in a Response object (essentially a series of Datasets) which can then + * be sent back to the client View. + */ + public static FillResponse newResponse(Context context, + boolean datasetAuth, AutofillFieldsCollection autofillFields, int saveType, + 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 (dataset != null) { + responseBuilder.addDataset(dataset); + } + } + } + } + if (saveType != 0) { + AutofillId[] autofillIds = autofillFields.getAutofillIds(); + responseBuilder.setSaveInfo(new SaveInfo.Builder(saveType, autofillIds).build()); + return responseBuilder.build(); + } else { + Log.d(TAG, "These fields are not meant to be saved by autofill."); + return null; + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java new file mode 100644 index 00000000..61e42050 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/MyAutofillService.java @@ -0,0 +1,128 @@ +/* + * 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; + +import android.app.assist.AssistStructure; +import android.content.IntentSender; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.service.autofill.AutofillService; +import android.service.autofill.FillCallback; +import android.service.autofill.FillContext; +import android.service.autofill.FillRequest; +import android.service.autofill.FillResponse; +import android.service.autofill.SaveCallback; +import android.service.autofill.SaveRequest; +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.model.AutofillFieldsCollection; +import com.example.android.autofillframework.service.model.ClientFormData; +import com.example.android.autofillframework.service.settings.MyPreferences; + +import java.util.HashMap; +import java.util.List; + +import static com.example.android.autofillframework.CommonUtil.TAG; +import static com.example.android.autofillframework.CommonUtil.bundleToString; + +public class MyAutofillService extends AutofillService { + + @Override + public void onFillRequest(AssistStructure assistStructure, Bundle bundle, int i, + CancellationSignal cancellationSignal, FillCallback fillCallback) { + /* Deprecated, ignore */ + } + + @Override + public void onSaveRequest(AssistStructure assistStructure, Bundle bundle, + SaveCallback saveCallback) { + /* Deprecated, ignore */ + } + + @Override + public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, + FillCallback callback) { + AssistStructure structure = request.getStructure(); + final Bundle data = request.getClientState(); + Log.d(TAG, "onFillRequest(): data=" + bundleToString(data)); + + // Temporary hack for disabling autofill for components in this autofill service. + // i.e. we don't want to autofill components in AuthActivity. + if (structure.getActivityComponent().toShortString() + .contains("com.example.android.autofillframework.service")) { + callback.onSuccess(null); + return; + } + cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { + @Override + public void onCancel() { + Log.w(TAG, "Cancel autofill not implemented in this sample."); + } + }); + // Parse AutoFill data in Activity + StructureParser parser = new StructureParser(structure); + parser.parse(); + 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(); + if (responseAuth) { + // If the entire Autofill Response is authenticated, AuthActivity is used + // to generate Response. + IntentSender sender = AuthActivity.getAuthIntentSenderForResponse(this); + RemoteViews presentation = AutofillHelper + .newRemoteViews(getPackageName(), getString(R.string.autofill_sign_in_prompt)); + responseBuilder + .setAuthentication(autofillFields.getAutofillIds(), sender, presentation); + callback.onSuccess(responseBuilder.build()); + } else { + boolean datasetAuth = MyPreferences.getInstance(this).isDatasetAuth(); + HashMap<String, ClientFormData> clientFormDataMap = + LocalAutofillRepository.getInstance(this).getClientFormData + (autofillFields.getFocusedHints(), autofillFields.getAllHints()); + FillResponse response = AutofillHelper.newResponse + (this, datasetAuth, autofillFields, saveTypes, clientFormDataMap); + callback.onSuccess(response); + } + } + + @Override + public void onSaveRequest(SaveRequest request, SaveCallback callback) { + List<FillContext> context = request.getFillContexts(); + final AssistStructure structure = context.get(context.size() - 1).getStructure(); + final Bundle data = request.getClientState(); + Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data)); + StructureParser parser = new StructureParser(structure); + parser.parse(); + ClientFormData clientFormData = parser.getClientFormData(); + LocalAutofillRepository.getInstance(this).saveClientFormData(clientFormData); + } + + @Override + public void onConnected() { + Log.d(TAG, "onConnected"); + } + + @Override + public void onDisconnected() { + Log.d(TAG, "onDisconnected"); + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.java new file mode 100644 index 00000000..b6294449 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/StructureParser.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; + +import android.app.assist.AssistStructure; +import android.app.assist.AssistStructure.ViewNode; +import android.app.assist.AssistStructure.WindowNode; +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 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 + */ +final class StructureParser { + private final AutofillFieldsCollection mAutofillFields = new AutofillFieldsCollection(); + private final AssistStructure mStructure; + private ClientFormData mClientFormData; + + StructureParser(AssistStructure structure) { + mStructure = structure; + + } + + /** + * Traverse AssistStructure and add ViewNode metadata to a flat list. + */ + void parse() { + 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); + } + } + + private void parseLocked(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)); + } + int childrenSize = viewNode.getChildCount(); + if (childrenSize > 0) { + for (int i = 0; i < childrenSize; i++) { + parseLocked(viewNode.getChildAt(i)); + } + } + } + + public AutofillFieldsCollection getAutofillFields() { + return mAutofillFields; + } + + public int getSaveTypes() { + return mAutofillFields.getSaveType(); + } + + public ClientFormData getClientFormData() { + return mClientFormData; + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java new file mode 100644 index 00000000..8de8b647 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/AutofillRepository.java @@ -0,0 +1,41 @@ +/* + * 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.datasource; + +import com.example.android.autofillframework.service.model.ClientFormData; + +import java.util.HashMap; +import java.util.List; + +public interface AutofillRepository { + + /** + * Gets saved ClientFormData that contains some objects that can autofill fields with these + * {@code autofillHints}. + */ + HashMap<String, ClientFormData> getClientFormData(List<String> focusedAutofillHints, + List<String> allAutofillHints); + + /** + * Saves LoginCredential under this datasetName. + */ + void saveClientFormData(ClientFormData clientFormData); + + /** + * Clears all data. + */ + void clear(); +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java new file mode 100644 index 00000000..8336fe1e --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/datasource/LocalAutofillRepository.java @@ -0,0 +1,127 @@ +/* + * 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.datasource; + +import android.content.Context; +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 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. + */ +public class LocalAutofillRepository 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 final SharedPreferences mPrefs; + + // TODO prepend with autofill data set in Settings. + private LocalAutofillRepository(Context context) { + mPrefs = context.getApplicationContext() + .getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); + } + + public static LocalAutofillRepository getInstance(Context context) { + if (sInstance == null) { + sInstance = new LocalAutofillRepository(context); + } + return sInstance; + } + + @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); + } + } + } + if (hasDataForFocusedAutofillHints) { + return clientFormDataMap; + } else { + return null; + } + } catch (JSONException e) { + 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()); + saveAllAutofillDataStringSet(allAutofillData); + incrementDatasetNumber(); + } + + @Override + public void clear() { + mPrefs.edit().remove(CLIENT_FORM_DATA_KEY).apply(); + } + + private Set<String> getAllAutofillDataStringSet() { + return mPrefs.getStringSet(CLIENT_FORM_DATA_KEY, new ArraySet<String>()); + } + + private void saveAllAutofillDataStringSet(Set<String> allAutofillDataStringSet) { + mPrefs.edit().putStringSet(CLIENT_FORM_DATA_KEY, allAutofillDataStringSet).apply(); + } + + /** + * For simplicity, datasets will be named in the form "dataset-X" where X means + * this was the Xth dataset saved. + */ + private int getDatasetNumber() { + return mPrefs.getInt(DATASET_NUMBER_KEY, 0); + } + + /** + * Every time a dataset is saved, this should be called to increment the dataset number. + * (only important for this service's dataset naming scheme). + */ + private void incrementDatasetNumber() { + mPrefs.edit().putInt(DATASET_NUMBER_KEY, getDatasetNumber() + 1).apply(); + } +}
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java new file mode 100644 index 00000000..4d4de2bc --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillField.java @@ -0,0 +1,120 @@ +/* + * 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.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). + */ +public class AutofillField { + private int mSaveType = 0; + private String[] mHints; + private AutofillId mId; + private int mAutofillType; + private String[] mAutofillOptions; + private boolean mFocused; + + public AutofillField(AssistStructure.ViewNode view) { + mId = view.getAutofillId(); + setHints(view.getAutofillHints()); + mAutofillType = view.getAutofillType(); + mAutofillOptions = view.getAutofillOptions(); + mFocused = view.isFocused(); + } + + public String[] getHints() { + return mHints; + } + + public void setHints(String[] hints) { + mHints = hints; + updateSaveTypeFromHints(); + } + + public int getSaveType() { + return mSaveType; + } + + public AutofillId getId() { + return mId; + } + + public void setId(AutofillId id) { + mId = id; + } + + public int getAutofillType() { + return mAutofillType; + } + + public int getAutofillOptionIndex(String value) { + for (int i = 0; i < mAutofillOptions.length; i++) { + if (mAutofillOptions[i].equals(value)) { + return i; + } + } + return -1; + } + + public boolean isFocused() { + return mFocused; + } + + private void updateSaveTypeFromHints() { + mSaveType = 0; + if (mHints == null) { + return; + } + for (String hint : mHints) { + switch (hint) { + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH: + case View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR: + case View.AUTOFILL_HINT_CREDIT_CARD_NUMBER: + case View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD; + break; + case View.AUTOFILL_HINT_EMAIL_ADDRESS: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; + break; + case View.AUTOFILL_HINT_PHONE: + case View.AUTOFILL_HINT_NAME: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_GENERIC; + break; + case View.AUTOFILL_HINT_PASSWORD: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_PASSWORD; + mSaveType &= ~SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS; + mSaveType &= ~SaveInfo.SAVE_DATA_TYPE_USERNAME; + break; + case View.AUTOFILL_HINT_POSTAL_ADDRESS: + case View.AUTOFILL_HINT_POSTAL_CODE: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_ADDRESS; + break; + case View.AUTOFILL_HINT_USERNAME: + mSaveType |= SaveInfo.SAVE_DATA_TYPE_USERNAME; + break; + } + } + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java new file mode 100644 index 00000000..0354b989 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/AutofillFieldsCollection.java @@ -0,0 +1,70 @@ +/* + * 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.view.autofill.AutofillId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +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 mSaveType = 0; + + public void add(AutofillField autofillField) { + mSaveType |= autofillField.getSaveType(); + size++; + mAutofillIds.add(autofillField.getId()); + List<String> hintsList = Arrays.asList(autofillField.getHints()); + mAllAutofillHints.addAll(hintsList); + if (autofillField.isFocused()) { + mFocusedAutofillHints.addAll(hintsList); + } + for (String hint : autofillField.getHints()) { + if (mAutofillHintsToFieldsMap.get(hint) == null) { + mAutofillHintsToFieldsMap.put(hint, new ArrayList<AutofillField>()); + } + mAutofillHintsToFieldsMap.get(hint).add(autofillField); + } + } + + public int getSaveType() { + return mSaveType; + } + + public AutofillId[] getAutofillIds() { + return mAutofillIds.toArray(new AutofillId[size]); + } + + public List<AutofillField> getFieldsForHint(String hint) { + return mAutofillHintsToFieldsMap.get(hint); + } + + public List<String> getFocusedHints() { + return mFocusedAutofillHints; + } + + public List<String> getAllHints() { + return mAllAutofillHints; + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java new file mode 100644 index 00000000..aa57e935 --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/ClientFormData.java @@ -0,0 +1,184 @@ +/* + * 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.service.autofill.Dataset; +import android.support.annotation.NonNull; +import android.util.Log; +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; + +/** + * 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; + + public ClientFormData() { + this(null, new HashMap<String, SavedAutofillValue>()); + } + + public ClientFormData(String datasetName, HashMap<String, SavedAutofillValue> hintMap) { + this.hintMap = hintMap; + this.datasetName = datasetName; + } + + 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; + } + } + + /** + * Returns the name of the {@link Dataset}. + */ + public String getDatasetName() { + return this.datasetName; + } + + /** + * Sets the {@link Dataset} name. + */ + public void setDatasetName(String datasetName) { + this.datasetName = datasetName; + } + + /** + * Sets values for a list of hints. + */ + public void set(@NonNull String[] autofillHints, @NonNull SavedAutofillValue autofillValue) { + if (autofillHints.length < 1) { + return; + } + for (int i = 0; i < autofillHints.length; i++) { + hintMap.put(autofillHints[i], autofillValue); + } + } + + /** + * Populates a {@link Dataset.Builder} with appropriate values for each {@link AutofillId} + * in a {@code AutofillFieldsCollection}. + */ + public boolean applyToFields(AutofillFieldsCollection autofillFieldsCollection, + Dataset.Builder datasetBuilder) { + boolean setValueAtLeastOnce = false; + List<String> allHints = autofillFieldsCollection.getAllHints(); + for (int hintIndex = 0; hintIndex < allHints.size(); hintIndex++) { + String hint = allHints.get(hintIndex); + List<AutofillField> autofillFields = autofillFieldsCollection.getFieldsForHint(hint); + if (autofillFields == null) { + continue; + } + for (int autofillFieldIndex = 0; autofillFieldIndex < autofillFields.size(); autofillFieldIndex++) { + AutofillField autofillField = autofillFields.get(autofillFieldIndex); + AutofillId autofillId = autofillField.getId(); + int autofillType = autofillField.getAutofillType(); + SavedAutofillValue savedAutofillValue = hintMap.get(hint); + switch (autofillType) { + case View.AUTOFILL_TYPE_LIST: + int listValue = autofillField.getAutofillOptionIndex(savedAutofillValue.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) { + datasetBuilder.setValue(autofillId, AutofillValue.forDate(dateValue)); + setValueAtLeastOnce = true; + } + break; + case View.AUTOFILL_TYPE_TEXT: + String textValue = savedAutofillValue.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(); + datasetBuilder.setValue(autofillId, AutofillValue.forToggle(toggleValue)); + setValueAtLeastOnce = true; + } + break; + case View.AUTOFILL_TYPE_NONE: + default: + Log.w(TAG, "Invalid autofill type - " + autofillType); + break; + } + } + } + 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()) { + return true; + } + } + return false; + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java new file mode 100644 index 00000000..73e0c81e --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/model/SavedAutofillValue.java @@ -0,0 +1,135 @@ +/* + * 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/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/MyPreferences.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/MyPreferences.java new file mode 100644 index 00000000..3926530e --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/MyPreferences.java @@ -0,0 +1,91 @@ +/* + * 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.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.support.annotation.NonNull; + +public class MyPreferences { + private static final String TAG = "MyPreferences"; + + private static final String RESPONSE_AUTH_KEY = "response_auth"; + private static final String DATASET_AUTH_KEY = "dataset_auth"; + private static final String MASTER_PASSWORD_KEY = "master_password"; + + private static MyPreferences sInstance; + private final SharedPreferences mPrefs; + + private MyPreferences(Context context) { + mPrefs = context.getApplicationContext().getSharedPreferences("my-settings", + Context.MODE_PRIVATE); + } + + public static MyPreferences getInstance(Context context) { + if (sInstance == null) { + sInstance = new MyPreferences(context); + } + return sInstance; + } + + /** + * Gets whether {@link FillResponse}s should require authentication. + */ + public boolean isResponseAuth() { + return mPrefs.getBoolean(RESPONSE_AUTH_KEY, false); + } + + /** + * Enables/disables authentication for the entire autofill {@link FillResponse}. + */ + public void setResponseAuth(boolean responseAuth) { + mPrefs.edit().putBoolean(RESPONSE_AUTH_KEY, responseAuth).apply(); + } + + /** + * Gets whether {@link Dataset}s should require authentication. + */ + public boolean isDatasetAuth() { + return mPrefs.getBoolean(DATASET_AUTH_KEY, false); + } + + /** + * Enables/disables authentication for individual autofill {@link Dataset}s. + */ + public void setDatasetAuth(boolean datasetAuth) { + mPrefs.edit().putBoolean(DATASET_AUTH_KEY, datasetAuth).apply(); + } + + /** + * Gets autofill master username. + */ + public String getMasterPassword() { + return mPrefs.getString(MASTER_PASSWORD_KEY, null); + } + + /** + * Sets autofill master password. + */ + public void setMasterPassword(@NonNull String masterPassword) { + mPrefs.edit().putString(MASTER_PASSWORD_KEY, masterPassword).apply(); + } + + public void clearCredentials() { + mPrefs.edit().remove(MASTER_PASSWORD_KEY).apply(); + } +} diff --git a/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java new file mode 100644 index 00000000..6387d36b --- /dev/null +++ b/input/autofill/AutofillFramework/kotlinApp/Application/src/main/java/com/example/android/autofillframework/service/settings/SettingsActivity.java @@ -0,0 +1,176 @@ +/* + * 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.settings; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; + +import com.example.android.autofillframework.R; +import com.example.android.autofillframework.service.datasource.LocalAutofillRepository; + +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, + R.id.settings_auth_responses_label, + R.id.settings_auth_responses_switch, + preferences.isResponseAuth(), + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + preferences.setResponseAuth(b); + } + }); + setupSettingsSwitch(R.id.settings_auth_datasets_container, + R.id.settings_auth_datasets_label, + R.id.settings_auth_datasets_switch, + preferences.isDatasetAuth(), + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + preferences.setDatasetAuth(b); + } + }); + setupSettingsButton(R.id.settings_clear_data_container, + R.id.settings_clear_data_label, + R.id.settings_clear_data_icon, + new View.OnClickListener() { + @Override + public void onClick(View view) { + buildClearDataDialog().show(); + } + }); + + setupSettingsButton(R.id.settings_auth_credentials_container, + R.id.settings_auth_credentials_label, + R.id.settings_auth_credentials_icon, + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (preferences.getMasterPassword() != null) { + buildCurrentCredentialsDialog().show(); + } else { + buildNewCredentialsDialog().show(); + } + } + }); + } + + private AlertDialog buildClearDataDialog() { + return new AlertDialog.Builder(SettingsActivity.this) + .setMessage(R.string.settings_clear_data_confirmation) + .setTitle(R.string.settings_clear_data_confirmation_title) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LocalAutofillRepository.getInstance + (SettingsActivity.this).clear(); + MyPreferences.getInstance(SettingsActivity.this) + .clearCredentials(); + dialog.dismiss(); + } + }) + .create(); + } + + private AlertDialog.Builder prepareCredentialsDialog() { + return new AlertDialog.Builder(SettingsActivity.this) + .setTitle(R.string.settings_auth_change_credentials_title) + .setNegativeButton(R.string.cancel, null); + } + + private AlertDialog buildCurrentCredentialsDialog() { + final EditText currentPasswordField = LayoutInflater + .from(SettingsActivity.this) + .inflate(R.layout.settings_authentication_dialog, null) + .findViewById(R.id.master_password_field); + return prepareCredentialsDialog() + .setMessage(R.string.settings_auth_enter_current_password) + .setView(currentPasswordField) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = currentPasswordField.getText().toString(); + if (MyPreferences.getInstance(SettingsActivity.this).getMasterPassword() + .equals(password)) { + buildNewCredentialsDialog().show(); + dialog.dismiss(); + } + } + }) + .create(); + } + + private AlertDialog buildNewCredentialsDialog() { + final EditText newPasswordField = LayoutInflater + .from(SettingsActivity.this) + .inflate(R.layout.settings_authentication_dialog, null) + .findViewById(R.id.master_password_field); + return prepareCredentialsDialog() + .setMessage(R.string.settings_auth_enter_new_password) + .setView(newPasswordField) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = newPasswordField.getText().toString(); + MyPreferences.getInstance(SettingsActivity.this).setMasterPassword(password); + dialog.dismiss(); + } + }) + .create(); + } + + private void setupSettingsSwitch(int containerId, int labelId, int switchId, boolean checked, + CompoundButton.OnCheckedChangeListener checkedChangeListener) { + ViewGroup container = (ViewGroup) findViewById(containerId); + String switchLabel = ((TextView) container.findViewById(labelId)).getText().toString(); + final Switch switchView = container.findViewById(switchId); + switchView.setContentDescription(switchLabel); + switchView.setChecked(checked); + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + switchView.performClick(); + } + }); + switchView.setOnCheckedChangeListener(checkedChangeListener); + } + + private void setupSettingsButton(int containerId, int labelId, int imageViewId, + final View.OnClickListener onClickListener) { + ViewGroup container = (ViewGroup) findViewById(containerId); + String buttonLabel = ((TextView) container.findViewById(labelId)).getText().toString(); + final ImageView imageView = container.findViewById(imageViewId); + imageView.setContentDescription(buttonLabel); + container.setOnClickListener(onClickListener); + } +} |