Introduce payloadtoggle data layer
Bug: 302691505
Flag: ACONFIG android.service.chooser.chooser_payload_toggling DEVELOPMENT
Test: N/A - code isn't live
Change-Id: I5f96b97919db21f3705add6d67fd97ae267c09e2
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt
new file mode 100644
index 0000000..b794500
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.contentpreview.payloadtoggle.data.model
+
+import android.graphics.drawable.Icon
+
+/** Data model for a custom action the user can take. */
+data class CustomActionModel(
+ /** Label presented to the user identifying this action. */
+ val label: CharSequence,
+ /** Icon presented to the user for this action. */
+ val icon: Icon,
+ /** When invoked, performs this action. */
+ val performAction: () -> Unit,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt
new file mode 100644
index 0000000..c3bb88c
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.contentpreview.payloadtoggle.data.repository
+
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Tracks the result of the current activity. */
+@ActivityRetainedScoped
+class ActivityResultRepository @Inject constructor() {
+ /** The result of the current activity, or `null` if the activity is still active. */
+ val activityResult = MutableStateFlow<Int?>(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt
new file mode 100644
index 0000000..b104d4b
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.contentpreview.payloadtoggle.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Stores previews for Shareousel UI that have been cached locally from a remote
+ * [android.database.Cursor].
+ */
+@ActivityRetainedScoped
+class CursorPreviewsRepository @Inject constructor() {
+ /** Previews available for display within Shareousel. */
+ val previewsModel = MutableStateFlow<PreviewsModel?>(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
new file mode 100644
index 0000000..8035580
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.contentpreview.payloadtoggle.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Stores set of selected previews. */
+@ViewModelScoped
+class PreviewSelectionsRepository @Inject constructor() {
+ /** Set of selected previews. */
+ val selections = MutableStateFlow<Set<PreviewModel>>(emptySet())
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt
new file mode 100644
index 0000000..c843684
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/TargetIntentRepository.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.contentpreview.payloadtoggle.data.repository
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.inject.TargetIntent
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Stores the target intent of the share sheet, and custom actions derived from the intent. */
+@ViewModelScoped
+class TargetIntentRepository
+@Inject
+constructor(
+ @TargetIntent initialIntent: Intent,
+ initialActions: List<CustomActionModel>,
+) {
+ val targetIntent = MutableStateFlow(initialIntent)
+
+ // TODO: this can probably be derived from [targetIntent]; right now, the [initialActions] are
+ // coming from a different place (ChooserRequest) than later ones (SelectionChangeCallback)
+ // and so this serves as the source of truth between the two.
+ val customActions = MutableStateFlow(initialActions)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt
new file mode 100644
index 0000000..faad5bb
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.contentpreview.payloadtoggle.domain.intent
+
+import android.app.ActivityOptions
+import android.app.PendingIntent
+import android.content.Context
+import com.android.intentresolver.R
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import javax.inject.Qualifier
+
+/** [PendingIntentSender] for Shareousel custom actions. */
+class CustomActionPendingIntentSender
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+) : PendingIntentSender {
+ override fun send(pendingIntent: PendingIntent) {
+ pendingIntent.send(
+ /* context = */ null,
+ /* code = */ 0,
+ /* intent = */ null,
+ /* onFinished = */ null,
+ /* handler = */ null,
+ /* requiredPermission = */ null,
+ /* options = */ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left,
+ )
+ .toBundle()
+ )
+ }
+
+ @Module
+ @InstallIn(SingletonComponent::class)
+ interface Binding {
+ @Binds
+ @CustomAction
+ fun bindSender(sender: CustomActionPendingIntentSender): PendingIntentSender
+ }
+}
+
+/** [PendingIntentSender] for Shareousel custom actions. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class CustomAction
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt
new file mode 100644
index 0000000..d75884d
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.contentpreview.payloadtoggle.domain.intent
+
+import android.app.PendingIntent
+import android.service.chooser.ChooserAction
+import android.util.Log
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object InitialCustomActionsModule {
+ @Provides
+ fun initialCustomActionModels(
+ chooserActions: List<ChooserAction>,
+ @CustomAction pendingIntentSender: PendingIntentSender,
+ ): List<CustomActionModel> = chooserActions.map { it.toCustomActionModel(pendingIntentSender) }
+}
+
+/**
+ * Returns a [CustomActionModel] that sends this [ChooserAction]'s
+ * [PendingIntent][ChooserAction.getAction].
+ */
+fun ChooserAction.toCustomActionModel(pendingIntentSender: PendingIntentSender) =
+ CustomActionModel(
+ label = label,
+ icon = icon,
+ performAction = {
+ try {
+ pendingIntentSender.send(action)
+ } catch (_: PendingIntent.CanceledException) {
+ Log.d(TAG, "Custom action, $label, has been cancelled")
+ }
+ }
+ )
+
+private const val TAG = "CustomShareActions"
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt
new file mode 100644
index 0000000..23ba31b
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.contentpreview.payloadtoggle.domain.intent
+
+import android.app.PendingIntent
+
+/** Sends [PendingIntent]s. */
+fun interface PendingIntentSender {
+ fun send(pendingIntent: PendingIntent)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt
new file mode 100644
index 0000000..ff96a9f
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.contentpreview.payloadtoggle.shared.model
+
+import android.net.Uri
+
+/** An individual preview presented in Shareousel. */
+data class PreviewModel(
+ /**
+ * Uri for this preview; if this preview is selected, this will be shared with the target app.
+ */
+ val uri: Uri,
+ /** Mimetype for the data [uri] points to. */
+ val mimeType: String?,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt
new file mode 100644
index 0000000..0ac99bd
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.contentpreview.payloadtoggle.shared.model
+
+/** A dataset of previews for Shareousel. */
+data class PreviewsModel(
+ /** All available [PreviewModel]s. */
+ val previewModels: Set<PreviewModel>,
+ /** Index into [previewModels] that should be initially displayed to the user. */
+ val startIdx: Int,
+ /**
+ * Signals that more data should be loaded to the left of this dataset. A `null` value indicates
+ * that there is no more data to load in that direction.
+ */
+ val loadMoreLeft: (() -> Unit)?,
+ /**
+ * Signals that more data should be loaded to the right of this dataset. A `null` value
+ * indicates that there is no more data to load in that direction.
+ */
+ val loadMoreRight: (() -> Unit)?,
+)
diff --git a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
new file mode 100644
index 0000000..9a8b876
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.inject
+
+import android.content.Intent
+import android.service.chooser.ChooserAction
+import androidx.lifecycle.SavedStateHandle
+import com.android.intentresolver.v2.ui.model.ActivityModel
+import com.android.intentresolver.v2.ui.model.ChooserRequest
+import com.android.intentresolver.v2.ui.viewmodel.readChooserRequest
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Qualifier
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ActivityModelModule {
+ @Provides
+ fun provideActivityModel(savedStateHandle: SavedStateHandle): ActivityModel =
+ requireNotNull(savedStateHandle[ActivityModel.ACTIVITY_MODEL_KEY]) {
+ "ActivityModel missing in SavedStateHandle! (${ActivityModel.ACTIVITY_MODEL_KEY})"
+ }
+
+ @Provides
+ @ViewModelScoped
+ fun provideChooserRequest(
+ activityModel: ActivityModel,
+ flags: ChooserServiceFlags,
+ ): ValidationResult<ChooserRequest> = readChooserRequest(activityModel, flags)
+
+ @Provides
+ @TargetIntent
+ fun targetIntent(chooserReq: ValidationResult<ChooserRequest>): Intent =
+ requireNotNull((chooserReq as? Valid)?.value?.targetIntent) { "no target intent available" }
+
+ @Provides
+ fun customActions(chooserReq: ValidationResult<ChooserRequest>): List<ChooserAction> =
+ requireNotNull((chooserReq as? Valid)?.value?.chooserActions) {
+ "no chooser actions available"
+ }
+}
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class TargetIntent