Autofill Fields with a Kotlin Script

Written by leonidivankin | Published 2022/08/15
Tech Story Tags: android | kotlin | android-app-development | android-apps | android-framework | mobile-app-development | mobile-apps

TLDRThe solution is suitable for filling fields of xml, compose, webView and other visual applications. Running a kotlin script from a scratch file sometimes causes an error.via the TL;DR App

Anyone who has encountered non-trivial bugs on android probably knows that, sometimes, it takes a lot of time and effort to fix them. Other times, the situation is even worse - it takes a lot of time to reproduce the bug. There are cases when there is nothing to do but restart the application under different conditions: with a network, without a network, without cache, with a bad network, with erroneous responses from the server, etc.

The situation can be aggravated by the fact that constant restarts require a lot of long single-type actions, e.g. filling fields with data. In this article, I want to describe one possible solution to this problem. Let’s get to it

Observations and Assumptions

  • It is assumed that you are familiar with adb

  • This solution is suitable for filling fields of XML, compose, webView, and other visual types as well as other applications.

  • Running a kotlin script from a scratch file sometimes causes an error. There is currently no clean solution. A quick way around the problem is to create another scratch file.

Selecting an example

Let's say we have an application that consists of two activities: LoginActivity and MainActivity. To go to MainActivity, you need to fill in the fields in LoginActivity and press Enter:

Code:

package com.leonidivankin.draftandroid.articles.autofill

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "MainActivity"
    }
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="24dp"
    tools:context=".articles.autofill.LoginActivity">

    <EditText
        android:id="@+id/editTextEmail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="[email protected]"
        app:layout_constraintBottom_toTopOf="@+id/editTextPhone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <EditText
        android:id="@+id/editTextPhone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="+1 650 123 4567"
        app:layout_constraintBottom_toTopOf="@+id/editTextPassword"
        app:layout_constraintTop_toBottomOf="@+id/editTextEmail" />

    <EditText
        android:id="@+id/editTextPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="123456"
        app:layout_constraintBottom_toTopOf="@+id/buttonEnter"
        app:layout_constraintTop_toBottomOf="@+id/editTextPhone" />

    <Button
        android:id="@+id/buttonEnter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="enter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editTextPassword" />

</androidx.constraintlayout.widget.ConstraintLayout>

package com.leonidivankin.draftandroid.articles.autofill

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.leonidivankin.draftandroid.databinding.ActivityLoginBinding

class LoginActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLoginBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)
        title = "LoginActivity"

        binding.buttonEnter.setOnClickListener {
            if (binding.editTextEmail.text.toString() == "[email protected]"
                && binding.editTextPhone.text.toString() == "+1 650 123 4567"
                && binding.editTextPassword.text.toString() == "123456"
            )
                startActivity(Intent(this, MainActivity::class.java))
        }
    }
}

To move to MainActivity, the fields must be filled in, and their values must meet certain requirements. For example, take the following values:

email: [email protected] phone: +1 650 123 4567

https://youtu.be/oX3NIZMYpyo

As you can see, it takes about 1 minute from startup to MainActivity. This is still on the assumption that there were almost no errors in filling the data. Sometimes, finding complex bugs requires multiple rebuilds of the application and restarts on the device. This means constant filling of fields to get further into the app.

It turns out that with each restart we would lose about 1 minute extra. The ideal would be to jump directly into MainActivity, bypassing LoginActivity, but in large projects, this is most often impossible due to the need to get a token from the server.

Another possible solution to this problem would be to assign default values to these fields. However, this is not always possible, because the fields may be in an external SDK or in a webView. In this case, you won't be able to set default values.

As we continue I’ll show you how you can optimize the filling of the fields with kotlin-script.

Autofill fields

Finding the necessary adb commands

I will use the adb commands to fill the text fields. To fill one text field, we need:

  1. Click on the text area.

  2. Fill it with text.

To click on a text area, you must use the adb command:

adb shell input tap x y

where x and y are coordinates relative to the upper left corner of the device in pixels (px). They can be found in different ways:

  1. There is an adb-command that knows how to determine the click coordinates:

adb shell getevent -l

But there are difficulties with the translation of coordinates. You can read more here.

  1. On the real device, you can enable the developer's settings to display click coordinates or grid.
  2. You can add a listener, which will give absolute coordinates at each click.
  3. You can also find out through Layout Inspector.

