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