1 /* 2 * 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 com.android.cts.verifier.sharesheet; 18 19 import static android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY; 20 import static android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT; 21 import static android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT; 22 import static android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN; 23 24 import android.app.PendingIntent; 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.service.chooser.ChooserResult; 32 import android.service.chooser.Flags; 33 import android.util.Log; 34 import android.view.View; 35 import android.widget.Button; 36 import android.widget.TextView; 37 import android.widget.Toast; 38 39 import androidx.annotation.StringRes; 40 41 import com.android.cts.verifier.PassFailButtons; 42 import com.android.cts.verifier.R; 43 44 import java.util.Objects; 45 46 abstract class SharesheetChooserResultActivity extends PassFailButtons.Activity { 47 private static final String TAG = "ChooserResultTest"; 48 49 private static final String CHOOSER_RESULT = 50 "com.android.cts.verifier.sharesheet.CHOOSER_RESULT"; 51 52 private ChooserResult mResultExpected; 53 private ChooserResult mResultReceived; 54 private TextView mInstructiontext; 55 private Button mShareButton; 56 57 private View mAfterShareSection; 58 private Button mCouldNotLocate; 59 private Button mActionPerformed; 60 61 private Handler mHandler; 62 private boolean mWaitingForResult; 63 private boolean mResumed; 64 private boolean mTestPassed; 65 private boolean mTestComplete; 66 67 private final Runnable NO_RESULT_RECEIVED = this::handleNoResultReceived; 68 private final Runnable RELAUNCH_TEST = () -> startActivity(getTestActivityIntent()); 69 70 getTestActivityIntent()71 protected abstract Intent getTestActivityIntent(); 72 73 private final BroadcastReceiver mChooserCallbackReceiver = new BroadcastReceiver() { 74 @Override 75 public void onReceive(Context context, Intent intent) { 76 onChooserResultReceived(Objects.requireNonNull(intent.getParcelableExtra( 77 Intent.EXTRA_CHOOSER_RESULT, 78 ChooserResult.class))); 79 } 80 }; 81 setInstructions(@tringRes int instructions)82 protected final void setInstructions(@StringRes int instructions) { 83 mInstructiontext.setText(instructions); 84 } 85 setAfterShareButtonLabels(@tringRes int actionTakenLabel, @StringRes int notFoundLabel)86 protected final void setAfterShareButtonLabels(@StringRes int actionTakenLabel, 87 @StringRes int notFoundLabel) { 88 mActionPerformed.setText(actionTakenLabel); 89 mCouldNotLocate.setText(notFoundLabel); 90 } 91 setExpectedResult(ChooserResult result)92 protected final void setExpectedResult(ChooserResult result) { 93 mResultExpected = result; 94 } 95 createChooserIntent()96 protected abstract Intent createChooserIntent(); 97 98 @Override onCreate(Bundle savedInstanceState)99 protected void onCreate(Bundle savedInstanceState) { 100 super.onCreate(savedInstanceState); 101 mHandler = getMainThreadHandler(); 102 if (!Flags.enableChooserResult()) { 103 // If the API isn't enabled, immediately let the test pass. 104 Toast.makeText(this, R.string.sharesheet_skipping_for_flag, Toast.LENGTH_LONG).show(); 105 setTestResultAndFinish(true); 106 return; 107 } 108 setContentView(R.layout.sharesheet_chooser_result_activity); 109 setPassFailButtonClickListeners(); 110 111 mInstructiontext = requireViewById(R.id.instructions); 112 mAfterShareSection = requireViewById(R.id.sharesheet_result_test_instructions_after_share); 113 114 mCouldNotLocate = requireViewById(R.id.sharesheet_result_test_not_found); 115 mActionPerformed = requireViewById(R.id.sharesheet_result_test_pressed); 116 mAfterShareSection.setVisibility(View.GONE); 117 118 mShareButton = requireViewById(R.id.sharesheet_share_button); 119 mShareButton.setText(R.string.sharesheet_share_label); 120 mShareButton.setOnClickListener(v -> { 121 mWaitingForResult = true; 122 startActivity(createChooserIntent()); 123 }); 124 125 // Can't pass until steps are completed. 126 getPassButton().setVisibility(View.GONE); 127 128 } 129 130 @Override onStart()131 protected void onStart() { 132 super.onStart(); 133 registerReceiver(mChooserCallbackReceiver, new IntentFilter(CHOOSER_RESULT), 134 RECEIVER_NOT_EXPORTED); 135 } 136 137 @Override onStop()138 protected void onStop() { 139 super.onStop(); 140 unregisterReceiver(mChooserCallbackReceiver); 141 } 142 143 @Override onPause()144 protected void onPause() { 145 super.onPause(); 146 mResumed = false; 147 } 148 149 @Override onResume()150 protected void onResume() { 151 super.onResume(); 152 mResumed = true; 153 mHandler.removeCallbacks(RELAUNCH_TEST); 154 155 if (mTestComplete) { 156 finishTest(); 157 return; 158 } 159 160 if (mWaitingForResult && mResultReceived == null) { 161 Log.d(TAG, "waiting for result (100ms)"); 162 mHandler.postDelayed(NO_RESULT_RECEIVED, 100); 163 } 164 } 165 handleNoResultReceived()166 private void handleNoResultReceived() { 167 Log.d(TAG, "Timed out while waiting for result (100ms)"); 168 mWaitingForResult = false; 169 170 // No ChooserResult was received. Ask the user if they pressed the button (or retry) 171 mInstructiontext.setText(R.string.sharesheet_result_test_instructions_after_share); 172 mAfterShareSection.setVisibility(View.VISIBLE); 173 mShareButton.setText(R.string.sharesheet_result_test_try_again); 174 175 // If there's no action to take, then the test is passed. 176 mCouldNotLocate.setOnClickListener(v -> { 177 Toast.makeText(this, R.string.sharesheet_result_test_no_button, 178 Toast.LENGTH_SHORT).show(); 179 setTestResultAndFinish(true); 180 181 }); 182 183 // Tester performed the requested action but no result received. FAIL. 184 mActionPerformed.setOnClickListener(v -> { 185 Toast.makeText(this, R.string.sharesheet_result_test_no_result_message, 186 Toast.LENGTH_SHORT).show(); 187 setTestResultAndFinish(false); 188 }); 189 } 190 onChooserResultReceived(ChooserResult result)191 private void onChooserResultReceived(ChooserResult result) { 192 Log.d(TAG, "onChooserResultReceived: " + resultToString(result)); 193 mHandler.removeCallbacks(NO_RESULT_RECEIVED); 194 mResultReceived = result; 195 196 if (!mWaitingForResult) { 197 return; 198 } 199 200 mTestPassed = mResultExpected.equals(result); 201 mTestComplete = true; 202 203 if (mResumed) { 204 finishTest(); 205 } else { 206 mHandler.postDelayed(RELAUNCH_TEST, 100); 207 } 208 } 209 finishTest()210 private void finishTest() { 211 if (!mTestPassed) { 212 Log.d(TAG, 213 "ChooserResult incorrect!\n expected: " + resultToString(mResultExpected) 214 + "\nreceived: " + resultToString(mResultReceived)); 215 Toast.makeText(this, R.string.sharesheet_result_test_incorrect_result, 216 Toast.LENGTH_SHORT).show(); 217 } 218 setTestResultAndFinish(mTestPassed); 219 } 220 wrapWithChooserIntent(Intent shareIntent)221 protected Intent wrapWithChooserIntent(Intent shareIntent) { 222 Intent resultIntent = new Intent(CHOOSER_RESULT).setPackage(getPackageName()); 223 PendingIntent shareResultIntent = PendingIntent.getBroadcast( 224 /* context= */ this, 225 /* flags= */ 0, 226 /* intent= */ resultIntent, 227 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); 228 229 Intent chooserIntent = Intent.createChooser( 230 /* target= */ shareIntent, 231 /* title= */ null, 232 /* sender= */ shareResultIntent.getIntentSender() 233 ); 234 chooserIntent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); 235 chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 236 chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 237 return chooserIntent; 238 } 239 typeToString(int type)240 private static String typeToString(int type) { 241 switch (type) { 242 case CHOOSER_RESULT_SELECTED_COMPONENT: 243 return "SELECTED_COMPONENT"; 244 case CHOOSER_RESULT_COPY: 245 return "COPY"; 246 case CHOOSER_RESULT_EDIT: 247 return "EDIT"; 248 case CHOOSER_RESULT_UNKNOWN: 249 default: 250 return "UNKNOWN"; 251 } 252 } 253 resultToString(ChooserResult result)254 private static String resultToString(ChooserResult result) { 255 return "ChooserResult{" 256 + "type=" + typeToString(result.getType()) 257 + " component=" + result.getSelectedComponent() 258 + " isShortcut=" + result.isShortcut() 259 + "}"; 260 } 261 } 262