1 /* <lambda>null2 * Copyright (C) 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 android.platform.helpers.notesrole 18 19 import android.content.BroadcastReceiver 20 import android.content.ClipData 21 import android.content.ClipDescription.MIMETYPE_TEXT_INTENT 22 import android.content.ClipDescription.MIMETYPE_TEXT_URILIST 23 import android.content.Context 24 import android.content.Intent 25 import android.content.Intent.ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE 26 import android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED 27 import android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS 28 import android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE 29 import android.content.IntentFilter 30 import android.net.Uri 31 import android.os.Bundle 32 import android.view.View 33 import android.widget.Button 34 import android.widget.ImageView 35 import android.widget.LinearLayout 36 import android.widget.TextView 37 import androidx.activity.ComponentActivity 38 import androidx.activity.result.ActivityResult 39 import androidx.activity.result.ActivityResultLauncher 40 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult 41 import androidx.core.view.setPadding 42 43 /** Sample notes app activity for app clips verification in end to end tests. */ 44 class NotesAppActivity : ComponentActivity() { 45 46 private val finishBroadcastReceiver: BroadcastReceiver = 47 object : BroadcastReceiver() { 48 override fun onReceive(context: Context?, intent: Intent?) { 49 finish() 50 } 51 } 52 53 private lateinit var appClipsLauncher: ActivityResultLauncher<Intent> 54 private lateinit var rootLinearLayout: LinearLayout 55 private var screenshotUri: Uri? = null 56 57 override fun onCreate(savedInstanceState: Bundle?) { 58 super.onCreate(savedInstanceState) 59 60 registerReceiver( 61 finishBroadcastReceiver, 62 IntentFilter(FINISH_NOTES_APP_ACTIVITY_ACTION), 63 RECEIVER_EXPORTED 64 ) 65 66 appClipsLauncher = 67 registerForActivityResult(StartActivityForResult(), this::onAppClipsActivityResult) 68 69 actionBar?.hide() 70 setContentView(createContentView()) 71 } 72 73 override fun onDestroy() { 74 super.onDestroy() 75 unregisterReceiver(finishBroadcastReceiver) 76 screenshotUri?.let { contentResolver.delete(it, /* extras= */ null) } 77 } 78 79 private fun createContentView(): View = 80 LinearLayout(this) 81 .apply { 82 orientation = LinearLayout.VERTICAL 83 setPadding(PADDING) 84 addView(createTriggerAppClipsButton()) 85 } 86 .also { rootLinearLayout = it } 87 88 private fun createTriggerAppClipsButton(): Button = 89 Button(this).apply { 90 text = APP_CLIPS_BUTTON_TEXT 91 setOnClickListener { 92 appClipsLauncher.launch(Intent(ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)) 93 } 94 } 95 96 private fun onAppClipsActivityResult(activityResult: ActivityResult) { 97 // Activity/app is killed after each test, so de-clutter views for easier verification. 98 rootLinearLayout.removeAllViews() 99 100 // Add a text view with specific text to make it easier for verification in tests. 101 TextView(this).apply { text = RESPONSE_TEXT }.also { rootLinearLayout.addView(it) } 102 103 val result = activityResult.data 104 val statusCode = 105 result?.getIntExtra( 106 EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, 107 CAPTURE_CONTENT_FOR_NOTE_FAILED 108 ) 109 // In case app clips returns error, show it and return early. 110 if (statusCode != CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { 111 showErrorViewWithText("Error status code: $statusCode") 112 return 113 } 114 115 // Show backlinks data, if available. 116 rootLinearLayout.addView( 117 result.clipData.let { clipData -> 118 TextView(this).apply { 119 text = 120 clipData?.let { getStringFormattedBacklinksData(it) } ?: NO_BACKLINKS_TEXT 121 } 122 } 123 ) 124 125 // Show app clips screenshot, if available. 126 result.data 127 ?.also { screenshotUri = it } 128 ?.let { uri -> 129 val imageView = ImageView(this).apply { setImageURI(uri) } 130 rootLinearLayout.addView(imageView) 131 } 132 } 133 134 private fun showErrorViewWithText(error: String) { 135 TextView(this).apply { text = error }.also { rootLinearLayout.addView(it) } 136 } 137 138 companion object { 139 private const val PADDING = 50 140 141 const val FINISH_NOTES_APP_ACTIVITY_ACTION: String = "FINISH_NOTES_APP_ACTIVITY_ACTION" 142 const val APP_CLIPS_BUTTON_TEXT: String = "TRIGGER APP CLIPS" 143 const val RESPONSE_TEXT: String = "RESPONSE" 144 const val NO_BACKLINKS_TEXT: String = "NO BACKLINKS AVAILABLE" 145 146 /** Formats Backlinks [ClipData] in readable format for verification. */ 147 fun getStringFormattedBacklinksData(clipData: ClipData): String = 148 clipData.let { data -> 149 data 150 .getItemAt(0) 151 .let { dataItem -> 152 when (data.description.getMimeType(0)) { 153 MIMETYPE_TEXT_URILIST -> dataItem.uri.toString() 154 MIMETYPE_TEXT_INTENT -> dataItem.intent.toString() 155 else -> dataItem.toString() 156 } 157 } 158 .let { backlinksData -> "${data.description.label}\n$backlinksData" } 159 } 160 } 161 } 162