xref: /aosp_15_r20/development/apps/ShareTest/src/com/android/sharetest/ShareTestActivity.kt (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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