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.Activity 20 import android.app.PendingIntent 21 import android.content.BroadcastReceiver 22 import android.content.ClipData 23 import android.content.Context 24 import android.content.Intent 25 import android.content.IntentFilter 26 import android.graphics.Color 27 import android.graphics.Typeface 28 import android.os.Bundle 29 import android.text.Spannable 30 import android.text.SpannableStringBuilder 31 import android.text.style.BackgroundColorSpan 32 import android.text.style.BulletSpan 33 import android.text.style.ForegroundColorSpan 34 import android.text.style.StyleSpan 35 import android.text.style.UnderlineSpan 36 import android.view.View 37 import android.view.ViewGroup.MarginLayoutParams 38 import android.widget.ArrayAdapter 39 import android.widget.Button 40 import android.widget.CheckBox 41 import android.widget.EditText 42 import android.widget.RadioButton 43 import android.widget.RadioGroup 44 import android.widget.Spinner 45 import android.widget.Toast 46 import androidx.annotation.RequiresApi 47 import androidx.core.view.ViewCompat 48 import androidx.core.view.WindowInsetsCompat 49 import androidx.core.view.updateLayoutParams 50 import com.android.sharetest.ImageContentProvider.Companion.IMAGE_COUNT 51 import com.android.sharetest.ImageContentProvider.Companion.makeItemUri 52 import kotlin.random.Random 53 54 private const val TYPE_IMAGE = "Image" 55 private const val TYPE_VIDEO = "Video" 56 private const val TYPE_PDF = "PDF Doc" 57 private const val TYPE_IMG_VIDEO = "Image / Video Mix" 58 private const val TYPE_IMG_PDF = "Image / PDF Mix" 59 private const val TYPE_VIDEO_PDF = "Video / PDF Mix" 60 private const val TYPE_ALL = "All Type Mix" 61 private const val ADDITIONAL_ITEM_COUNT = 1_000 62 63 @RequiresApi(34) 64 class ShareTestActivity : Activity() { 65 private lateinit var customActionReceiver: BroadcastReceiver 66 private lateinit var refinementReceiver: BroadcastReceiver 67 private lateinit var mediaSelection: RadioGroup 68 private lateinit var textSelection: RadioGroup 69 private lateinit var mediaTypeSelection: Spinner 70 private lateinit var mediaTypeHeader: View 71 private lateinit var richText: CheckBox 72 private lateinit var albumCheck: CheckBox 73 private lateinit var metadata: EditText 74 private lateinit var shareouselCheck: CheckBox 75 private lateinit var altIntentCheck: CheckBox 76 private lateinit var callerTargetCheck: CheckBox 77 private lateinit var selectionLatencyGroup: RadioGroup 78 private lateinit var imageSizeMetadataCheck: CheckBox 79 private val customActionFactory = CustomActionFactory(this) 80 81 override fun onCreate(savedInstanceState: Bundle?) { 82 super.onCreate(savedInstanceState) 83 setContentView(R.layout.activity_main) 84 85 val container = requireViewById<View>(R.id.container) 86 ViewCompat.setOnApplyWindowInsetsListener(container) { v, windowInsets -> 87 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 88 v.updateLayoutParams<MarginLayoutParams> { 89 leftMargin = insets.left 90 topMargin = insets.top 91 rightMargin = insets.right 92 bottomMargin = insets.bottom 93 } 94 95 WindowInsetsCompat.CONSUMED 96 } 97 98 customActionReceiver = object : BroadcastReceiver() { 99 override fun onReceive(context: Context?, intent: Intent) { 100 Toast.makeText( 101 this@ShareTestActivity, 102 "Custom action invoked, isModified: ${!intent.isInitial}", 103 Toast.LENGTH_LONG 104 ) 105 .show() 106 } 107 } 108 109 refinementReceiver = object : BroadcastReceiver() { 110 override fun onReceive(context: Context?, intent: Intent) { 111 // Need to show refinement in another activity because this one is beneath the 112 // sharesheet. 113 val activityIntent = Intent(this@ShareTestActivity, RefinementActivity::class.java) 114 activityIntent.putExtras(intent) 115 startActivity(activityIntent) 116 } 117 } 118 119 registerReceiver( 120 customActionReceiver, 121 IntentFilter(CustomActionFactory.BROADCAST_ACTION), 122 Context.RECEIVER_EXPORTED 123 ) 124 125 registerReceiver( 126 refinementReceiver, 127 IntentFilter(REFINEMENT_ACTION), 128 Context.RECEIVER_EXPORTED 129 ) 130 131 richText = requireViewById(R.id.use_rich_text) 132 albumCheck = requireViewById(R.id.album_text) 133 shareouselCheck = requireViewById(R.id.shareousel) 134 altIntentCheck = requireViewById(R.id.alt_intent) 135 callerTargetCheck = requireViewById(R.id.caller_direct_target) 136 mediaTypeSelection = requireViewById(R.id.media_type_selection) 137 mediaTypeHeader = requireViewById(R.id.media_type_header) 138 selectionLatencyGroup = requireViewById(R.id.selection_latency) 139 imageSizeMetadataCheck = requireViewById(R.id.image_size_metadata) 140 mediaSelection = requireViewById<RadioGroup>(R.id.media_selection).apply { 141 setOnCheckedChangeListener { _, id -> updateMediaTypesList(id) } 142 check(R.id.no_media) 143 } 144 metadata = requireViewById(R.id.metadata) 145 146 textSelection = requireViewById<RadioGroup>(R.id.text_selection).apply { 147 check(R.id.short_text) 148 } 149 requireViewById<RadioGroup>(R.id.action_selection).check(R.id.no_actions) 150 151 requireViewById<Button>(R.id.share).setOnClickListener(this::share) 152 153 requireViewById<RadioButton>(R.id.no_media).setOnClickListener { 154 if (textSelection.checkedRadioButtonId == R.id.no_text) { 155 textSelection.check(R.id.short_text) 156 } 157 } 158 159 requireViewById<RadioGroup>(R.id.image_latency).setOnCheckedChangeListener { _, checkedId -> 160 ImageContentProvider.openLatency = when (checkedId) { 161 R.id.image_latency_50 -> 50 162 R.id.image_latency_200 -> 200 163 R.id.image_latency_800 -> 800 164 else -> 0 165 } 166 } 167 requireViewById<RadioGroup>(R.id.image_latency).check(R.id.image_latency_none) 168 169 requireViewById<RadioGroup>(R.id.image_get_type_latency).setOnCheckedChangeListener { 170 _, 171 checkedId, 172 -> 173 ImageContentProvider.getTypeLatency = when (checkedId) { 174 R.id.image_get_type_latency_50 -> 50 175 R.id.image_get_type_latency_200 -> 200 176 R.id.image_get_type_latency_800 -> 800 177 else -> 0 178 } 179 } 180 requireViewById<RadioGroup>(R.id.image_get_type_latency).check( 181 R.id.image_get_type_latency_none 182 ) 183 184 requireViewById<RadioGroup>(R.id.image_query_latency).let { radioGroup -> 185 radioGroup.setOnCheckedChangeListener { 186 _, 187 checkedId, 188 -> 189 ImageContentProvider.queryLatency = when (checkedId) { 190 R.id.image_query_latency_50 -> 50 191 R.id.image_query_latency_200 -> 200 192 R.id.image_query_latency_800 -> 800 193 else -> 0 194 } 195 } 196 radioGroup.check(R.id.image_query_latency_none) 197 } 198 199 requireViewById<RadioGroup>(R.id.image_load_failure_rate).setOnCheckedChangeListener { 200 _, 201 checkedId, 202 -> 203 ImageContentProvider.openFailureRate = when (checkedId) { 204 R.id.image_load_failure_rate_50 -> .5f 205 R.id.image_load_failure_rate_100 -> 1f 206 else -> 0f 207 } 208 } 209 requireViewById<RadioGroup>(R.id.image_load_failure_rate).check( 210 R.id.image_load_failure_rate_none 211 ) 212 } 213 214 private fun updateMediaTypesList(id: Int) { 215 when (id) { 216 R.id.no_media -> removeMediaTypeOptions() 217 R.id.one_image -> setSingleMediaTypeOptions() 218 R.id.many_images -> setAllMediaTypeOptions() 219 } 220 } 221 222 private fun removeMediaTypeOptions() { 223 mediaTypeSelection.adapter = ArrayAdapter( 224 this, android.R.layout.simple_spinner_item, emptyArray<String>() 225 ).apply { 226 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 227 } 228 setMediaTypeVisibility(false) 229 } 230 231 private fun setSingleMediaTypeOptions() { 232 mediaTypeSelection.adapter = ArrayAdapter( 233 this, 234 android.R.layout.simple_spinner_item, 235 arrayOf(TYPE_IMAGE, TYPE_VIDEO, TYPE_PDF) 236 ).apply { 237 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 238 } 239 setMediaTypeVisibility(true) 240 } 241 242 private fun setAllMediaTypeOptions() { 243 mediaTypeSelection.adapter = ArrayAdapter( 244 this, 245 android.R.layout.simple_spinner_item, 246 arrayOf( 247 TYPE_IMAGE, 248 TYPE_VIDEO, 249 TYPE_PDF, 250 TYPE_IMG_VIDEO, 251 TYPE_IMG_PDF, 252 TYPE_VIDEO_PDF, 253 TYPE_ALL 254 ) 255 ).apply { 256 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 257 } 258 setMediaTypeVisibility(true) 259 } 260 261 private fun setMediaTypeVisibility(visible: Boolean) { 262 val visibility = if (visible) View.VISIBLE else View.GONE 263 mediaTypeHeader.visibility = visibility 264 mediaTypeSelection.visibility = visibility 265 shareouselCheck.visibility = visibility 266 altIntentCheck.visibility = visibility 267 } 268 269 private fun share(view: View) { 270 val share = Intent(Intent.ACTION_SEND) 271 share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 272 273 val mimeTypes = getSelectedContentTypes() 274 275 val imageIndex = Random.nextInt(ADDITIONAL_ITEM_COUNT) 276 277 when (mediaSelection.checkedRadioButtonId) { 278 R.id.one_image -> share.apply { 279 val sharedUri = makeItemUri( 280 imageIndex, 281 mimeTypes[imageIndex % mimeTypes.size], 282 imageSizeMetadataCheck.isChecked 283 ) 284 putExtra(Intent.EXTRA_STREAM, sharedUri) 285 clipData = ClipData("", arrayOf("image/jpg"), ClipData.Item(sharedUri)) 286 type = if (mimeTypes.size == 1) mimeTypes[0] else "*/*" 287 } 288 289 R.id.many_images -> share.apply { 290 val imageUris = ArrayList( 291 (0 until IMAGE_COUNT).map { idx -> 292 makeItemUri( 293 idx, 294 mimeTypes[idx % mimeTypes.size], 295 imageSizeMetadataCheck.isChecked 296 ) 297 }) 298 action = Intent.ACTION_SEND_MULTIPLE 299 clipData = ClipData("", arrayOf("image/jpg"), ClipData.Item(imageUris[0])).apply { 300 for (i in 1 until IMAGE_COUNT) { 301 addItem(ClipData.Item(imageUris[i])) 302 } 303 } 304 type = if (mimeTypes.size == 1) mimeTypes[0] else "*/*" 305 putParcelableArrayListExtra( 306 Intent.EXTRA_STREAM, 307 imageUris 308 ) 309 } 310 } 311 312 val url = "https://developer.android.com/training/sharing/send#adding-rich-content-previews" 313 314 when (textSelection.checkedRadioButtonId) { 315 R.id.short_text -> share.setText(createShortText()) 316 R.id.long_text -> share.setText(createLongText()) 317 R.id.url_text -> share.setText(url) 318 } 319 320 if (requireViewById<CheckBox>(R.id.include_title).isChecked) { 321 share.putExtra(Intent.EXTRA_TITLE, createTextTitle()) 322 } 323 324 if (requireViewById<CheckBox>(R.id.include_icon).isChecked) { 325 share.clipData = ClipData( 326 "", arrayOf("image/png"), ClipData.Item(ImageContentProvider.ICON_URI) 327 ) 328 share.data = ImageContentProvider.ICON_URI 329 } 330 331 val chosenComponentPendingIntent = PendingIntent.getBroadcast( 332 this, 0, 333 Intent(this, ChosenComponentBroadcastReceiver::class.java), 334 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 335 ) 336 337 val chooserIntent = 338 Intent.createChooser(share, null, chosenComponentPendingIntent.intentSender) 339 340 val sendingImage = mediaSelection.checkedRadioButtonId.let { 341 it == R.id.one_image || it == R.id.many_images 342 } 343 if (sendingImage && altIntentCheck.isChecked) { 344 chooserIntent.putExtra( 345 Intent.EXTRA_ALTERNATE_INTENTS, 346 arrayOf(createAlternateIntent(share)) 347 ) 348 } 349 if (callerTargetCheck.isChecked) { 350 chooserIntent.putExtra( 351 Intent.EXTRA_CHOOSER_TARGETS, 352 arrayOf(createCallerTarget(this, "Initial Direct Target")) 353 ) 354 } 355 356 if (albumCheck.isChecked) { 357 chooserIntent.putExtra( 358 Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT, 359 Intent.CHOOSER_CONTENT_TYPE_ALBUM 360 ) 361 } 362 363 if (requireViewById<CheckBox>(R.id.include_modify_share).isChecked) { 364 chooserIntent.setModifyShareAction(this) 365 } 366 367 if (requireViewById<CheckBox>(R.id.use_refinement).isChecked) { 368 chooserIntent.putExtra( 369 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, 370 createRefinementIntentSender(this, true) 371 ) 372 } 373 374 when (requireViewById<RadioGroup>(R.id.action_selection).checkedRadioButtonId) { 375 R.id.one_action -> chooserIntent.putExtra( 376 Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, customActionFactory.getCustomActions(1) 377 ) 378 379 R.id.five_actions -> chooserIntent.putExtra( 380 Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, customActionFactory.getCustomActions(5) 381 ) 382 } 383 384 if (metadata.text.isNotEmpty()) { 385 chooserIntent.putExtra(Intent.EXTRA_METADATA_TEXT, metadata.text) 386 } 387 if (shareouselCheck.isChecked) { 388 val additionalContentUri = 389 AdditionalContentProvider.ADDITIONAL_CONTENT_URI.buildUpon() 390 .appendQueryParameter( 391 AdditionalContentProvider.PARAM_COUNT, 392 ADDITIONAL_ITEM_COUNT.toString(), 393 ) 394 .appendQueryParameter( 395 AdditionalContentProvider.PARAM_SIZE_META, 396 imageSizeMetadataCheck.isChecked.toString(), 397 ) 398 .also { builder -> 399 mimeTypes.forEach { 400 builder.appendQueryParameter( 401 AdditionalContentProvider.PARAM_MIME_TYPE, it) 402 } 403 } 404 .build() 405 chooserIntent.putExtra( 406 Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, 407 additionalContentUri, 408 ) 409 chooserIntent.putExtra(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, 0) 410 chooserIntent.clipData?.addItem(ClipData.Item(additionalContentUri)) 411 if (mediaSelection.checkedRadioButtonId == R.id.one_image) { 412 chooserIntent.putExtra( 413 AdditionalContentProvider.CURSOR_START_POSITION, 414 imageIndex, 415 ) 416 } 417 val latency = when (selectionLatencyGroup.checkedRadioButtonId) { 418 R.id.selection_latency_50 -> 50 419 R.id.selection_latency_200 -> 200 420 R.id.selection_latency_800 -> 800 421 else -> 0 422 } 423 if (latency > 0) { 424 chooserIntent.putExtra(AdditionalContentProvider.EXTRA_SELECTION_LATENCY, latency) 425 } 426 } 427 428 startActivity(chooserIntent) 429 } 430 431 private fun getSelectedContentTypes(): Array<String> = 432 mediaTypeSelection.selectedItem?.let { types -> 433 when (types) { 434 TYPE_VIDEO -> arrayOf("video/mp4") 435 TYPE_PDF -> arrayOf("application/pdf") 436 TYPE_IMG_VIDEO -> arrayOf("image/jpeg", "video/mp4") 437 TYPE_IMG_PDF -> arrayOf("image/jpeg", "application/pdf") 438 TYPE_VIDEO_PDF -> arrayOf("video/mp4", "application/pdf") 439 TYPE_ALL -> arrayOf("image/jpeg", "video/mp4", "application/pdf") 440 else -> null 441 } 442 } ?: arrayOf("image/jpeg") 443 444 private fun createShortText(): CharSequence = 445 SpannableStringBuilder() 446 .append("This", StyleSpan(Typeface.BOLD), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 447 .append(" is ", StyleSpan(Typeface.ITALIC), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 448 .append("a bit of ") 449 .append("text", BackgroundColorSpan(Color.YELLOW), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 450 .append(" to ") 451 .append("share", ForegroundColorSpan(Color.GREEN), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 452 .append(".") 453 .let { 454 if (richText.isChecked) it else it.toString() 455 } 456 457 private fun createLongText(): CharSequence = 458 SpannableStringBuilder("Here is a lot more text to share:") 459 .apply { 460 val colors = 461 arrayOf( 462 Color.RED, 463 Color.GREEN, 464 Color.BLUE, 465 Color.CYAN, 466 Color.MAGENTA, 467 Color.YELLOW, 468 Color.BLACK, 469 Color.DKGRAY, 470 Color.GRAY, 471 ) 472 for (color in colors) { 473 append("\n") 474 append( 475 createShortText(), BulletSpan(40, color, 20), 476 Spannable.SPAN_INCLUSIVE_EXCLUSIVE 477 ) 478 } 479 } 480 .let { 481 if (richText.isChecked) it else it.toString() 482 } 483 484 private fun createTextTitle(): CharSequence = 485 SpannableStringBuilder() 486 .append("Here's", UnderlineSpan(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 487 .append(" the ", StyleSpan(Typeface.ITALIC), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 488 .append("Title", ForegroundColorSpan(Color.RED), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 489 .append("!") 490 .let { 491 if (richText.isChecked) it else it.toString() 492 } 493 494 override fun onDestroy() { 495 super.onDestroy() 496 unregisterReceiver(customActionReceiver) 497 unregisterReceiver(refinementReceiver) 498 } 499 } 500 501 502