Introducing Curtains

Lift the curtain on Android Windows!

Reddit
LinkedIn

On Android, every distinct floating thing you see on your device is managed by a distinct window. Every activity, every dialog, every floating menu, every toast (until Android Q), the status bar, the notification bar, the keyboard, the text selection toolbar, etc. That's a lot of windows! Even though Android Apps create and remove windows all the time, as an app developer the complexity of window management is hidden from you.

However, there are times where Android developers need to know about the windows created by their apps, as they're never in control of the entirety of their code:

  • App developers constantly leverage 3rd party libraries and work in code bases which have high complexity and many collaborators.
  • Library developers write code that gets integrated within app code they do not control.

At Square, we've been writing the same window helper code over and over again in our Android Apps as well as LeakCanary and Radiography. Even Espresso includes similar helper code.

Today, we’re introducing Curtains, a small library that lift the curtain on Android Windows!

logo_512.png

The library has two main entry points, Curtains.kt and Windows.kt.

Curtains.kt

Curtains.kt provides access to the current root views (Curtains.rootViews), as well as the ability to set listeners to get notified of additions and removals:

Curtains.onRootViewsChangedListeners += OnRootViewsChangedListener { view, added ->
  println("root $view ${if (added) "added" else "removed"}")
}

Windows.kt

Windows.kt provides window related extension functions.

New Android windows are created by calling WindowManager.addView(), and the Android Framework calls WindowManager.addView() for you in many different places. View.windowType helps figure out what widget added a root view:

when(view.windowType) {
  PHONE_WINDOW -> TODO("View attached to an Activity or Dialog")
  POPUP_WINDOW -> TODO("View attached to a PopupWindow")
  TOOLTIP -> TODO("View attached to a tooltip")
  TOAST -> TODO("View attached to a toast")
  UNKNOWN -> TODO("?!? is this view attached? Is this Android 42?")
}

If View.windowType returns PHONE_WINDOW, you can then retrieve the corresponding android.view.Window instance:

Windows.kt provides window related extension functions.

val window: Window? = view.phoneWindow

Once you have a android.view.Window instance, you can easily intercept touch events:

window.touchEventInterceptors += TouchEventInterceptor { event, dispatch ->
  dispatch(event)
}

Or set a callback to avoid the side effects of calling Window.getDecorView() too early:

window.onDecorViewReady { decorView ->
}

Or react when setContentView() is called:

window.onContentChangedListeners += OnContentChangedListener {
}

All together

We can combine these APIs to log touch events for all android.view.Window instances:

class ExampleApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    Curtains.onRootViewsChangedListeners += OnRootViewAddedListener { view ->
      view.phoneWindow?.let { window ->
        if (view.windowAttachCount == 0) {
          window.touchEventInterceptors += OnTouchEventListener { motionEvent ->
            Log.d("ExampleApplication", "$window received $motionEvent")
          }
        }
      }
    }
  }
}

Or measure the elapsed time from when a window is added to when it is fully draw:

// Measure the time from when a window is added to when it is fully drawn.
class ExampleApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    val handler = Handler(Looper.getMainLooper())

    Curtains.onRootViewsChangedListeners += OnRootViewAddedListener { view ->
      view.phoneWindow?.let { window ->
        val windowAddedAt = SystemClock.uptimeMillis()
        window.onNextDraw {
          // Post at front to fully account for drawing time.
          handler.postAtFrontOfQueue {
            val duration = SystemClock.uptimeMillis() - windowAddedAt
            Log.d("ExampleApplication", "$window fully drawn in $duration ms")
          }
        }
      }
    }
  }
}

To get started using Curtains, checkout the README on GitHub for Gradle instructions and more information on available APIs. Bug reports and feature requests are welcome as well!