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