1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.android.autofillframework.app 17 18 import android.content.Context 19 import android.graphics.Canvas 20 import android.graphics.Color 21 import android.graphics.Paint 22 import android.graphics.Paint.Style 23 import android.graphics.Rect 24 import android.util.AttributeSet 25 import android.util.Log 26 import android.util.SparseArray 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.ViewStructure 30 import android.view.autofill.AutofillManager 31 import android.view.autofill.AutofillValue 32 import android.widget.EditText 33 import android.widget.TextView 34 import com.example.android.autofillframework.CommonUtil.TAG 35 import com.example.android.autofillframework.CommonUtil.bundleToString 36 import com.example.android.autofillframework.R 37 import java.util.Arrays 38 39 40 /** 41 * Custom View with virtual child views for Username/Password text fields. 42 */ 43 class CustomVirtualView(context: Context, attrs: AttributeSet) : View(context, attrs) { 44 45 val usernameText: CharSequence 46 get() = usernameLine.fieldTextItem.text 47 val passwordText: CharSequence 48 get() = passwordLine.fieldTextItem.text 49 private var nextId: Int = 0 50 private val lines = ArrayList<Line>() 51 private val items = SparseArray<Item>() 52 private val autofillManager = context.getSystemService(AutofillManager::class.java) 53 private var focusedLine: Line? = null 54 private val textHeight = 90 <lambda>null55 private val textPaint = Paint().apply { 56 style = Style.FILL 57 textSize = textHeight.toFloat() 58 } 59 private val topMargin = 100 60 private val leftMargin = 100 61 private val verticalGap = 10 62 private val lineLength = textHeight + verticalGap 63 private val focusedColor = Color.RED 64 private val unfocusedColor = Color.BLACK 65 private val usernameLine = addLine("usernameField", context.getString(R.string.username_label), 66 arrayOf(View.AUTOFILL_HINT_USERNAME), " ", true) 67 private val passwordLine = addLine("passwordField", context.getString(R.string.password_label), 68 arrayOf(View.AUTOFILL_HINT_PASSWORD), " ", false) 69 autofillnull70 override fun autofill(values: SparseArray<AutofillValue>) { 71 // User has just selected a Dataset from the list of autofill suggestions. 72 // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant 73 // to fill a specific autofillable view. Now we have to update the UI based on the 74 // AutofillValues in the list. 75 Log.d(TAG, "autofill(): " + values) 76 for (i in 0 until values.size()) { 77 val id = values.keyAt(i) 78 val value = values.valueAt(i) 79 items[id]?.apply { 80 if (editable) { 81 // Set the item's text to the text wrapped in the AutofillValue. 82 text = value.textValue 83 } else { 84 Log.w(TAG, "Item for autofillId $id is not editable: $this") 85 } 86 } 87 } 88 postInvalidate() 89 } 90 onProvideAutofillVirtualStructurenull91 override fun onProvideAutofillVirtualStructure(structure: ViewStructure, flags: Int) { 92 // Build a ViewStructure to pack in AutoFillService requests. 93 structure.setClassName(javaClass.name) 94 val childrenSize = items.size() 95 Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = " 96 + childrenSize + ", extras: " + bundleToString(structure.extras)) 97 var index = structure.addChildCount(childrenSize) 98 for (i in 0 until childrenSize) { 99 val item = items.valueAt(i) 100 Log.d(TAG, "Adding new child at index $index: $item") 101 structure.newChild(index).apply { 102 setAutofillId(structure.autofillId, item.id) 103 setAutofillHints(item.hints) 104 setAutofillType(item.type) 105 setDataIsSensitive(!item.sanitized) 106 setAutofillValue(AutofillValue.forText(item.text)) 107 setFocused(item.focused) 108 setId(item.id, context.packageName, null, item.line.idEntry) 109 setClassName(item.className) 110 } 111 index++ 112 } 113 } 114 onDrawnull115 override fun onDraw(canvas: Canvas) { 116 super.onDraw(canvas) 117 118 Log.d(TAG, "onDraw: " + lines.size + " lines; canvas:" + canvas) 119 var x: Float 120 var y = (topMargin + lineLength).toFloat() 121 122 lines.forEach { 123 x = leftMargin.toFloat() 124 Log.v(TAG, "Drawing $it at x=$x, y=$y") 125 textPaint.color = if (it.fieldTextItem.focused) focusedColor else unfocusedColor 126 val readOnlyText = it.labelItem.text.toString() + ": [" 127 val writeText = it.fieldTextItem.text.toString() + "]" 128 // Paints the label first... 129 canvas.drawText(readOnlyText, x, y, textPaint) 130 // ...then paints the edit text and sets the proper boundary 131 val deltaX = textPaint.measureText(readOnlyText) 132 x += deltaX 133 it.bounds.set(x.toInt(), (y - lineLength).toInt(), 134 (x + textPaint.measureText(writeText)).toInt(), y.toInt()) 135 Log.d(TAG, "setBounds(" + x + ", " + y + "): " + it.bounds) 136 canvas.drawText(writeText, x, y, textPaint) 137 y += lineLength.toFloat() 138 } 139 } 140 onTouchEventnull141 override fun onTouchEvent(event: MotionEvent): Boolean { 142 val y = event.y.toInt() 143 Log.d(TAG, "Touched: y=$y, range=$lineLength, top=$topMargin") 144 var lowerY = topMargin 145 var upperY = -1 146 for (line in lines) { 147 upperY = lowerY + lineLength 148 Log.d(TAG, "Line $line ranges from $lowerY to $upperY") 149 if (y in lowerY..upperY) { 150 Log.d(TAG, "Removing focus from " + focusedLine) 151 focusedLine?.changeFocus(false) 152 Log.d(TAG, "Changing focus to " + line) 153 focusedLine = line.apply { changeFocus(true) } 154 invalidate() 155 break 156 } 157 lowerY += lineLength 158 } 159 return super.onTouchEvent(event) 160 } 161 resetFieldsnull162 fun resetFields() { 163 usernameLine.reset() 164 passwordLine.reset() 165 postInvalidate() 166 } 167 addLinenull168 private fun addLine(idEntry: String, label: String, hints: Array<String>, text: String, 169 sanitized: Boolean) = Line(idEntry, label, hints, text, sanitized).also { 170 lines.add(it) 171 items.apply { 172 put(it.labelItem.id, it.labelItem) 173 put(it.fieldTextItem.id, it.fieldTextItem) 174 } 175 } 176 177 private inner class Item internal constructor( 178 val line: Line, 179 val id: Int, 180 val hints: Array<String>?, 181 val type: Int, var text: CharSequence, val editable: Boolean, 182 val sanitized: Boolean) { 183 var focused = false 184 toStringnull185 override fun toString(): String { 186 return id.toString() + ": " + text + if (editable) 187 " (editable)" 188 else 189 " (read-only)" + if (sanitized) " (sanitized)" else " (sensitive" 190 } 191 192 val className: String 193 get() = if (editable) EditText::class.java.name else TextView::class.java.name 194 } 195 196 private inner class Line constructor(val idEntry: String, label: String, hints: Array<String>, 197 text: String, sanitized: Boolean) { 198 199 // Boundaries of the text field, relative to the CustomView 200 internal val bounds = Rect() 201 var labelItem: Item = Item(this, ++nextId, null, View.AUTOFILL_TYPE_NONE, label, false, true) 202 var fieldTextItem: Item = Item(this, ++nextId, hints, View.AUTOFILL_TYPE_TEXT, text, true, sanitized) 203 changeFocusnull204 internal fun changeFocus(focused: Boolean) { 205 fieldTextItem.focused = focused 206 if (focused) { 207 val absBounds = absCoordinates 208 Log.d(TAG, "focus gained on " + fieldTextItem.id + "; absBounds=" + absBounds) 209 autofillManager.notifyViewEntered(this@CustomVirtualView, fieldTextItem.id, absBounds) 210 } else { 211 Log.d(TAG, "focus lost on " + fieldTextItem.id) 212 autofillManager.notifyViewExited(this@CustomVirtualView, fieldTextItem.id) 213 } 214 } 215 216 private val absCoordinates: Rect 217 // Must offset the boundaries so they're relative to the CustomView. 218 get() { 219 val offset = IntArray(2) 220 getLocationOnScreen(offset) 221 val absBounds = Rect(bounds.left + offset[0], 222 bounds.top + offset[1], 223 bounds.right + offset[0], bounds.bottom + offset[1]) 224 Log.v(TAG, "absCoordinates for " + fieldTextItem.id + ": bounds=" + bounds 225 + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds) 226 return absBounds 227 } 228 resetnull229 fun reset() { 230 fieldTextItem.text = " " 231 } 232 toStringnull233 override fun toString(): String { 234 return "Label: " + labelItem + " Text: " + fieldTextItem + " Focused: " + 235 fieldTextItem.focused 236 } 237 } 238 }