{"id":13611684,"url":"https://github.com/square/curtains","last_synced_at":"2025-06-13T01:39:02.774Z","repository":{"id":38044753,"uuid":"335059626","full_name":"square/curtains","owner":"square","description":"Lift the curtain on Android Windows!","archived":false,"fork":false,"pushed_at":"2024-01-16T21:22:35.000Z","size":2146,"stargazers_count":607,"open_issues_count":3,"forks_count":30,"subscribers_count":13,"default_branch":"main","last_synced_at":"2025-03-30T07:02:10.315Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/square.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-02-01T19:24:16.000Z","updated_at":"2025-03-21T09:45:29.000Z","dependencies_parsed_at":"2024-01-16T23:44:56.284Z","dependency_job_id":"0a2414b6-e8f7-4a2d-94ff-c99dfef4fea6","html_url":"https://github.com/square/curtains","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fcurtains","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fcurtains/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fcurtains/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fcurtains/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/square","download_url":"https://codeload.github.com/square/curtains/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248670513,"owners_count":21142896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T19:02:00.233Z","updated_at":"2025-04-13T05:33:18.491Z","avatar_url":"https://github.com/square.png","language":"Kotlin","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"readme":"# Curtains\n\n_Lift the curtain on Android Windows!_\n\nCurtains provides centralized APIs for dealing with Android windows.\n\nHere are a few use cases that Curtains enables:\n\n* Intercepting touch events on all activities and dialogs: for logging, detecting frozen frames on touch,\nfixing known [bugs](https://issuetracker.google.com/issues/156666934) or ignoring touch events\nduring transitions.\n* Knowing when root views are detached, e.g. to detect if they might be leaking ([LeakCanary](https://github.com/square/leakcanary)).\n* Listing all attached root views for debugging ([Radiography](https://github.com/square/radiography)) or test purposes ([Espresso](https://github.com/android/android-test/blob/master/espresso/core/java/androidx/test/espresso/base/RootsOracle.java)).\n\n## Table of contents\n\n* [Usage](#usage)\n* [FAQ](#faq)\n* [License](#license)\n\n![logo_512.png](assets/logo_512.png)\n\n## Usage\n\nAdd the `curtains` dependency to your library or app's `build.gradle` file:\n\n```gradle\ndependencies {\n  implementation 'com.squareup.curtains:curtains:1.2.5'\n}\n```\n\nThe library has two main entry points, [Curtains.kt](https://github.com/square/curtains/blob/main/curtains/src/main/java/curtains/Curtains.kt) and [Windows.kt](https://github.com/square/curtains/blob/main/curtains/src/main/java/curtains/Windows.kt).\n\n### Curtains.kt\n\n[Curtains.kt](https://github.com/square/curtains/blob/main/curtains/src/main/java/curtains/Curtains.kt)\nprovides access to the current root views (`Curtains.rootViews`), as well as the ability to set\nlisteners to get notified of additions and removals:\n\n```kotlin\nCurtains.onRootViewsChangedListeners += OnRootViewsChangedListener { view, added -\u003e\n  println(\"root $view ${if (added) \"added\" else \"removed\"}\")\n}\n```\n\n### Windows.kt\n\n[Windows.kt](https://github.com/square/curtains/blob/main/curtains/src/main/java/curtains/Windows.kt)\nprovides window related extension functions.\n\nNew Android windows are created by calling\n[WindowManager.addView()](https://developer.android.com/reference/android/view/WindowManager),\nand the Android Framework calls `WindowManager.addView()` for you in many different places.\n`View.windowType` helps figure out what widget added a root view:\n\n```kotlin\nwhen(view.windowType) {\n  PHONE_WINDOW -\u003e TODO(\"View attached to an Activity or Dialog\")\n  POPUP_WINDOW -\u003e TODO(\"View attached to a PopupWindow\")\n  TOOLTIP -\u003e TODO(\"View attached to a tooltip\")\n  TOAST -\u003e TODO(\"View attached to a toast\")\n  UNKNOWN -\u003e TODO(\"?!? is this view attached? Is this Android 42?\")\n}\n```\n\nIf `View.windowType` returns `PHONE_WINDOW`, you can then retrieve the corresponding\n`android.view.Window` instance:\n\n[Windows.kt](https://github.com/square/curtains/blob/main/curtains/src/main/java/curtains/Windows.kt)\nprovides window related extension functions.\n\n```kotlin\nval window: Window? = view.phoneWindow\n```\n\nOnce you have a `android.view.Window` instance, you can easily intercept touch events:\n\n```kotlin\nwindow.touchEventInterceptors += TouchEventInterceptor { event, dispatch -\u003e\n  dispatch(event)\n}\n```\n\nOr intercept key events:\n\n```kotlin\nwindow.keyEventInterceptors += KeyEventInterceptor { event, dispatch -\u003e\n  dispatch(event)\n}\n```\n\nOr set a callback to avoid the side effects of calling Window.getDecorView() too early:\n\n```kotlin\nwindow.onDecorViewReady { decorView -\u003e\n}\n```\n\nOr react when `setContentView()` is called:\n\n```kotlin\nwindow.onContentChangedListeners += OnContentChangedListener {\n}\n```\n\n### All together\n\nWe can combine these APIs to log touch events for all `android.view.Window` instances:\n```kotlin\nclass ExampleApplication : Application() {\n  override fun onCreate() {\n    super.onCreate()\n\n    Curtains.onRootViewsChangedListeners += OnRootViewAddedListener { view -\u003e\n      view.phoneWindow?.let { window -\u003e\n        if (view.windowAttachCount == 0) {\n          window.touchEventInterceptors += OnTouchEventListener { motionEvent -\u003e\n            Log.d(\"ExampleApplication\", \"$window received $motionEvent\")\n          }\n        }\n      }\n    }\n\n  }\n}\n```\n\nOr measure the elapsed time from when a window is added to when it is fully draw:\n\n```kotlin\n// Measure the time from when a window is added to when it is fully drawn.\nclass ExampleApplication : Application() {\n  override fun onCreate() {\n    super.onCreate()\n\n    val handler = Handler(Looper.getMainLooper())\n\n    Curtains.onRootViewsChangedListeners += OnRootViewAddedListener { view -\u003e\n      view.phoneWindow?.let { window -\u003e\n        val windowAddedAt = SystemClock.uptimeMillis()\n        window.onNextDraw {\n          // Post at front to fully account for drawing time.\n          handler.postAtFrontOfQueue {\n            val duration = SystemClock.uptimeMillis() - windowAddedAt\n            Log.d(\"ExampleApplication\", \"$window fully drawn in $duration ms\")\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n## FAQ\n\n### What's an Android window anyway?\n\nNo one knows exactly. Here are some window facts:\n\n* Every floating thing you see on your phone is managed by a distinct window. Every activity, every\ndialog, every floating menu, every toast\n([until Android Q](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Toast.java;l=108-114;drc=8fe35e5f2195e416f250ba5332bce676c362b210)),\nthe status bar, the notification bar, the keyboard, the text selection toolbar, etc.\n* Every window is associated to a surface, in which a view hierarchy can draw.\n* Every window is associated to an input event socket. As touch events come in, the window manager\nservice dispatches them to the right window and corresponding input event socket.\n* Android apps don't have anything that represents the concept of a window within their\nown process. That concept lives within the WindowManager service which sits in the `system_server` process.\n* The Android Framework offers an API to create a new Window: [WindowManager.addView()](https://developer.android.com/reference/android/view/WindowManager).\nNotice how the API to create a _window_ is named `addView()`. This means _please create a window and\nlet this view be the root of its view hierarchy_.\n* All standard Android components (Activity, dialog, menus) take care of creating a window for you.\n* [android.view.Window](https://developer.android.com/reference/android/view/Window) is not a window.\nIt provides shared helper code and public API surface for [Activity](https://developer.android.com/reference/android/app/Activity), [Dialog](https://developer.android.com/reference/android/app/Dialog) and [DreamService](https://developer.android.com/reference/android/service/dreams/DreamService) (lol).\n**This is important**: some Android widgets create floating windows using a `Dialog` (which wraps a `android.view.Window`) while others use a [PopupWindow](https://developer.android.com/reference/android/widget/PopupWindow).\n`android.widget.PopupWindow` is entirely separate from `android.view.Window`.\n* Inside an Android app, the class that best represents a window is\n[ViewRootImpl](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=194;drc=d31ee388115d17c2fd337f2806b37390c7d29834).\nEvery call to `WindowManager.addView()` triggers the creation of a new `ViewRootImpl` instance which\nsits in between WindowManager and the view provided to `WindowManager.addView()`. This class is internal and\nyou will be yelled at if you mess with it.\n\n### Will this library break my app?\n\nFirst things first, see the [License](#license): unless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\nThe hooks leveraged by this library are also used by [Espresso](https://github.com/android/android-test/blob/master/espresso/core/java/androidx/test/espresso/base/RootsOracle.java),\nwhich makes it unlikely that they'll break in the future. On top of that, Curtains has\ncomprehensive UI test coverage across API levels 16 to 30.\n\n### Does the Android Framework provide official APIs we can use instead of this?\n\nSadly, no.\n\nAndroid developers are never in control of the entirety of their code:\n* App developers constantly leverage 3rd party libraries and work in code bases which high\ncomplexity and many collaborators.\n* Library developers write code that gets integrated within app code they do not control.\n\nAndroid developers need APIs to manage components in a centralized way, unfortunately, the Android\nFramework lacks many such APIs: tracking the lifecycle of Android windows (e.g. you can't know if a\nlibrary shows a dialog), tracking the lifecycle of Android manifest components (services, providers,\nbroadcast receiver) or accessing view state without subclassing.\n\n### Who named this library?\n\nI ([@pyricau](http://github.com/pyricau)) initially named it\n[vasistas](https://www.grammarphobia.com/blog/2013/11/vasistas.html) but that was too hard to\npronounce for English speakers. [Christina Lee](https://github.com/christinalee) suggested that\ncurtains are useful add-ons to windows in the real world and hence this library is now _Curtains_.\n\n## License\n\n\u003cpre\u003e\nCopyright 2021 Square Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\u003c/pre\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsquare%2Fcurtains","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsquare%2Fcurtains","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsquare%2Fcurtains/lists"}