Lift the curtain on Android Windows!

Related tags

UI/UX curtains
Overview

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

logo_512.png

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:

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?") } ">
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 a android.view.Window) while others use a PopupWindow. android.widget.PopupWindow is entirely separate from android.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 new ViewRootImpl instance which sits in between WindowManager and the view provided to WindowManager.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.
Comments
  • Add Window.onWindowFocusChangedListeners API

    Add Window.onWindowFocusChangedListeners API

    Following up on https://github.com/square/curtains/issues/3

    The API largely continues what OnContentChangedListener established.

    Tests check that the callback isn't invoked immediately with a current value (I don't think we know it anyway). Tests check that activity focus changes after being resumed and before being paused. There isn't a test case for activity gaining focus after a dialog is dismissed. At this point we're just testing the framework instead of the library, I think. I ran tests locally on AOSP AVDs API 19 and 21-29.

    opened by consp1racy 51
  • How exactly to use

    How exactly to use "window.touchEventInterceptors" ?

    I tried to use it on a new project:

    window.touchEventInterceptors += TouchEventInterceptor { event, dispatch ->
      Log.d("AppLog", "touch")
      dispatch(event)
    }
    

    But it doesn't seem to do anything.

    See attached:

    RecordAppSample.zip

    opened by AndroidDeveloperLB 19
  • OnTouchEventListener stop working after adding inner Fragments on Activity

    OnTouchEventListener stop working after adding inner Fragments on Activity

    Hi, I'm trying to use OnTouchEventListener like this:

    class PingDispatcherLifecycleCallback : Application.ActivityLifecycleCallbacks {
        private val pingDispatcher = OnTouchEventListener { motionEvent ->
            if (motionEvent.action == KeyEvent.ACTION_DOWN) {
                Log.d("ping", "send")
            }
        }
    
        override fun onActivityResumed(activity: Activity) {
            activity.window.touchEventInterceptors += pingDispatcher
        }
    
        override fun onActivityPaused(activity: Activity) {
            activity.window.touchEventInterceptors -= pingDispatcher
        }
        ...
    }
    

    if put Fragment into Activity and add child Fragment into root Fragment It stop to receive onTouch events

    it become works after adding this callback into PingDispatcherLifecycleCallback in onActivityResumed callback

    class PingDispatcherLifecycleCallback : Application.ActivityLifecycleCallbacks {
        ...
        private val fragmentOnAttachListener = object : FragmentManager.FragmentLifecycleCallbacks() {
            override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
                window.touchEventInterceptors -= pingDispatcher
                window.touchEventInterceptors += pingDispatcher
            }
        }
    
        override fun onActivityResumed(activity: Activity) {
            supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentOnAttachListener, true)
        }
        ...
    }
    

    is it bug or legal behaviour of Android?

    opened by kuFEAR 9
  • Use Gradle 7.3.2. Log4shell mitigation.

    Use Gradle 7.3.2. Log4shell mitigation.

    Gradle 7.3.2 adds dependency constraints to the build classpath to reject known-bad versions of log4j.

    See also https://blog.gradle.org/log4j-vulnerability.

    opened by autonomousapps 4
  • enable support for dropping jetifier

    enable support for dropping jetifier

    Can we switch to using the androidx components so that consumers can drop jetifier? Since this leakcanary is dependant on this, so consumers cannot drop the jetifier.

    Currently it seems the class in question is

    android.support.v7.view.WindowCallbackWrapper

    opened by humblerookie 4
  • onWindowFocusChanged callback

    onWindowFocusChanged callback

    Hi, unless you're already working on it, i'd like to start working on a PR for onWindowFocusChanged callback.

    One question, do we leverage DispatchState (like touch events) or not (like content changed, my favorite)?

    opened by consp1racy 4
  • Add Proguard rules for WindowCallbackWrapper

    Add Proguard rules for WindowCallbackWrapper

    Copied over from https://github.com/square/leakcanary/issues/2280 by @mlilienberg

    Description

    My app is crashing when showing Dialog Fragments while R8 is turned on and with latest version 2.8.1. I think the issue is that curtains dependency does not provide correct proguard rules. curtains.internal.WindowCallbackWrapper is accessing WindowCallbackWrapper from androidx and android.support via reflection. Adding proguard rules to app level solves the issue for me but it would be better if leakcanary or curtains could define those rules.

    -keep class androidx.appcompat.view.WindowCallbackWrapper { *; }
    -keep class android.support.v7.view.WindowCallbackWrapper { *; }
    
    opened by pyricau 2
  • Support androidx.appcompat.widget.TooltipPopup as window type

    Support androidx.appcompat.widget.TooltipPopup as window type

    See https://github.com/square/curtains/pull/30#issuecomment-1005422341 for details

    View.windowType should return TOOLTIP if the window was created by androidx.appcompat.widget.TooltipPopup

    We need to do this without having a direct bytecode reference to androidx.appcompat.widget.TooltipPopup or adding a transitive dependency, as curtains needs to remain android x free.

    opened by pyricau 2
  • Bugfix: wrong WindowType because of i18n.

    Bugfix: wrong WindowType because of i18n.

    Because of i18n, the title may be not "Tooltip" in some country. For example, "提示" in Chinese.

    And the unit test tooltip_view_has_TOOLTIP_type failed.

    opened by CoXier 2
  • window.callback.wrappedCallback crashes with java.lang.NullPointerException due to R8 obfuscation of androidx.appcompat.view.WindowCallbackWrapper

    window.callback.wrappedCallback crashes with java.lang.NullPointerException due to R8 obfuscation of androidx.appcompat.view.WindowCallbackWrapper

    com.squareup.curtains:curtains:1.2.1 androidx.appcompat:appcompat:1.3.0 R8 mapping.txt: androidx.appcompat.view.WindowCallbackWrapper -> impl.g1: android.view.Window$Callback mWrapped -> c

    retraced stack trace: java.lang.NullPointerException at curtains.internal.WindowCallbackWrapper$Companion.getJetpackWrapped(WindowCallbackWrapper.kt:112) at curtains.internal.WindowCallbackWrapper$Companion.unwrap(WindowCallbackWrapper.kt:147) at curtains.WindowsKt.getWrappedCallback(Windows.kt:247)

    opened by andriykryzhko 2
  • Support library dependency?

    Support library dependency?

    Using the plugin canISayByeByeJetifier to detect libraries using Android's deprecated Support Libraries, it accuses Leak Canary as a problem because it uses Curtains. This is the issue it gives:

    curtains\internal\WindowCallbackWrapper$Companion$jetpackWrapperClass$2.class -> android.support.v7.view.WindowCallbackWrapper

    opened by FelipeRRM-Carrot 1
  • Window null for Jetpack Compose PopupLayout root view

    Window null for Jetpack Compose PopupLayout root view

    I have found a screen to have multiple root views when using Jetpack Compose DropDownMenu and the DropDownMenu is visible. The first is a standard DecorView, the second is a PopupLayout.

    When acquiring the window of the PopupLayout through rootView.phoneWindow, null is returned.

    A nudge towards the right direction to acquire the window of the PopupLayout would be greatly appreciated.

    opened by AnupKhanal 2
  • Cannot call removeOnDrawListener inside of onDraw

    Cannot call removeOnDrawListener inside of onDraw

    java.lang.IllegalStateException: Cannot call removeOnDrawListener inside of onDraw
    	at android.view.ViewTreeObserver.removeOnDrawListener(ViewTreeObserver.java:704)
    	at curtains.internal.NextDrawListener$onDraw$1.run(NextDrawListener.kt:27)
    	at android.os.Handler.handleCallback(Handler.java:790)
    	at android.os.Handler.dispatchMessage(Handler.java:99)
    	at android.os.Looper.loop(Looper.java:164)
    	at android.app.ActivityThread.main(ActivityThread.java:6523)
    	at java.lang.reflect.Method.invoke(Native Method)
    	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
    
    opened by linchangjian 3
  • WindowCallbackWrapper is removed when action bar is applied in a fragment.

    WindowCallbackWrapper is removed when action bar is applied in a fragment.

    I noticed this issue when testing an app that uses fragments to set the toolbar.

    MyFragment.kt

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onViewCreated(view, savedInstanceState)
      // This causes the activity's window callback to be reset to `mAppCompatWindowCallback`
      (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
    }
    

    The root cause can be seen in AppCompatDelegateImpl.

    AppCompatDelegateImpl.java

        @Override
       public void setSupportActionBar(Toolbar toolbar) {
          // Below the windowCallback is reset based on `mAppCompatWindowCallback` or some wrapped variant. Curtain's wrapper will be overridden.
           if (toolbar != null) {
               final ToolbarActionBar tbab = new ToolbarActionBar(toolbar, getTitle(),
                       mAppCompatWindowCallback);
               mActionBar = tbab;
               mWindow.setCallback(tbab.getWrappedWindowCallback());
           } else {
               mActionBar = null;
               // Re-set the original window callback since we may have already set a Toolbar wrapper
               mWindow.setCallback(mAppCompatWindowCallback);
           }
    
           invalidateOptionsMenu();
       }
    

    Longer explanation:

    Setting a toolbar in Activitity.onCreate() is not an issue since mWindow.setCallback(...) above runs well before the ActivityThread goes through the process of adding a new view to the global window manager.

    Curtains relies on modifications of mViews in the window manager via the spy delegator list. If setSupportActionBar runs after the view is added to the window manager, Curtains can't attach its window callback. Curtains would have to wait until addView is called again (which is not the case for fragments).

    Possible workarounds

    1. Modify mAppCompatWindowCallback to be the Curtains callback wrapper, such that any subsequent setSupportActionBar() call retains the Curtains window callback.

    2. In WindowCallbackWrapper.Companion add something to check if the callback is the Curtain's variant (or just invoke Window.listeners which does this refresh itself). Now the question is, when to best invoke this - perhaps from a fragment lifecycle listener via onFragmentStarted?

       fun Window.reapplyWindowCallbackWrapper(): Boolean {
         synchronized(listenersLock) {
           if (this.callback is WindowCallbackWrapper) return false
           val wrapperToApply = callbackCache[this]?.get() ?: WindowCallbackWrapper(this.callback)
           this.callback = wrapperToApply
           callbackCache[this] = WeakReference(wrapperToApply)
           return true
         }
       }
    
    opened by vicidroiddev 0
  • Support unwrapping from android.view.WindowCallbackWrapper

    Support unwrapping from android.view.WindowCallbackWrapper

    curtains.WindowsKt#getWrappedCallback currently supports unwrapping jetpack and curtains callbacks. We could add support for the AOSP WindowCallbackWrapper as well.

    opened by pyricau 6
