1 /*
2  * Copyright (C) 2017 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.autofillservice.cts;
18 
19 import static android.autofillservice.cts.CannedFillResponse.ResponseType.NULL;
20 
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import android.app.Activity;
24 import android.app.PendingIntent;
25 import android.app.assist.AssistStructure;
26 import android.autofillservice.cts.CannedFillResponse.CannedDataset;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentSender;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.Parcelable;
34 import android.util.Log;
35 import android.util.SparseArray;
36 import android.view.autofill.AutofillManager;
37 
38 import com.google.common.base.Preconditions;
39 
40 import java.util.ArrayList;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * This class simulates authentication at the dataset at reponse level
46  */
47 public class AuthenticationActivity extends AbstractAutoFillActivity {
48 
49     private static final String TAG = "AuthenticationActivity";
50     private static final String EXTRA_DATASET_ID = "dataset_id";
51     private static final String EXTRA_RESPONSE_ID = "response_id";
52 
53     /**
54      * When launched with this intent, it will pass it back to the
55      * {@link AutofillManager#EXTRA_CLIENT_STATE} of the result.
56      */
57     private static final String EXTRA_OUTPUT_CLIENT_STATE = "output_client_state";
58 
59 
60     private static final int MSG_WAIT_FOR_LATCH = 1;
61 
62     private static Bundle sData;
63     private static final SparseArray<CannedDataset> sDatasets = new SparseArray<>();
64     private static final SparseArray<CannedFillResponse> sResponses = new SparseArray<>();
65     private static final ArrayList<PendingIntent> sPendingIntents = new ArrayList<>();
66 
67     private static Object sLock = new Object();
68 
69     // Guarded by sLock
70     private static int sResultCode;
71 
72     // Guarded by sLock
73     // Used to block response until it's counted down.
74     private static CountDownLatch sResponseLatch;
75 
76     private Handler mHandler;
77 
resetStaticState()78     static void resetStaticState() {
79         setResultCode(RESULT_OK);
80         sDatasets.clear();
81         sResponses.clear();
82         for (int i = 0; i < sPendingIntents.size(); i++) {
83             final PendingIntent pendingIntent = sPendingIntents.get(i);
84             Log.d(TAG, "Cancelling " + pendingIntent);
85             pendingIntent.cancel();
86         }
87     }
88 
89     /**
90      * Creates an {@link IntentSender} with the given unique id for the given dataset.
91      */
createSender(Context context, int id, CannedDataset dataset)92     public static IntentSender createSender(Context context, int id, CannedDataset dataset) {
93         return createSender(context, id, dataset, null);
94     }
95 
createSender(Context context, int id, CannedDataset dataset, Bundle outClientState)96     public static IntentSender createSender(Context context, int id,
97             CannedDataset dataset, Bundle outClientState) {
98         Preconditions.checkArgument(id > 0, "id must be positive");
99         Preconditions.checkState(sDatasets.get(id) == null, "already have id");
100         sDatasets.put(id, dataset);
101         return createSender(context, EXTRA_DATASET_ID, id, outClientState);
102     }
103 
104     /**
105      * Creates an {@link IntentSender} with the given unique id for the given fill response.
106      */
createSender(Context context, int id, CannedFillResponse response)107     public static IntentSender createSender(Context context, int id,
108             CannedFillResponse response) {
109         return createSender(context, id, response, null);
110     }
111 
createSender(Context context, int id, CannedFillResponse response, Bundle outData)112     public static IntentSender createSender(Context context, int id,
113             CannedFillResponse response, Bundle outData) {
114         Preconditions.checkArgument(id > 0, "id must be positive");
115         Preconditions.checkState(sResponses.get(id) == null, "already have id");
116         sResponses.put(id, response);
117         return createSender(context, EXTRA_RESPONSE_ID, id, outData);
118     }
119 
createSender(Context context, String extraName, int id, Bundle outClientState)120     private static IntentSender createSender(Context context, String extraName, int id,
121             Bundle outClientState) {
122         final Intent intent = new Intent(context, AuthenticationActivity.class);
123         intent.putExtra(extraName, id);
124         if (outClientState != null) {
125             Log.d(TAG, "Create with " + outClientState + " as " + EXTRA_OUTPUT_CLIENT_STATE);
126             intent.putExtra(EXTRA_OUTPUT_CLIENT_STATE, outClientState);
127         }
128         final PendingIntent pendingIntent = PendingIntent.getActivity(context, id, intent, 0);
129         sPendingIntents.add(pendingIntent);
130         return pendingIntent.getIntentSender();
131     }
132 
133     /**
134      * Creates an {@link IntentSender} with the given unique id.
135      */
createSender(Context context, int id)136     public static IntentSender createSender(Context context, int id) {
137         Preconditions.checkArgument(id > 0, "id must be positive");
138         return PendingIntent
139                 .getActivity(context, id, new Intent(context, AuthenticationActivity.class),
140                         PendingIntent.FLAG_CANCEL_CURRENT)
141                 .getIntentSender();
142     }
143 
getData()144     public static Bundle getData() {
145         final Bundle data = sData;
146         sData = null;
147         return data;
148     }
149 
150     /**
151      * Sets the value that's passed to {@link Activity#setResult(int, Intent)} when on
152      * {@link Activity#onCreate(Bundle)}.
153      */
setResultCode(int resultCode)154     public static void setResultCode(int resultCode) {
155         synchronized (sLock) {
156             sResultCode = resultCode;
157         }
158     }
159 
160     /**
161      * Sets the value that's passed to {@link Activity#setResult(int, Intent)}, but only calls it
162      * after the {@code latch}'s countdown reaches {@code 0}.
163      */
setResultCode(CountDownLatch latch, int resultCode)164     public static void setResultCode(CountDownLatch latch, int resultCode) {
165         synchronized (sLock) {
166             sResponseLatch = latch;
167             sResultCode = resultCode;
168         }
169     }
170 
171     @Override
onCreate(Bundle savedInstanceState)172     protected void onCreate(Bundle savedInstanceState) {
173         super.onCreate(savedInstanceState);
174 
175         mHandler = new Handler(Looper.getMainLooper(), (m) -> {
176             switch (m.what) {
177                 case MSG_WAIT_FOR_LATCH:
178                     waitForLatchAndDoIt();
179                     break;
180                 default:
181                     throw new IllegalArgumentException("invalid message: " + m);
182             }
183             return true;
184         });
185 
186         if (sResponseLatch != null) {
187             Log.d(TAG, "Delaying message until latch is counted down");
188             mHandler.dispatchMessage(mHandler.obtainMessage(MSG_WAIT_FOR_LATCH));
189         } else {
190             doIt();
191         }
192     }
193 
waitForLatchAndDoIt()194     private void waitForLatchAndDoIt() {
195         try {
196             final boolean called = sResponseLatch.await(5, TimeUnit.SECONDS);
197             if (!called) {
198                 throw new IllegalStateException("latch not called in 5 seconds");
199             }
200             doIt();
201         } catch (InterruptedException e) {
202             Thread.interrupted();
203             throw new IllegalStateException("interrupted");
204         }
205     }
206 
doIt()207     private void doIt() {
208         // We should get the assist structure...
209         final AssistStructure structure = getIntent().getParcelableExtra(
210                 AutofillManager.EXTRA_ASSIST_STRUCTURE);
211         assertWithMessage("structure not called").that(structure).isNotNull();
212 
213         // and the bundle
214         sData = getIntent().getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE);
215         final CannedFillResponse response =
216                 sResponses.get(getIntent().getIntExtra(EXTRA_RESPONSE_ID, 0));
217         final CannedDataset dataset =
218                 sDatasets.get(getIntent().getIntExtra(EXTRA_DATASET_ID, 0));
219 
220         final Parcelable result;
221 
222         if (response != null) {
223             if (response.getResponseType() == NULL) {
224                 result = null;
225             } else {
226                 result = response.asFillResponse(/* contexts= */ null,
227                         (id) -> Helper.findNodeByResourceId(structure, id));
228             }
229         } else if (dataset != null) {
230             result = dataset.asDataset((id) -> Helper.findNodeByResourceId(structure, id));
231         } else {
232             throw new IllegalStateException("no dataset or response");
233         }
234 
235         // Pass on the auth result
236         final Intent intent = new Intent();
237         intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
238 
239         final Bundle outClientState = getIntent().getBundleExtra(EXTRA_OUTPUT_CLIENT_STATE);
240         if (outClientState != null) {
241             Log.d(TAG, "Adding " + outClientState + " as " + AutofillManager.EXTRA_CLIENT_STATE);
242             intent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, outClientState);
243         }
244 
245         final int resultCode;
246         synchronized (sLock) {
247             resultCode = sResultCode;
248         }
249         Log.d(TAG, "Returning code " + resultCode);
250         setResult(resultCode, intent);
251 
252         // Done
253         finish();
254     }
255 }
256