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