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