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