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