Curtains
Lift the curtain on Android Windows!
Curtains provides centralized APIs for dealing with Android windows.
Here are a few use cases that Curtains enables:
- Intercepting touch events on all activities and dialogs: for logging, detecting frozen frames on touch, fixing known bugs or ignoring touch events during transitions.
- Knowing when root views are detached, e.g. to detect if they might be leaking (LeakCanary).
- Listing all attached root views for debugging (Radiography) or test purposes (Espresso).
Table of contents
Usage
Add the curtains
dependency to your library or app's build.gradle
file:
dependencies {
implementation 'com.squareup.curtains:curtains:1.2.2'
}
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 intercept key events:
window.keyEventInterceptors += KeyEventInterceptor { 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")
}
}
}
}
}
}
FAQ
What's an Android window anyway?
No one knows exactly. Here are some window facts:
- Every floating thing you see on your phone 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.
- Every window is associated to a surface, in which a view hierarchy can draw.
- Every window is associated to an input event socket. As touch events come in, the window manager service dispatches them to the right window and corresponding input event socket.
- Android apps don't have anything that represents the concept of a window within their own process. That concept lives within the WindowManager service which sits in the
system_server
process. - The Android Framework offers an API to create a new Window: WindowManager.addView(). Notice how the API to create a window is named
addView()
. This means please create a window and let this view be the root of its view hierarchy. - All standard Android components (Activity, dialog, menus) take care of creating a window for you.
- android.view.Window is not a window. It provides shared helper code and public API surface for Activity, Dialog and DreamService (lol). This is important: some Android widgets create floating windows using a
Dialog
(which wraps aandroid.view.Window
) while others use a PopupWindow.android.widget.PopupWindow
is entirely separate fromandroid.view.Window
. - Inside an Android app, the class that best represents a window is ViewRootImpl. Every call to
WindowManager.addView()
triggers the creation of a newViewRootImpl
instance which sits in between WindowManager and the view provided toWindowManager.addView()
. This class is internal and you will be yelled at if you mess with it.
Will this library break my app?
First things first, see the License: unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
The hooks leveraged by this library are also used by Espresso, which makes it unlikely that they'll break in the future. On top of that, Curtains has comprehensive UI test coverage across API levels 16 to 30.
Does the Android Framework provide official APIs we can use instead of this?
Sadly, no.
Android developers are never in control of the entirety of their code:
- App developers constantly leverage 3rd party libraries and work in code bases which high complexity and many collaborators.
- Library developers write code that gets integrated within app code they do not control.
Android developers need APIs to manage components in a centralized way, unfortunately, the Android Framework lacks many such APIs: tracking the lifecycle of Android windows (e.g. you can't know if a library shows a dialog), tracking the lifecycle of Android manifest components (services, providers, broadcast receiver) or accessing view state without subclassing.
Who named this library?
I (@pyricau) initially named it vasistas but that was too hard to pronounce for English speakers. Christina Lee suggested that curtains are useful add-ons to windows in the real world and hence this library is now Curtains.
License
Copyright 2021 Square Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.