Home | History | Annotate | Download | only in app
      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
     55     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 
     70     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 
     91     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 
    115     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 
    141     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 
    162     fun resetFields() {
    163         usernameLine.reset()
    164         passwordLine.reset()
    165         postInvalidate()
    166     }
    167 
    168     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 
    185         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 
    204         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 
    229         fun reset() {
    230             fieldTextItem.text = "        "
    231         }
    232 
    233         override fun toString(): String {
    234             return "Label: " + labelItem + " Text: " + fieldTextItem + " Focused: " +
    235                     fieldTextItem.focused
    236         }
    237     }
    238 }