Releases(v1.2.4)
Owner
Square
Square
Android library used to create an awesome Android UI based on a draggable element similar to the last YouTube graphic component.

Draggable Panel DEPRECATED. This project is not maintained anymore. Draggable Panel is an Android library created to build a draggable user interface

Pedro Vicente Gómez Sánchez 3k Dec 6, 2022
TourGuide is an Android library that aims to provide an easy way to add pointers with animations over a desired Android View

TourGuide TourGuide is an Android library. It lets you add pointer, overlay and tooltip easily, guiding users on how to use your app. Refer to the exa

Tan Jun Rong 2.6k Jan 5, 2023
Bubbles for Android is an Android library to provide chat heads capabilities on your apps. With a fast way to integrate with your development.

Bubbles for Android Bubbles for Android is an Android library to provide chat heads capabilities on your apps. With a fast way to integrate with your

Txus Ballesteros 1.5k Jan 2, 2023
View that imitates Ripple Effect on click which was introduced in Android L (for Android 2.3+)

RippleView View that imitates Ripple Effect on click which was introduced in Android L. Usage For a working implementation, Have a look at the Sample

Muthuramakrishnan Viswanathan 1.2k Dec 30, 2022
A new canvas drawing library for Android. Aims to be the Fabric.js for Android. Supports text, images, and hand/stylus drawing input. The library has a website and API docs, check it out

