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