In this example, I am more comfortable using paragraph 4.

  1. Launch the Layout Inspector.

  2. Connect to the current process.

  3. Select the necessary view.

  4. Check its coordinates. It will depend on the size and type of the device. Most likely, your coordinates will be different.

  5. Note that these coordinates are in dp. And the adb command needs coordinates in px. In my case it is x: 243=72, y: 3643=1092

  6. The final command turns out to be:

adb shell input tap 72 1092
  1. Go to the terminal and enter this command:

  2. The text input field is now active.

To find out the pixel density, you need to go to Device Manager:

In my case it is xxhdi. Here the ratio between dp and px is 3. The ratios are described in more detail here.

Next, you must enter the desired text in the field. To do this, use the command:

adb shell input text '[email protected]'

Enter it into the terminal and check that the necessary email is entered. Similar actions are carried out with other text fields: phone, password. Next, we need to click on the button, for this, there is a command:

adb shell input tap 72 1497

I've already put the coordinates I need there.

We found all the necessary adb commands to fill in all the text fields and click on the button. Here is the complete list:

adb shell input tap 72 1092
adb shell input text '[email protected]'
adb shell input tap 72 1227
adb shell input text '+1 650 123 4567'
adb shell input tap 72 1362
adb shell input text 123456
adb shell input tap 72 1497

Now we need to build a script from these commands that will execute them instead of us.

Creating a kotlin script

You can build a script in different ways and in different languages. Here are some of the options:

  1. Make a bash script.

  2. Make a plugin for Android Studio.

  3. Make Gradle task, etc.

But each of these methods has disadvantages. The bash script is difficult to maintain, the plugin for Android Studio needs to be maintained for new versions of the environment, gradle tasks take a long time to run due to dependencies with gradle and gradle wrapper.

So I use another way - kotlin-script (kts). The kotlin language is more familiar to the android developer, the script is independent of the project, android studio, and gradle.

Create a scratch file using Ctrl+Alt+Shift+Insert (Windows). In the window that appears, select Kotlin:

This will create a scratch.kts file. Notice the green arrow. This means that the file is executable and can be run. Uncheck Interactive mode so that the script won't restart every time you change the file.

There is a special method that allows you to execute an adb command from a kotlin script:

Runtime.getRuntime().exec()

Insert all of our commands. My complete scratch file is as follows:

fun run(){
   run("adb shell am force-stop com.leonidivankin.draftandroid")
   run("adb shell am start -n \"com.leonidivankin.draftandroid/com.leonidivankin.draftandroid.articles.autofill.LoginActivity\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER")
   Thread.sleep(2000)
   run("adb shell input tap 72 1092")
   run("adb shell input text '[email protected]'")
   Thread.sleep(500)
   run("adb shell input tap 72 1227")
   run("adb shell input text '+1 650 123 4567'")
   Thread.sleep(500)
   run("adb shell input tap 72 1362")
   run("adb shell input text 123456")
   Thread.sleep(500)
   run("adb shell input tap 72 1497")
}

fun run(command: String){
   Runtime.getRuntime().exec(command)
}

This file consists of the adb commands we defined above with some additions.

Note that I inserted the lines first:

fun run(){
   run("adb shell am force-stop com.leonidivankin.draftandroid")
   run("adb shell am start -n \"com.leonidivankin.draftandroid/com.leonidivankin.draftandroid.articles.autofill.LoginActivity\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER")

These lines are needed when the application is running and you need to restart it.

I also put the exec() command in a separate method to reduce the amount of code:

fun run(command: String){
   Runtime.getRuntime().exec(command)
}

Note that some commands have Thread.sleep() between them. This is to wait for the application to start or the previous command to be entered. The point is that adb commands are executed asynchronously. And there is a probability that the focus will be shifted to the next text field without waiting for text input in the current one.

Total transition time to MainActivity from launching is not more than 5 seconds, which reduced the time by 20 times.

https://youtube.com/shorts/R484DB_RScc?feature=share

Conclusion

As Steve McConnell wrote in Perfect Code, programming is a complicated thing. It gets even more complicated because you have to look for complex and nontrivial bugs. So, from my point of view, the role of any optimizations is rather high. Despite the fact that within the framework of the whole project, they seem microscopic. Try to optimize the launch of your application by autocomplete fields. If you find bugs or know how else to improve the script, post in the comments.


Written by leonidivankin | I'm an android developer
Published by HackerNoon on 2022/08/15