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 }