1 /* 2 * Copyright (C) 2013 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.notifications; 18 19 import android.app.Activity; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.app.Service; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.os.Bundle; 28 import android.os.IBinder; 29 import android.provider.Settings.Secure; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.Button; 35 import android.widget.ImageView; 36 import android.widget.TextView; 37 import com.android.cts.verifier.PassFailButtons; 38 import com.android.cts.verifier.R; 39 40 import java.util.ArrayList; 41 import java.util.Iterator; 42 import java.util.List; 43 import java.util.concurrent.LinkedBlockingQueue; 44 45 public abstract class InteractiveVerifierActivity extends PassFailButtons.Activity 46 implements Runnable { 47 private static final String TAG = "InteractiveVerifier"; 48 private static final String STATE = "state"; 49 private static final String STATUS = "status"; 50 private static LinkedBlockingQueue<String> sDeletedQueue = new LinkedBlockingQueue<String>(); 51 protected static final String LISTENER_PATH = "com.android.cts.verifier/" + 52 "com.android.cts.verifier.notifications.MockListener"; 53 protected static final int SETUP = 0; 54 protected static final int READY = 1; 55 protected static final int RETEST = 2; 56 protected static final int PASS = 3; 57 protected static final int FAIL = 4; 58 protected static final int WAIT_FOR_USER = 5; 59 60 protected static final int NOTIFICATION_ID = 1001; 61 62 // TODO remove these once b/10023397 is fixed 63 public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners"; 64 public static final String NOTIFICATION_LISTENER_SETTINGS = 65 "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"; 66 67 protected InteractiveTestCase mCurrentTest; 68 protected PackageManager mPackageManager; 69 protected NotificationManager mNm; 70 protected Context mContext; 71 protected Runnable mRunner; 72 protected View mHandler; 73 protected String mPackageString; 74 75 private LayoutInflater mInflater; 76 private ViewGroup mItemList; 77 private List<InteractiveTestCase> mTestList; 78 private Iterator<InteractiveTestCase> mTestOrder; 79 80 public static class DismissService extends Service { 81 @Override 82 public IBinder onBind(Intent intent) { 83 return null; 84 } 85 86 @Override 87 public void onStart(Intent intent, int startId) { 88 if(intent != null) { sDeletedQueue.offer(intent.getAction()); } 89 } 90 } 91 92 protected abstract class InteractiveTestCase { 93 int status; 94 private View view; 95 96 abstract View inflate(ViewGroup parent); 97 View getView(ViewGroup parent) { 98 if (view == null) { 99 view = inflate(parent); 100 } 101 return view; 102 } 103 104 /** @return true if the test should re-run when the test activity starts. */ 105 boolean autoStart() { 106 return false; 107 } 108 109 /** Set status to {@link #READY} to proceed, or {@link #SETUP} to try again. */ 110 void setUp() { status = READY; next(); }; 111 112 /** Set status to {@link #PASS} or @{link #FAIL} to proceed, or {@link #READY} to retry. */ 113 void test() { status = FAIL; next(); }; 114 115 /** Do not modify status. */ 116 void tearDown() { next(); }; 117 118 protected void logFail() { 119 logFail(null); 120 } 121 122 protected void logFail(String message) { 123 logWithStack("failed " + this.getClass().getSimpleName() + 124 ((message == null) ? "" : ": " + message)); 125 } 126 } 127 128 abstract int getTitleResource(); 129 abstract int getInstructionsResource(); 130 131 protected void onCreate(Bundle savedState) { 132 super.onCreate(savedState); 133 int savedStateIndex = (savedState == null) ? 0 : savedState.getInt(STATE, 0); 134 int savedStatus = (savedState == null) ? SETUP : savedState.getInt(STATUS, SETUP); 135 Log.i(TAG, "restored state(" + savedStateIndex + "}, status(" + savedStatus + ")"); 136 mContext = this; 137 mRunner = this; 138 mNm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 139 mPackageManager = getPackageManager(); 140 mInflater = getLayoutInflater(); 141 View view = mInflater.inflate(R.layout.nls_main, null); 142 mItemList = (ViewGroup) view.findViewById(R.id.nls_test_items); 143 mHandler = mItemList; 144 mTestList = new ArrayList<>(); 145 mTestList.addAll(createTestItems()); 146 for (InteractiveTestCase test: mTestList) { 147 mItemList.addView(test.getView(mItemList)); 148 } 149 mTestOrder = mTestList.iterator(); 150 for (int i = 0; i < savedStateIndex; i++) { 151 mCurrentTest = mTestOrder.next(); 152 mCurrentTest.status = PASS; 153 } 154 mCurrentTest = mTestOrder.next(); 155 mCurrentTest.status = savedStatus; 156 157 setContentView(view); 158 setPassFailButtonClickListeners(); 159 getPassButton().setEnabled(false); 160 161 setInfoResources(getTitleResource(), getInstructionsResource(), -1); 162 } 163 164 @Override 165 protected void onSaveInstanceState (Bundle outState) { 166 final int stateIndex = mTestList.indexOf(mCurrentTest); 167 outState.putInt(STATE, stateIndex); 168 outState.putInt(STATUS, mCurrentTest.status); 169 Log.i(TAG, "saved state(" + stateIndex + "}, status(" + (mCurrentTest.status) + ")"); 170 } 171 172 @Override 173 protected void onResume() { 174 super.onResume(); 175 if (mCurrentTest.autoStart()) { 176 mCurrentTest.status = READY; 177 } 178 next(); 179 } 180 181 // Interface Utilities 182 183 protected void markItem(InteractiveTestCase test) { 184 if (test == null) { return; } 185 View item = test.view; 186 ImageView status = (ImageView) item.findViewById(R.id.nls_status); 187 View button = item.findViewById(R.id.nls_action_button); 188 switch (test.status) { 189 case WAIT_FOR_USER: 190 status.setImageResource(R.drawable.fs_warning); 191 break; 192 193 case SETUP: 194 case READY: 195 case RETEST: 196 status.setImageResource(R.drawable.fs_clock); 197 break; 198 199 case FAIL: 200 status.setImageResource(R.drawable.fs_error); 201 button.setClickable(false); 202 button.setEnabled(false); 203 break; 204 205 case PASS: 206 status.setImageResource(R.drawable.fs_good); 207 button.setClickable(false); 208 button.setEnabled(false); 209 break; 210 211 } 212 status.invalidate(); 213 } 214 215 protected View createNlsSettingsItem(ViewGroup parent, int messageId) { 216 return createUserItem(parent, R.string.nls_start_settings, messageId); 217 } 218 219 protected View createRetryItem(ViewGroup parent, int messageId, Object... messageFormatArgs) { 220 return createUserItem(parent, R.string.attention_ready, messageId, messageFormatArgs); 221 } 222 223 protected View createUserItem(ViewGroup parent, int actionId, int messageId, 224 Object... messageFormatArgs) { 225 View item = mInflater.inflate(R.layout.nls_item, parent, false); 226 TextView instructions = (TextView) item.findViewById(R.id.nls_instructions); 227 instructions.setText(getString(messageId, messageFormatArgs)); 228 Button button = (Button) item.findViewById(R.id.nls_action_button); 229 button.setText(actionId); 230 button.setTag(actionId); 231 return item; 232 } 233 234 protected View createAutoItem(ViewGroup parent, int stringId) { 235 View item = mInflater.inflate(R.layout.nls_item, parent, false); 236 TextView instructions = (TextView) item.findViewById(R.id.nls_instructions); 237 instructions.setText(stringId); 238 View button = item.findViewById(R.id.nls_action_button); 239 button.setVisibility(View.GONE); 240 return item; 241 } 242 243 // Test management 244 245 abstract protected List<InteractiveTestCase> createTestItems(); 246 247 public void run() { 248 if (mCurrentTest == null) { return; } 249 markItem(mCurrentTest); 250 switch (mCurrentTest.status) { 251 case SETUP: 252 Log.i(TAG, "running setup for: " + mCurrentTest.getClass().getSimpleName()); 253 mCurrentTest.setUp(); 254 break; 255 256 case WAIT_FOR_USER: 257 Log.i(TAG, "waiting for user: " + mCurrentTest.getClass().getSimpleName()); 258 break; 259 260 case READY: 261 case RETEST: 262 Log.i(TAG, "running test for: " + mCurrentTest.getClass().getSimpleName()); 263 mCurrentTest.test(); 264 break; 265 266 case FAIL: 267 Log.i(TAG, "FAIL: " + mCurrentTest.getClass().getSimpleName()); 268 mCurrentTest = null; 269 break; 270 271 case PASS: 272 Log.i(TAG, "pass for: " + mCurrentTest.getClass().getSimpleName()); 273 mCurrentTest.tearDown(); 274 if (mTestOrder.hasNext()) { 275 mCurrentTest = mTestOrder.next(); 276 Log.i(TAG, "next test is: " + mCurrentTest.getClass().getSimpleName()); 277 } else { 278 Log.i(TAG, "no more tests"); 279 mCurrentTest = null; 280 getPassButton().setEnabled(true); 281 mNm.cancelAll(); 282 } 283 break; 284 } 285 markItem(mCurrentTest); 286 } 287 288 /** 289 * Return to the state machine to progress through the tests. 290 */ 291 protected void next() { 292 mHandler.removeCallbacks(mRunner); 293 mHandler.post(mRunner); 294 } 295 296 /** 297 * Wait for things to settle before returning to the state machine. 298 */ 299 protected void delay() { 300 delay(3000); 301 } 302 303 /** 304 * Wait for some time. 305 */ 306 protected void delay(long waitTime) { 307 mHandler.removeCallbacks(mRunner); 308 mHandler.postDelayed(mRunner, waitTime); 309 } 310 311 // UI callbacks 312 313 public void launchSettings() { 314 startActivity(new Intent(NOTIFICATION_LISTENER_SETTINGS)); 315 } 316 317 public void actionPressed(View v) { 318 Object tag = v.getTag(); 319 if (tag instanceof Integer) { 320 int id = ((Integer) tag).intValue(); 321 if (id == R.string.nls_start_settings) { 322 launchSettings(); 323 } else if (id == R.string.attention_ready) { 324 mCurrentTest.status = READY; 325 next(); 326 } 327 } 328 } 329 330 // Utilities 331 332 protected PendingIntent makeIntent(int code, String tag) { 333 Intent intent = new Intent(tag); 334 intent.setComponent(new ComponentName(mContext, DismissService.class)); 335 PendingIntent pi = PendingIntent.getService(mContext, code, intent, 336 PendingIntent.FLAG_UPDATE_CURRENT); 337 return pi; 338 } 339 340 protected boolean checkEquals(long expected, long actual, String message) { 341 if (expected == actual) { 342 return true; 343 } 344 logWithStack(String.format(message, expected, actual)); 345 return false; 346 } 347 348 protected boolean checkEquals(String expected, String actual, String message) { 349 if (expected.equals(actual)) { 350 return true; 351 } 352 logWithStack(String.format(message, expected, actual)); 353 return false; 354 } 355 356 protected boolean checkFlagSet(int expected, int actual, String message) { 357 if ((expected & actual) != 0) { 358 return true; 359 } 360 logWithStack(String.format(message, expected, actual)); 361 return false; 362 }; 363 364 protected void logWithStack(String message) { 365 Throwable stackTrace = new Throwable(); 366 stackTrace.fillInStackTrace(); 367 Log.e(TAG, message, stackTrace); 368 } 369 370 // Common Tests: useful for the side-effects they generate 371 372 protected class IsEnabledTest extends InteractiveTestCase { 373 @Override 374 View inflate(ViewGroup parent) { 375 return createNlsSettingsItem(parent, R.string.nls_enable_service); 376 } 377 378 @Override 379 boolean autoStart() { 380 return true; 381 } 382 383 @Override 384 void test() { 385 Intent settings = new Intent(NOTIFICATION_LISTENER_SETTINGS); 386 if (settings.resolveActivity(mPackageManager) == null) { 387 logFail("no settings activity"); 388 status = FAIL; 389 } else { 390 String listeners = Secure.getString(getContentResolver(), 391 ENABLED_NOTIFICATION_LISTENERS); 392 if (listeners != null && listeners.contains(LISTENER_PATH)) { 393 status = PASS; 394 } else { 395 status = WAIT_FOR_USER; 396 } 397 next(); 398 } 399 } 400 401 void tearDown() { 402 // wait for the service to start 403 delay(); 404 } 405 } 406 407 protected class ServiceStartedTest extends InteractiveTestCase { 408 @Override 409 View inflate(ViewGroup parent) { 410 return createAutoItem(parent, R.string.nls_service_started); 411 } 412 413 @Override 414 void test() { 415 MockListener.probeListenerStatus(mContext, 416 new MockListener.StatusCatcher() { 417 @Override 418 public void accept(int result) { 419 if (result == Activity.RESULT_OK) { 420 status = PASS; 421 next(); 422 } else { 423 logFail(); 424 status = RETEST; 425 delay(); 426 } 427 } 428 }); 429 delay(); // in case the catcher never returns 430 } 431 432 @Override 433 void tearDown() { 434 MockListener.resetListenerData(mContext); 435 delay(); 436 } 437 } 438 } 439