• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 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.sharetest
18 
19 import android.app.AlertDialog
20 import android.app.Dialog
21 import android.content.BroadcastReceiver
22 import android.content.ClipData
23 import android.content.Context
24 import android.content.Intent
25 import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
26 import android.content.IntentFilter
27 import android.content.res.Configuration
28 import android.os.Bundle
29 import android.provider.MediaStore
30 import android.service.chooser.ChooserSession
31 import android.service.chooser.ChooserSession.ChooserController
32 import android.util.Log
33 import androidx.activity.compose.setContent
34 import androidx.compose.foundation.clickable
35 import androidx.compose.foundation.layout.Arrangement
36 import androidx.compose.foundation.layout.Column
37 import androidx.compose.foundation.layout.Row
38 import androidx.compose.foundation.layout.Spacer
39 import androidx.compose.foundation.layout.fillMaxSize
40 import androidx.compose.foundation.layout.fillMaxWidth
41 import androidx.compose.foundation.layout.padding
42 import androidx.compose.foundation.layout.wrapContentHeight
43 import androidx.compose.material3.Button
44 import androidx.compose.material3.Checkbox
45 import androidx.compose.material3.Scaffold
46 import androidx.compose.material3.Text
47 import androidx.compose.material3.TextField
48 import androidx.compose.runtime.getValue
49 import androidx.compose.runtime.mutableFloatStateOf
50 import androidx.compose.runtime.mutableStateOf
51 import androidx.compose.runtime.remember
52 import androidx.compose.runtime.setValue
53 import androidx.compose.ui.Alignment
54 import androidx.compose.ui.Modifier
55 import androidx.compose.ui.draw.drawBehind
56 import androidx.compose.ui.geometry.Offset
57 import androidx.compose.ui.graphics.Color
58 import androidx.compose.ui.graphics.SolidColor
59 import androidx.compose.ui.layout.onGloballyPositioned
60 import androidx.compose.ui.unit.dp
61 import androidx.core.os.bundleOf
62 import androidx.fragment.app.DialogFragment
63 import androidx.fragment.app.FragmentActivity
64 import androidx.lifecycle.Lifecycle
65 import androidx.lifecycle.compose.collectAsStateWithLifecycle
66 import androidx.lifecycle.lifecycleScope
67 import androidx.lifecycle.repeatOnLifecycle
68 import com.android.sharetest.ui.theme.ActivityTheme
69 import dagger.hilt.android.AndroidEntryPoint
70 import kotlinx.coroutines.ExperimentalCoroutinesApi
71 import kotlinx.coroutines.flow.MutableStateFlow
72 import kotlinx.coroutines.flow.map
73 import kotlinx.coroutines.flow.scan
74 import kotlinx.coroutines.flow.update
75 import kotlinx.coroutines.launch
76 
77 private const val KEY_SESSION = "chooser-session"
78 private const val EXTRA_CHOOSER_INTERACTIVE_CALLBACK =
79     "com.android.extra.EXTRA_CHOOSER_INTERACTIVE_CALLBACK"
80 
81 @AndroidEntryPoint(value = FragmentActivity::class)
82 class InteractiveShareTestActivity : Hilt_InteractiveShareTestActivity() {
83     private val TAG = "ShareTest/$hashId"
84     private var chooserWindowTopOffset = MutableStateFlow(-1)
85     private val isInMultiWindowMode = MutableStateFlow<Boolean>(false)
86     private val chooserSession = MutableStateFlow<ChooserSession?>(null)
87     private val useRefinementFlow = MutableStateFlow<Boolean>(false)
88     private val refinementReceiver =
89         object : BroadcastReceiver() {
90             override fun onReceive(context: Context?, intent: Intent) {
91                 // Need to show refinement in another activity because this one is beneath the
92                 // sharesheet.
93                 val activityIntent =
94                     Intent(this@InteractiveShareTestActivity, RefinementActivity::class.java)
95                 activityIntent.putExtras(intent)
96                 startActivity(activityIntent)
97             }
98         }
99 
100     private val sessionStateListener =
101         object : ChooserSession.ChooserSessionUpdateListener {
102             override fun onChooserConnected(
103                 session: ChooserSession?,
104                 chooserController: ChooserController?,
105             ) {
106                 Log.d(TAG, "onChooserConnected")
107             }
108 
109             override fun onChooserDisconnected(session: ChooserSession?) {
110                 Log.d(TAG, "onChooserDisconnected")
111             }
112 
113             override fun onSessionClosed(session: ChooserSession?) {
114                 Log.d(TAG, "onSessionClosed")
115                 chooserSession.update { oldValue -> if (oldValue === session) null else oldValue }
116             }
117 
118             override fun onDrawerVerticalOffsetChanged(session: ChooserSession, offset: Int) {
119                 chooserWindowTopOffset.value = offset
120             }
121         }
122 
123     @OptIn(ExperimentalCoroutinesApi::class)
124     override fun onCreate(savedInstanceState: Bundle?) {
125         super.onCreate(savedInstanceState)
126 
127         isInMultiWindowMode.value = isInMultiWindowMode()
128         chooserSession.value =
129             savedInstanceState?.getParcelable(KEY_SESSION, ChooserSession::class.java)?.apply {
130                 setChooserStateListener(sessionStateListener)
131             }
132 
133         lifecycleScope.launch {
134             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
135                 chooserSession
136                     .scan<ChooserSession?, ChooserSession?>(null) { prevSession, newSession ->
137                         prevSession?.setChooserStateListener(null)
138                         prevSession?.cancel()
139                         newSession?.setChooserStateListener(sessionStateListener)
140                         newSession
141                     }
142                     .collect {}
143             }
144         }
145 
146         val previews = buildList {
147             for (i in 0..2) {
148                 val uri = ImageContentProvider.makeItemUri(i, "image/jpg", true)
149                 add(Preview(uri, uri, isImage = true))
150             }
151         }
152 
153         setContent {
154             var sharedText by remember { mutableStateOf("A text to share") }
155             val previewWindowBottom by chooserWindowTopOffset.collectAsStateWithLifecycle(-1)
156             val showLaunchInSplitScreen by
157                 isInMultiWindowMode.map { !it }.collectAsStateWithLifecycle(true)
158             val spacing = 5.dp
159             val brush = SolidColor(Color.Red)
160             // val isChooserRunning by chooserSessionManager.activeSession.map { it != null }
161             //     .collectAsStateWithLifecycle(false)
162             val isChooserRunning by
163                 chooserSession.map { it?.isActive == true }.collectAsStateWithLifecycle(false)
164             val userRefinement by useRefinementFlow.collectAsStateWithLifecycle(false)
165             ActivityTheme {
166                 Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
167                     Column(
168                         modifier = Modifier.padding(innerPadding),
169                         verticalArrangement = Arrangement.spacedBy(spacing),
170                     ) {
171                         Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
172                             Button(onClick = { startCameraApp() }) { Text("Pick Camera App") }
173                             Button(onClick = { launchActivity() }) { Text("Launch Activity") }
174                         }
175                         Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
176                             if (showLaunchInSplitScreen) {
177                                 Button(onClick = { launchSelfInSplitScreen() }) {
178                                     Text("Launch Self in Split-Screen")
179                                 }
180                             }
181                             Button(onClick = { launchDialog() }) { Text("Launch Dialog") }
182                         }
183                         Row(
184                             modifier = Modifier.fillMaxWidth().wrapContentHeight(),
185                             horizontalArrangement = Arrangement.spacedBy(spacing),
186                         ) {
187                             TextField(
188                                 value = sharedText,
189                                 modifier = Modifier.weight(1f),
190                                 onValueChange = { sharedText = it },
191                             )
192                             Button(onClick = { shareText(sharedText) }) { Text("Share Text") }
193                         }
194                         Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
195                             if (previews.isNotEmpty()) {
196                                 Button(onClick = { shareImages(previews, 1) }) {
197                                     Text("Share One Image")
198                                 }
199                                 if (previews.size > 1) {
200                                     Button(onClick = { shareImages(previews, 2) }) {
201                                         Text("Share Two Images")
202                                     }
203                                 }
204                             }
205                         }
206                         Row(
207                             horizontalArrangement = Arrangement.spacedBy(spacing),
208                             modifier = Modifier.clickable { updateRefinement() },
209                         ) {
210                             Checkbox(
211                                 checked = userRefinement,
212                                 onCheckedChange = {},
213                                 modifier = Modifier.align(Alignment.CenterVertically),
214                             )
215                             Text(
216                                 "Use Refinement",
217                                 modifier = Modifier.align(Alignment.CenterVertically),
218                             )
219                         }
220                         if (isChooserRunning) {
221                             Button(onClick = { closeChooser() }) { Text("Close Chooser") }
222                         }
223                     }
224 
225                     var windowTop by remember { mutableFloatStateOf(0f) }
226                     Spacer(
227                         modifier =
228                             Modifier.fillMaxSize()
229                                 .onGloballyPositioned { coords ->
230                                     windowTop = coords.localToWindow(Offset.Zero).y
231                                 }
232                                 .drawBehind {
233                                     if (previewWindowBottom >= 0 && isChooserRunning) {
234                                         val top = previewWindowBottom.toFloat() - windowTop
235                                         drawLine(
236                                             brush = brush,
237                                             start = Offset(0f, top),
238                                             end = Offset(size.width, top),
239                                             strokeWidth = 2.dp.toPx(),
240                                         )
241                                     }
242                                 }
243                     )
244                 }
245             }
246         }
247     }
248 
249     override fun onStart() {
250         Log.d(TAG, "onStart")
251         super.onStart()
252     }
253 
254     override fun onResume() {
255         Log.d(TAG, "onResume")
256         super.onResume()
257     }
258 
259     override fun onPause() {
260         Log.d(TAG, "onPause")
261         super.onPause()
262     }
263 
264     override fun onStop() {
265         Log.d(TAG, "onStop")
266         super.onStop()
267     }
268 
269     override fun onDestroy() {
270         Log.d(TAG, "onDestroy")
271         if (useRefinementFlow.value) {
272             unregisterReceiver(refinementReceiver)
273         }
274         super.onDestroy()
275     }
276 
277     override fun onSaveInstanceState(outState: Bundle) {
278         Log.d(TAG, "onSaveInstanceState")
279         super.onSaveInstanceState(outState)
280         chooserSession.value?.let { outState.putParcelable(KEY_SESSION, it) }
281     }
282 
283     override fun onConfigurationChanged(newConfig: Configuration) {
284         Log.d(TAG, "onConfigurationChanged")
285         super.onConfigurationChanged(newConfig)
286     }
287 
288     private fun updateRefinement() {
289         useRefinementFlow.update {
290             if (it) {
291                 unregisterReceiver(refinementReceiver)
292             } else {
293                 registerReceiver(
294                     refinementReceiver,
295                     IntentFilter(REFINEMENT_ACTION),
296                     RECEIVER_EXPORTED,
297                 )
298             }
299             !it
300         }
301     }
302 
303     private fun startCameraApp() {
304         val targetIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
305         startOrUpdate(Intent.createChooser(targetIntent, null))
306     }
307 
308     private fun launchActivity() {
309         startActivity(Intent(this, SendTextActivity::class.java))
310     }
311 
312     private fun launchDialog() {
313         val dialog = TestDialog()
314         dialog.show(supportFragmentManager, "dialog")
315     }
316 
317     private fun launchSelfInSplitScreen() {
318         startActivity(
319             Intent(this, javaClass).apply {
320                 setFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK)
321             }
322         )
323     }
324 
325     private fun shareText(text: String) {
326         val targetIntent =
327             Intent(Intent.ACTION_SEND).apply {
328                 putExtra(Intent.EXTRA_TEXT, text)
329                 setType("text/plain")
330             }
331         val chooserIntent = Intent.createChooser(targetIntent, null)
332         startOrUpdate(chooserIntent)
333     }
334 
335     private fun shareImages(previews: List<Preview>, count: Int) {
336         require(count > 0) { "Unexpected count argument value: $count" }
337         val targetIntent =
338             Intent(if (count == 1) Intent.ACTION_SEND else Intent.ACTION_SEND_MULTIPLE).apply {
339                 if (count == 1) {
340                     putExtra(Intent.EXTRA_STREAM, previews[0].uri)
341                 } else {
342                     putExtra(
343                         Intent.EXTRA_STREAM,
344                         ArrayList(previews.take(count).map { it.uri }.toList()),
345                     )
346                 }
347                 clipData =
348                     ClipData("image", arrayOf("image/*"), ClipData.Item(previews[0].uri)).apply {
349                         previews.take(count).forEachIndexed { idx, item ->
350                             if (idx != 0) {
351                                 addItem(ClipData.Item(item.uri))
352                             }
353                         }
354                     }
355                 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
356                 setType("image/*")
357             }
358         val chooserIntent = Intent.createChooser(targetIntent, null)
359         startOrUpdate(chooserIntent)
360     }
361 
362     private fun closeChooser() {
363         chooserSession.value?.cancel()
364         chooserSession.value = null
365         chooserWindowTopOffset.value = -1
366     }
367 
368     private fun startOrUpdate(chooserIntent: Intent) {
369         val chooserController = chooserSession.value?.takeIf { it.isActive }?.chooserController
370         if (useRefinementFlow.value) {
371             chooserIntent.putExtra(
372                 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER,
373                 createRefinementIntentSender(this@InteractiveShareTestActivity, true),
374             )
375         }
376         chooserIntent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, createResultIntentSender(this))
377         if (chooserController == null) {
378             val session = ChooserSession()
379             chooserSession.value = session
380             startActivity(
381                 Intent(chooserIntent).apply {
382                     putExtras(bundleOf(EXTRA_CHOOSER_INTERACTIVE_CALLBACK to session))
383                 }
384             )
385         } else {
386             chooserController.updateIntent(chooserIntent)
387         }
388     }
389 }
390 
391 class TestDialog : DialogFragment() {
onCreateDialognull392     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
393         return AlertDialog.Builder(requireContext())
394             .setMessage("Just a test dialog")
395             .setPositiveButton("Close") { _, _ -> dismiss() }
396             .create()
397     }
398 }
399