1 /* <lambda>null2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.window.layout.adapter.extensions 18 19 import android.app.Activity 20 import android.content.Context 21 import androidx.annotation.GuardedBy 22 import androidx.annotation.UiContext 23 import androidx.annotation.VisibleForTesting 24 import androidx.core.util.Consumer 25 import androidx.window.RequiresWindowSdkExtension 26 import androidx.window.core.ConsumerAdapter 27 import androidx.window.extensions.layout.WindowLayoutComponent 28 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo 29 import androidx.window.layout.WindowLayoutInfo 30 import java.util.concurrent.Executor 31 import java.util.concurrent.locks.ReentrantLock 32 import kotlin.concurrent.withLock 33 34 @RequiresWindowSdkExtension(version = 1) 35 internal open class ExtensionWindowBackendApi1( 36 val component: WindowLayoutComponent, 37 private val consumerAdapter: ConsumerAdapter 38 ) : ExtensionWindowBackendApi0() { 39 40 private val globalLock = ReentrantLock() 41 42 @GuardedBy("globalLock") 43 private val contextToListeners = mutableMapOf<Context, MulticastConsumer>() 44 45 @GuardedBy("globalLock") 46 private val listenerToContext = mutableMapOf<Consumer<WindowLayoutInfo>, Context>() 47 48 @GuardedBy("globalLock") 49 private val consumerToToken = mutableMapOf<MulticastConsumer, ConsumerAdapter.Subscription>() 50 51 /** 52 * Registers a listener to consume new values of [WindowLayoutInfo]. If there was a listener 53 * registered for a given [Context] then the new listener will receive a replay of the last 54 * known value. 55 * 56 * @param context the host of a [android.view.Window] or an area on the screen. Has to be an 57 * [Activity] or a [UiContext] created with [Context#createWindowContext] or 58 * InputMethodService. 59 * @param executor an executor from the parent interface 60 * @param callback the listener that will receive new values 61 */ 62 override fun registerLayoutChangeCallback( 63 @UiContext context: Context, 64 executor: Executor, 65 callback: Consumer<WindowLayoutInfo> 66 ) { 67 globalLock.withLock { 68 contextToListeners[context]?.let { listener -> 69 listener.addListener(callback) 70 listenerToContext[callback] = context 71 } 72 ?: run { 73 val consumer = MulticastConsumer(context) 74 contextToListeners[context] = consumer 75 listenerToContext[callback] = context 76 consumer.addListener(callback) 77 78 // The registrations above maintain 1-many mapping of Context-Listeners across 79 // different subscription implementations. 80 val disposableToken = 81 if (context is Activity) { 82 consumerAdapter.createSubscription( 83 component, 84 OEMWindowLayoutInfo::class, 85 "addWindowLayoutInfoListener", 86 "removeWindowLayoutInfoListener", 87 context, 88 consumer::accept 89 ) 90 } else { 91 // WM Extensions v1 addWindowLayoutInfoListener only 92 // supports Activities. Return empty WindowLayoutInfo if the 93 // provided Context is not an Activity. 94 consumer.accept(OEMWindowLayoutInfo(emptyList())) 95 return@registerLayoutChangeCallback 96 } 97 consumerToToken[consumer] = disposableToken 98 } 99 } 100 } 101 102 /** 103 * Unregisters a listener, if this is the last listener for a [UiContext] then the listener is 104 * removed from the [WindowLayoutComponent]. Calling with the same listener multiple times in a 105 * row does not have an effect. 106 * 107 * @param callback a listener that may have been registered 108 */ 109 override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) { 110 globalLock.withLock { 111 val context = listenerToContext[callback] ?: return 112 val multicastListener = contextToListeners[context] ?: return 113 multicastListener.removeListener(callback) 114 listenerToContext.remove(callback) 115 if (multicastListener.isEmpty()) { 116 contextToListeners.remove(context) 117 consumerToToken.remove(multicastListener)?.dispose() 118 } 119 } 120 } 121 122 /** Returns {@code true} if all the collections are empty, {@code false} otherwise */ 123 @VisibleForTesting 124 override fun hasRegisteredListeners(): Boolean { 125 return !(contextToListeners.isEmpty() && 126 listenerToContext.isEmpty() && 127 consumerToToken.isEmpty()) 128 } 129 } 130