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