FabricView - A new canvas drawing library for Android. The library was born as part of a project in SD Hacks (www.sdhacks.io) on October 3rd. It is cu

Antwan Gaggi 1k Dec 13, 2022
MarkdownView is an Android webview with the capablity of loading Markdown text or file and display it as HTML, it uses MarkdownJ and extends Android webview.

About MarkdownView (Markdown For Android) is an Android library that helps you display Markdown text or files (local/remote) as formatted HTML, and st

Feras Alnatsheh 1k Dec 20, 2022
SwipeBack for Android Activities to do pretty the same as the android "back-button" will do, but in a really intuitive way by using a swipe gesture

SwipeBack SwipeBack is for Android Activities to do pretty the same as the android "back-button" will do, but in a really intuitive way by using a swi

Hannes Dorfmann 697 Dec 14, 2022
A backport of the SwitchPreference component that was introduced on Android 4 (ICS / level 14). This port works on Android 2.1+ (Eclair MR1 / level 7).

Android Switch Preference Backport A backport of the SwitchPreference component that was introduced on Android 4 (ICS / level 14). This port works on

Benoit Lubek 498 Dec 29, 2022
Wizard Pager is a library that provides an example implementation of a Wizard UI on Android, it's based of Roman Nurik's wizard pager (https://github.com/romannurik/android-wizardpager)

Wizard Pager Wizard Pager is a library that provides an example implementation of a Wizard UI on Android, it's based of Roman Nurik's wizard pager (ht

Julián Suárez 520 Nov 11, 2022
Make your native android Toasts Fancy. A library that takes the standard Android toast to the next level with a variety of styling options. Style your toast from code.

FancyToast-Android Prerequisites Add this in your root build.gradle file (not your module build.gradle file): allprojects { repositories { ... ma

Shashank Singhal 1.2k Dec 26, 2022
Make your native android Dialog Fancy. A library that takes the standard Android Dialog to the next level with a variety of styling options. Style your dialog from code.

FancyAlertDialog-Android Prerequisites Add this in your root build.gradle file (not your module build.gradle file): allprojects { repositories { ..

Shashank Singhal 350 Dec 9, 2022
Xamarin.Android provides open-source bindings of the Android SDK for use with .NET managed languages such as C#

Xamarin.Android Xamarin.Android provides open-source bindings of the Android SDK for use with .NET managed languages such as C#. Build Status Platform

Xamarin 1.8k Jan 5, 2023
A Tinder-like Android library to create the swipe cards effect. You can swipe left or right to like or dislike the content.

Swipecards Travis master: A Tinder-like cards effect as of August 2014. You can swipe left or right to like or dislike the content. The library create

Dionysis Lorentzos 2.3k Dec 9, 2022
Bootstrap style widgets for Android, with Glyph Icons

Android-Bootstrap Android Bootstrap is an Android library which provides custom views styled according to the Twitter Bootstrap Specification. This al

Bearded Hen 7.3k Jan 3, 2023
[] A fast PDF reader component for Android development

This project is no longer maintained. You can find a good replacement here, which is a fork relying on Pdfium instead of Vudroid/MuPDF for decoding PD

Joan Zapata 2.8k Dec 16, 2022
[] Define and render UI specs on top of your Android UI

dspec A simple way to define and render UI specs on top of your Android UI. Usage Enclose the target UI with a DesignSpecFrameLayout, usually the root

Lucas Rocha 561 Dec 16, 2022
A Material design Android pincode library. Supports Fingerprint.

LolliPin A Lollipop material design styled android pincode library (API 14+) To include in your project, add this to your build.gradle file: //Loll

Omada Health 1.6k Nov 25, 2022
Android Library to implement simple touch/tap/swipe gestures

SimpleFingerGestures An android library to implement simple 1 or 2 finger gestures easily Example Library The library is inside the libSFG folder Samp

Arnav Gupta 315 Dec 21, 2022