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