1 /*
2  * Copyright 2024 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 com.android.photopicker.core
18 
19 import android.content.Context
20 import android.os.Process
21 import android.os.UserHandle
22 import android.util.Log
23 import com.android.photopicker.core.configuration.ConfigurationManager
24 import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
25 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
26 import com.android.photopicker.core.events.Events
27 import com.android.photopicker.core.features.FeatureManager
28 import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
29 import com.android.photopicker.core.selection.Selection
30 import com.android.photopicker.core.selection.SelectionImpl
31 import com.android.photopicker.core.selection.SelectionStrategy
32 import com.android.photopicker.core.selection.SelectionStrategy.Companion.determineSelectionStrategy
33 import com.android.photopicker.core.user.UserMonitor
34 import com.android.photopicker.data.DataService
35 import com.android.photopicker.data.DataServiceImpl
36 import com.android.photopicker.data.MediaProviderClient
37 import com.android.photopicker.data.NotificationService
38 import com.android.photopicker.data.NotificationServiceImpl
39 import com.android.photopicker.data.model.Media
40 import dagger.Module
41 import dagger.Provides
42 import dagger.hilt.InstallIn
43 import dagger.hilt.android.ActivityRetainedLifecycle
44 import dagger.hilt.android.components.ActivityRetainedComponent
45 import dagger.hilt.android.qualifiers.ApplicationContext
46 import dagger.hilt.android.scopes.ActivityRetainedScoped
47 import kotlinx.coroutines.CoroutineDispatcher
48 import kotlinx.coroutines.CoroutineScope
49 import kotlinx.coroutines.SupervisorJob
50 import kotlinx.coroutines.cancel
51 
52 /**
53  * Injection Module that provides access to objects bound to the ActivityRetainedScope.
54  *
55  * These can be injected by requesting the type with the [@ActivityRetainedScoped] qualifier.
56  *
57  * The module outlives the individual activities (and survives configuration changes), but is bound
58  * to a single Photopicker session.
59  *
60  * Note: Jobs that are launched in the [CoroutineScope] provided by this module will be
61  * automatically cancelled when the ActivityRetainedScope is ended.
62  */
63 @Module
64 @InstallIn(ActivityRetainedComponent::class)
65 class ActivityModule {
66 
67     companion object {
68         val TAG: String = "PhotopickerActivityModule"
69     }
70 
71     // Avoid initialization until it's actually needed.
72     private lateinit var backgroundScope: CoroutineScope
73     private lateinit var configurationManager: ConfigurationManager
74     private lateinit var dataService: DataService
75     private lateinit var events: Events
76     private lateinit var featureManager: FeatureManager
77     private lateinit var mainScope: CoroutineScope
78     private lateinit var notificationService: NotificationService
79     private lateinit var selection: Selection<Media>
80     private lateinit var userMonitor: UserMonitor
81 
82     /** Provider for a @Background Dispatcher [CoroutineScope]. */
83     @Provides
84     @ActivityRetainedScoped
85     @Background
provideBackgroundScopenull86     fun provideBackgroundScope(
87         @Background dispatcher: CoroutineDispatcher,
88         activityRetainedLifecycle: ActivityRetainedLifecycle
89     ): CoroutineScope {
90         if (::backgroundScope.isInitialized) {
91             return backgroundScope
92         } else {
93             Log.d(TAG, "Initializing background scope.")
94             backgroundScope = CoroutineScope(SupervisorJob() + dispatcher)
95             activityRetainedLifecycle.addOnClearedListener {
96                 Log.d(TAG, "Activity retained lifecycle is ending, cancelling background scope.")
97                 backgroundScope.cancel()
98             }
99             return backgroundScope
100         }
101     }
102 
103     /** Provider for the [ConfigurationManager]. */
104     @Provides
105     @ActivityRetainedScoped
provideConfigurationManagernull106     fun provideConfigurationManager(
107         @ActivityRetainedScoped @Background scope: CoroutineScope,
108         @Background dispatcher: CoroutineDispatcher,
109     ): ConfigurationManager {
110         if (::configurationManager.isInitialized) {
111             return configurationManager
112         } else {
113             Log.d(
114                 ConfigurationManager.TAG,
115                 "ConfigurationManager requested but not yet initialized." +
116                     " Initializing ConfigurationManager."
117             )
118             configurationManager =
119                 ConfigurationManager(
120                     /* runtimeEnv= */ PhotopickerRuntimeEnv.ACTIVITY,
121                     /* scope= */ scope,
122                     /* dispatcher= */ dispatcher,
123                     /* deviceConfigProxy= */ DeviceConfigProxyImpl(),
124                 )
125             return configurationManager
126         }
127     }
128 
129     /**
130      * Provider method for [DataService]. This is lazily initialized only when requested to save on
131      * initialization costs of this module.
132      *
133      * Ideally this should not be limited to an Activity scope. This is planned to be changed soon.
134      */
135     @Provides
136     @ActivityRetainedScoped
provideDataServicenull137     fun provideDataService(
138         @ActivityRetainedScoped @Background scope: CoroutineScope,
139         @Background dispatcher: CoroutineDispatcher,
140         @ActivityRetainedScoped userMonitor: UserMonitor,
141         @ActivityRetainedScoped notificationService: NotificationService,
142         @ActivityRetainedScoped configurationManager: ConfigurationManager,
143         @ActivityRetainedScoped featureManager: FeatureManager
144     ): DataService {
145         if (!::dataService.isInitialized) {
146             Log.d(
147                 DataService.TAG,
148                 "DataService requested but not yet initialized. Initializing DataService."
149             )
150             dataService =
151                 DataServiceImpl(
152                     userMonitor.userStatus,
153                     scope,
154                     dispatcher,
155                     notificationService,
156                     MediaProviderClient(),
157                     configurationManager.configuration,
158                     featureManager
159                 )
160         }
161         return dataService
162     }
163 
164     /**
165      * Provider method for [Events]. This is lazily initialized only when requested to save on
166      * initialization costs of this module.
167      */
168     @Provides
169     @ActivityRetainedScoped
provideEventsnull170     fun provideEvents(
171         @Background scope: CoroutineScope,
172         featureManager: FeatureManager,
173         configurationManager: ConfigurationManager,
174     ): Events {
175         if (::events.isInitialized) {
176             return events
177         } else {
178             Log.d(Events.TAG, "Events requested but not yet initialized. Initializing Events.")
179             return Events(scope, configurationManager.configuration, featureManager)
180         }
181     }
182 
183     @Provides
184     @ActivityRetainedScoped
provideFeatureManagernull185     fun provideFeatureManager(
186         @ActivityRetainedScoped @Background scope: CoroutineScope,
187         @ActivityRetainedScoped configurationManager: ConfigurationManager,
188     ): FeatureManager {
189 
190         if (::featureManager.isInitialized) {
191             return featureManager
192         } else {
193             Log.d(
194                 FeatureManager.TAG,
195                 "FeatureManager requested but not yet initialized. Initializing FeatureManager."
196             )
197             featureManager =
198                 // Do not pass a set of FeatureRegistrations here to use the standard set of
199                 // enabled features.
200                 FeatureManager(
201                     configurationManager.configuration,
202                     scope,
203                 )
204             return featureManager
205         }
206     }
207 
208     /** Provider for a @Main Dispatcher [CoroutineScope]. */
209     @Provides
210     @ActivityRetainedScoped
211     @Main
provideMainScopenull212     fun provideMainScope(
213         @Main dispatcher: CoroutineDispatcher,
214         activityRetainedLifecycle: ActivityRetainedLifecycle
215     ): CoroutineScope {
216 
217         if (::mainScope.isInitialized) {
218             return mainScope
219         } else {
220             Log.d(TAG, "Initializing main scope.")
221             mainScope = CoroutineScope(SupervisorJob() + dispatcher)
222             activityRetainedLifecycle.addOnClearedListener {
223                 Log.d(TAG, "Activity retained lifecycle is ending, cancelling main scope.")
224                 mainScope.cancel()
225             }
226             return mainScope
227         }
228     }
229 
230     @Provides
231     @ActivityRetainedScoped
provideNotificationServicenull232     fun provideNotificationService(): NotificationService {
233 
234         if (!::notificationService.isInitialized) {
235             Log.d(
236                 NotificationService.TAG,
237                 "NotificationService requested but not yet initialized. " +
238                     "Initializing NotificationService."
239             )
240             notificationService = NotificationServiceImpl()
241         }
242         return notificationService
243     }
244 
245     @Provides
246     @ActivityRetainedScoped
provideSelectionnull247     fun provideSelection(
248         @ActivityRetainedScoped @Background scope: CoroutineScope,
249         configurationManager: ConfigurationManager,
250     ): Selection<Media> {
251 
252         if (::selection.isInitialized) {
253             return selection
254         } else {
255             Log.d(TAG, "Initializing selection.")
256             selection =
257                 when (determineSelectionStrategy(configurationManager.configuration.value)) {
258                     SelectionStrategy.GRANTS_AWARE_SELECTION ->
259                         GrantsAwareSelectionImpl(
260                             scope = scope,
261                             configuration = configurationManager.configuration,
262                         )
263 
264                     SelectionStrategy.DEFAULT ->
265                         SelectionImpl(
266                             scope = scope,
267                             configuration = configurationManager.configuration,
268                         )
269                 }
270             return selection
271         }
272     }
273 
274     /** Provides the UserHandle of the current process owner. */
275     @Provides
276     @ActivityRetainedScoped
provideUserHandlenull277     fun provideUserHandle(): UserHandle {
278         return Process.myUserHandle()
279     }
280 
281     /** Provider for the [UserMonitor]. This is lazily initialized only when requested. */
282     @Provides
283     @ActivityRetainedScoped
provideUserMonitornull284     fun provideUserMonitor(
285         @ApplicationContext context: Context,
286         @ActivityRetainedScoped configurationManager: ConfigurationManager,
287         @ActivityRetainedScoped @Background scope: CoroutineScope,
288         @Background dispatcher: CoroutineDispatcher,
289         @ActivityRetainedScoped handle: UserHandle,
290     ): UserMonitor {
291         if (::userMonitor.isInitialized) {
292             return userMonitor
293         } else {
294             Log.d(
295                 UserMonitor.TAG,
296                 "UserMonitor requested but not yet initialized. Initializing UserMonitor."
297             )
298             userMonitor =
299                 UserMonitor(context, configurationManager.configuration, scope, dispatcher, handle)
300             return userMonitor
301         }
302     }
303 }
304