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