blob: 23f70b4b0eafdf5a7009b19ad34f89561a2e78ab [file] [log] [blame]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.intentresolver
import android.app.Activity
import android.os.UserHandle
import android.provider.Settings
import android.service.chooser.Flags.interactiveChooser
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.intentresolver.annotation.JavaInterop
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION
import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
import com.android.intentresolver.data.model.ChooserRequest
import com.android.intentresolver.platform.GlobalSettings
import com.android.intentresolver.ui.viewmodel.ChooserViewModel
import com.android.intentresolver.validation.Invalid
import com.android.intentresolver.validation.Valid
import com.android.intentresolver.validation.log
import dagger.hilt.android.scopes.ActivityScoped
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
private const val TAG: String = "ChooserHelper"
/**
* __Purpose__
*
* Cleanup aid. Provides a pathway to cleaner code.
*
* __Incoming References__
*
* ChooserHelper must not expose any properties or functions directly back to ChooserActivity. If a
* value or operation is required by ChooserActivity, then it must be added to ChooserInitializer
* (or a new interface as appropriate) with ChooserActivity supplying a callback to receive it at
* the appropriate point. This enforces unidirectional control flow.
*
* __Outgoing References__
*
* _ChooserActivity_
*
* This class must only reference it's host as Activity/ComponentActivity; no down-cast to
* [ChooserActivity]. Other components should be created here or supplied via Injection, and not
* referenced directly within ChooserActivity. This prevents circular dependencies from forming. If
* necessary, during cleanup the dependency can be supplied back to ChooserActivity as described
* above in 'Incoming References', see [ChooserInitializer].
*
* _Elsewhere_
*
* Where possible, Singleton and ActivityScoped dependencies should be injected here instead of
* referenced from an existing location. If not available for injection, the value should be
* constructed here, then provided to where it is needed.
*/
@ActivityScoped
@JavaInterop
class ChooserHelper
@Inject
constructor(
hostActivity: Activity,
private val activityResultRepo: ActivityResultRepository,
private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository,
private val globalSettings: GlobalSettings,
) : DefaultLifecycleObserver {
// This is guaranteed by Hilt, since only a ComponentActivity is injectable.
private val activity: ComponentActivity = hostActivity as ComponentActivity
private val viewModel by activity.viewModels<ChooserViewModel>()
// TODO: provide the following through an init object passed into [setInitialize]
private lateinit var activityInitializer: Runnable
/** Invoked when there are updates to ChooserRequest */
var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {}
/** Invoked when there are a new change to payload selection */
var onPendingSelection: Runnable = Runnable {}
var onTargetEnabled: Consumer<Boolean> = Consumer {}
var onMinimizeDrawerRequested: Consumer<Boolean> = Consumer {}
init {
activity.lifecycle.addObserver(this)
}
/**
* Set the initialization hook for the host activity.
*
* This _must_ be called from [ChooserActivity.onCreate].
*/
fun setInitializer(initializer: Runnable) {
check(activity.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
"setInitializer must be called before onCreate returns"
}
activityInitializer = initializer
}
/** Invoked by Lifecycle, after [ChooserActivity.onCreate] _returns_. */
override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "CREATE")
Log.i(TAG, "${viewModel.activityModel}")
val callerUid: Int = viewModel.activityModel.launchedFromUid
if (callerUid < 0 || UserHandle.isIsolated(callerUid)) {
Log.e(TAG, "Can't start a chooser from uid $callerUid")
activity.finish()
return
}
if (globalSettings.getBooleanOrNull(Settings.Global.SECURE_FRP_MODE) == true) {
Log.e(TAG, "Sharing disabled due to active FRP lock.")
activity.finish()
return
}
when (val request = viewModel.initialRequest) {
is Valid -> initializeActivity(request)
is Invalid -> reportErrorsAndFinish(request)
}
activity.lifecycleScope.launch {
activity.setResult(activityResultRepo.activityResult.filterNotNull().first())
activity.finish()
}
activity.lifecycleScope.launch {
val hasPendingIntentFlow =
pendingSelectionCallbackRepo.pendingTargetIntent
.map { it != null }
.distinctUntilChanged()
.onEach { hasPendingIntent ->
if (hasPendingIntent) {
onPendingSelection.run()
}
}
activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val hasSelectionFlow =
if (
viewModel.previewDataProvider.previewType ==
CONTENT_PREVIEW_PAYLOAD_SELECTION
) {
viewModel.shareouselViewModel.hasSelectedItems.stateIn(scope = this)
} else {
MutableStateFlow(true).asStateFlow()
}
launch {
if (interactiveChooser()) {
hasSelectionFlow
.combine(viewModel.interactiveSessionInteractor.isTargetEnabled) {
hasSelection,
isEnabled ->
hasSelection && isEnabled
}
.distinctUntilChanged()
} else {
hasSelectionFlow
}
.collect { onTargetEnabled.accept(it) }
}
val requestControlFlow =
hasSelectionFlow
.combine(hasPendingIntentFlow) { hasSelections, hasPendingIntent ->
hasSelections && !hasPendingIntent
}
.distinctUntilChanged()
viewModel.request
.combine(requestControlFlow) { request, isReady -> request to isReady }
// only take ChooserRequest if there are no pending callbacks
.filter { it.second }
.map { it.first }
.distinctUntilChanged(areEquivalent = { old, new -> old === new })
.collect { onChooserRequestChanged.accept(it) }
}
}
if (interactiveChooser()) {
activity.lifecycleScope.launch {
viewModel.interactiveSessionInteractor.isSessionActive
.filter { !it }
.collect { activity.finish() }
}
activity.lifecycleScope.launch {
for (isMinimized in viewModel.interactiveSessionInteractor.minimizeRequests) {
onMinimizeDrawerRequested.accept(isMinimized)
}
}
}
}
override fun onStart(owner: LifecycleOwner) {
Log.i(TAG, "START")
}
override fun onResume(owner: LifecycleOwner) {
Log.i(TAG, "RESUME")
}
override fun onPause(owner: LifecycleOwner) {
Log.i(TAG, "PAUSE")
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "STOP")
}
override fun onDestroy(owner: LifecycleOwner) {
Log.i(TAG, "DESTROY")
}
private fun reportErrorsAndFinish(request: Invalid<ChooserRequest>) {
request.errors.forEach { it.log(TAG) }
activity.finish()
}
private fun initializeActivity(request: Valid<ChooserRequest>) {
request.warnings.forEach { it.log(TAG) }
activityInitializer.run()
}
}