• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 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.intentresolver
18 
19 import android.app.Activity
20 import android.os.UserHandle
21 import android.provider.Settings
22 import android.util.Log
23 import androidx.activity.ComponentActivity
24 import androidx.activity.viewModels
25 import androidx.lifecycle.DefaultLifecycleObserver
26 import androidx.lifecycle.Lifecycle
27 import androidx.lifecycle.LifecycleOwner
28 import androidx.lifecycle.lifecycleScope
29 import androidx.lifecycle.repeatOnLifecycle
30 import com.android.intentresolver.Flags.interactiveSession
31 import com.android.intentresolver.Flags.unselectFinalItem
32 import com.android.intentresolver.annotation.JavaInterop
33 import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION
34 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
35 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
36 import com.android.intentresolver.data.model.ChooserRequest
37 import com.android.intentresolver.platform.GlobalSettings
38 import com.android.intentresolver.ui.viewmodel.ChooserViewModel
39 import com.android.intentresolver.validation.Invalid
40 import com.android.intentresolver.validation.Valid
41 import com.android.intentresolver.validation.log
42 import dagger.hilt.android.scopes.ActivityScoped
43 import java.util.function.Consumer
44 import javax.inject.Inject
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.asStateFlow
47 import kotlinx.coroutines.flow.combine
48 import kotlinx.coroutines.flow.distinctUntilChanged
49 import kotlinx.coroutines.flow.filter
50 import kotlinx.coroutines.flow.filterNotNull
51 import kotlinx.coroutines.flow.first
52 import kotlinx.coroutines.flow.map
53 import kotlinx.coroutines.flow.onEach
54 import kotlinx.coroutines.flow.stateIn
55 import kotlinx.coroutines.launch
56 
57 private const val TAG: String = "ChooserHelper"
58 
59 /**
60  * __Purpose__
61  *
62  * Cleanup aid. Provides a pathway to cleaner code.
63  *
64  * __Incoming References__
65  *
66  * ChooserHelper must not expose any properties or functions directly back to ChooserActivity. If a
67  * value or operation is required by ChooserActivity, then it must be added to ChooserInitializer
68  * (or a new interface as appropriate) with ChooserActivity supplying a callback to receive it at
69  * the appropriate point. This enforces unidirectional control flow.
70  *
71  * __Outgoing References__
72  *
73  * _ChooserActivity_
74  *
75  * This class must only reference it's host as Activity/ComponentActivity; no down-cast to
76  * [ChooserActivity]. Other components should be created here or supplied via Injection, and not
77  * referenced directly within ChooserActivity. This prevents circular dependencies from forming. If
78  * necessary, during cleanup the dependency can be supplied back to ChooserActivity as described
79  * above in 'Incoming References', see [ChooserInitializer].
80  *
81  * _Elsewhere_
82  *
83  * Where possible, Singleton and ActivityScoped dependencies should be injected here instead of
84  * referenced from an existing location. If not available for injection, the value should be
85  * constructed here, then provided to where it is needed.
86  */
87 @ActivityScoped
88 @JavaInterop
89 class ChooserHelper
90 @Inject
91 constructor(
92     hostActivity: Activity,
93     private val activityResultRepo: ActivityResultRepository,
94     private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository,
95     private val globalSettings: GlobalSettings,
96 ) : DefaultLifecycleObserver {
97     // This is guaranteed by Hilt, since only a ComponentActivity is injectable.
98     private val activity: ComponentActivity = hostActivity as ComponentActivity
99     private val viewModel by activity.viewModels<ChooserViewModel>()
100 
101     // TODO: provide the following through an init object passed into [setInitialize]
102     private lateinit var activityInitializer: Runnable
103     /** Invoked when there are updates to ChooserRequest */
104     var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {}
105     /** Invoked when there are a new change to payload selection */
106     var onPendingSelection: Runnable = Runnable {}
107     var onHasSelections: Consumer<Boolean> = Consumer {}
108 
109     init {
110         activity.lifecycle.addObserver(this)
111     }
112 
113     /**
114      * Set the initialization hook for the host activity.
115      *
116      * This _must_ be called from [ChooserActivity.onCreate].
117      */
118     fun setInitializer(initializer: Runnable) {
119         check(activity.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
120             "setInitializer must be called before onCreate returns"
121         }
122         activityInitializer = initializer
123     }
124 
125     /** Invoked by Lifecycle, after [ChooserActivity.onCreate] _returns_. */
126     override fun onCreate(owner: LifecycleOwner) {
127         Log.i(TAG, "CREATE")
128         Log.i(TAG, "${viewModel.activityModel}")
129 
130         val callerUid: Int = viewModel.activityModel.launchedFromUid
131         if (callerUid < 0 || UserHandle.isIsolated(callerUid)) {
132             Log.e(TAG, "Can't start a chooser from uid $callerUid")
133             activity.finish()
134             return
135         }
136 
137         if (globalSettings.getBooleanOrNull(Settings.Global.SECURE_FRP_MODE) == true) {
138             Log.e(TAG, "Sharing disabled due to active FRP lock.")
139             activity.finish()
140             return
141         }
142 
143         when (val request = viewModel.initialRequest) {
144             is Valid -> initializeActivity(request)
145             is Invalid -> reportErrorsAndFinish(request)
146         }
147 
148         activity.lifecycleScope.launch {
149             activity.setResult(activityResultRepo.activityResult.filterNotNull().first())
150             activity.finish()
151         }
152 
153         activity.lifecycleScope.launch {
154             val hasPendingIntentFlow =
155                 pendingSelectionCallbackRepo.pendingTargetIntent
156                     .map { it != null }
157                     .distinctUntilChanged()
158                     .onEach { hasPendingIntent ->
159                         if (hasPendingIntent) {
160                             onPendingSelection.run()
161                         }
162                     }
163             activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
164                 val hasSelectionFlow =
165                     if (
166                         unselectFinalItem() &&
167                             viewModel.previewDataProvider.previewType ==
168                                 CONTENT_PREVIEW_PAYLOAD_SELECTION
169                     ) {
170                         viewModel.shareouselViewModel.hasSelectedItems.stateIn(scope = this).also {
171                             flow ->
172                             launch { flow.collect { onHasSelections.accept(it) } }
173                         }
174                     } else {
175                         MutableStateFlow(true).asStateFlow()
176                     }
177                 val requestControlFlow =
178                     hasSelectionFlow
179                         .combine(hasPendingIntentFlow) { hasSelections, hasPendingIntent ->
180                             hasSelections && !hasPendingIntent
181                         }
182                         .distinctUntilChanged()
183                 viewModel.request
184                     .combine(requestControlFlow) { request, isReady -> request to isReady }
185                     // only take ChooserRequest if there are no pending callbacks
186                     .filter { it.second }
187                     .map { it.first }
188                     .distinctUntilChanged(areEquivalent = { old, new -> old === new })
189                     .collect { onChooserRequestChanged.accept(it) }
190             }
191         }
192 
193         if (interactiveSession()) {
194             activity.lifecycleScope.launch {
195                 viewModel.interactiveSessionInteractor.isSessionActive
196                     .filter { !it }
197                     .collect { activity.finish() }
198             }
199         }
200     }
201 
202     override fun onStart(owner: LifecycleOwner) {
203         Log.i(TAG, "START")
204     }
205 
206     override fun onResume(owner: LifecycleOwner) {
207         Log.i(TAG, "RESUME")
208     }
209 
210     override fun onPause(owner: LifecycleOwner) {
211         Log.i(TAG, "PAUSE")
212     }
213 
214     override fun onStop(owner: LifecycleOwner) {
215         Log.i(TAG, "STOP")
216     }
217 
218     override fun onDestroy(owner: LifecycleOwner) {
219         Log.i(TAG, "DESTROY")
220     }
221 
222     private fun reportErrorsAndFinish(request: Invalid<ChooserRequest>) {
223         request.errors.forEach { it.log(TAG) }
224         activity.finish()
225     }
226 
227     private fun initializeActivity(request: Valid<ChooserRequest>) {
228         request.warnings.forEach { it.log(TAG) }
229         activityInitializer.run()
230     }
231 }
232