1 /* 2 * Copyright (C) 2015 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.net; 18 19 import com.android.cts.verifier.PassFailButtons; 20 import com.android.cts.verifier.R; 21 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.pm.PackageManager; 27 import android.graphics.Typeface; 28 import android.net.ConnectivityManager; 29 import android.net.ConnectivityManager.NetworkCallback; 30 import android.net.LinkAddress; 31 import android.net.LinkProperties; 32 import android.net.Network; 33 import android.net.NetworkCapabilities; 34 import android.net.NetworkRequest; 35 import android.os.BatteryManager; 36 import android.os.Bundle; 37 import android.os.PowerManager; 38 import android.os.SystemClock; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.WindowManager.LayoutParams; 42 import android.widget.Button; 43 import android.widget.ScrollView; 44 import android.widget.TextView; 45 46 import java.io.BufferedReader; 47 import java.io.InputStreamReader; 48 import java.io.IOException; 49 import java.lang.reflect.Field; 50 import java.net.Inet6Address; 51 import java.net.InetAddress; 52 import java.net.HttpURLConnection; 53 import java.net.UnknownHostException; 54 import java.net.URL; 55 import java.util.concurrent.atomic.AtomicBoolean; 56 import java.util.Random; 57 58 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; 59 import static android.net.NetworkCapabilities.TRANSPORT_WIFI; 60 61 /** 62 * A CTS Verifier test case for testing IPv6 network connectivity while the screen is off. 63 * 64 * This tests that Wi-Fi implementations are compliant with section 7.4.5 65 * ("Minimum Network Capability") of the CDD. Specifically, it requires that: "unicast IPv6 66 * packets sent to the device MUST NOT be dropped, even when the screen is not in an active 67 * state." 68 * 69 * The verification is attempted as follows: 70 * 71 * [1] The device must have Wi-Fi capability. 72 * [2] The device must join an IPv6-capable network (basic IPv6 connectivity to an 73 * Internet resource is tested). 74 * [3] If the device has a battery, the device must be disconnected from any power source. 75 * [4] The screen is put to sleep. 76 * [5] After two minutes, another IPv6 connectivity test is performed. 77 */ 78 public class ConnectivityScreenOffTestActivity extends PassFailButtons.Activity { 79 80 private static final String TAG = ConnectivityScreenOffTestActivity.class.getSimpleName(); 81 private static final String V6CONN_URL = "https://ipv6.google.com/generate_204"; 82 private static final String V6ADDR_URL = "https://google-ipv6test.appspot.com/ip.js?fmt=text"; 83 84 private static final long MIN_SCREEN_OFF_MS = 1000 * (30 + (long) new Random().nextInt(51)); 85 private static final long MIN_POWER_DISCONNECT_MS = MIN_SCREEN_OFF_MS; 86 87 private final Object mLock; 88 private final AppState mState; 89 private BackgroundTestingThread mTestingThread; 90 91 private final ScreenAndPlugStateReceiver mReceiver; 92 private final IntentFilter mIntentFilter; 93 private boolean mWaitForPowerDisconnected; 94 95 private PowerManager mPowerManager; 96 private PowerManager.WakeLock mWakeLock; 97 private ConnectivityManager mCM; 98 private NetworkCallback mNetworkCallback; 99 100 private ScrollView mScrollView; 101 private TextView mTextView; 102 private long mUserActivityTimeout = -1; 103 104 ConnectivityScreenOffTestActivity()105 public ConnectivityScreenOffTestActivity() { 106 mLock = new Object(); 107 mState = new AppState(); 108 109 mReceiver = new ScreenAndPlugStateReceiver(); 110 111 mIntentFilter = new IntentFilter(); 112 mIntentFilter.addAction(Intent.ACTION_SCREEN_ON); 113 mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF); 114 mIntentFilter.addAction(Intent.ACTION_POWER_CONNECTED); 115 mIntentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED); 116 } 117 118 @Override onCreate(Bundle savedInstanceState)119 protected void onCreate(Bundle savedInstanceState) { 120 super.onCreate(savedInstanceState); 121 configureFromSystemServices(); 122 setupUserInterface(); 123 } 124 125 @Override onDestroy()126 protected void onDestroy() { 127 clearNetworkCallback(); 128 stopAnyExistingTestingThread(); 129 unregisterReceiver(mReceiver); 130 mWakeLock.release(); 131 super.onDestroy(); 132 } 133 setupUserInterface()134 private void setupUserInterface() { 135 setContentView(R.layout.network_screen_off); 136 setPassFailButtonClickListeners(); 137 getPassButton().setEnabled(false); 138 setInfoResources( 139 R.string.network_screen_off_test, 140 R.string.network_screen_off_test_instructions, 141 -1); 142 143 mScrollView = (ScrollView) findViewById(R.id.scroll); 144 mTextView = (TextView) findViewById(R.id.text); 145 mTextView.setTypeface(Typeface.MONOSPACE); 146 mTextView.setTextSize(14.0f); 147 148 // Get the start button and attach the listener. 149 getStartButton().setOnClickListener(new View.OnClickListener() { 150 @Override 151 public void onClick(View v) { 152 getStartButton().setEnabled(false); 153 startTest(); 154 } 155 }); 156 } 157 configureFromSystemServices()158 private void configureFromSystemServices() { 159 final Intent batteryInfo = registerReceiver( 160 null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 161 162 // Whether or not this device (currently) has a battery. 163 mWaitForPowerDisconnected = 164 batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) && !isLeanback(); 165 166 // Check if the device is already on battery power. 167 if (mWaitForPowerDisconnected) { 168 BatteryManager battMgr = (BatteryManager) getSystemService(Context.BATTERY_SERVICE); 169 if (!battMgr.isCharging()) { 170 mState.setPowerDisconnected(); 171 } 172 } 173 174 mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); 175 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 176 mWakeLock.acquire(); 177 178 registerReceiver(mReceiver, mIntentFilter); 179 180 mCM = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 181 } 182 clearNetworkCallback()183 private void clearNetworkCallback() { 184 if (mNetworkCallback != null) { 185 mCM.unregisterNetworkCallback(mNetworkCallback); 186 mNetworkCallback = null; 187 } 188 } 189 stopAnyExistingTestingThread()190 private void stopAnyExistingTestingThread() { 191 synchronized (mLock) { 192 if (mTestingThread != null) { 193 // The testing thread will observe this and exit on its own (eventually). 194 mTestingThread.setStopped(); 195 } 196 } 197 } 198 setTestPassing()199 private void setTestPassing() { 200 logAndUpdate("Test PASSED!"); 201 runOnUiThread(new Runnable() { 202 @Override 203 public void run() { 204 getPassButton().setEnabled(true); 205 } 206 }); 207 } 208 logAndUpdate(final String msg)209 private void logAndUpdate(final String msg) { 210 Log.d(TAG, msg); 211 runOnUiThread(new Runnable() { 212 @Override 213 public void run() { 214 mTextView.append(msg); 215 mTextView.append("\n"); 216 mScrollView.fullScroll(View.FOCUS_DOWN); // Scroll to bottom 217 } 218 }); 219 } 220 getStartButton()221 private Button getStartButton() { 222 return (Button) findViewById(R.id.start_btn); 223 } 224 setUserActivityTimeout(long timeout)225 private void setUserActivityTimeout(long timeout) { 226 final LayoutParams params = getWindow().getAttributes(); 227 228 try { 229 final Field field = params.getClass().getField("userActivityTimeout"); 230 // Save the original value. 231 if (mUserActivityTimeout < 0) { 232 mUserActivityTimeout = field.getLong(params); 233 Log.d(TAG, "saving userActivityTimeout: " + mUserActivityTimeout); 234 } 235 field.setLong(params, 1); 236 } catch (NoSuchFieldException e) { 237 Log.d(TAG, "No luck with userActivityTimeout: ", e); 238 return; 239 } catch (IllegalAccessException e) { 240 Log.d(TAG, "No luck with userActivityTimeout: ", e); 241 return; 242 } 243 244 getWindow().setAttributes(params); 245 } 246 tryScreenOff()247 private void tryScreenOff() { 248 runOnUiThread(new Runnable() { 249 @Override 250 public void run() { 251 setUserActivityTimeout(1); 252 } 253 }); 254 } 255 tryScreenOn()256 private void tryScreenOn() { 257 runOnUiThread(new Runnable() { 258 @Override 259 public void run() { 260 PowerManager.WakeLock screenOnLock = mPowerManager.newWakeLock( 261 PowerManager.FULL_WAKE_LOCK 262 | PowerManager.ACQUIRE_CAUSES_WAKEUP 263 | PowerManager.ON_AFTER_RELEASE, TAG + ":screenOn"); 264 screenOnLock.acquire(); 265 setUserActivityTimeout((mUserActivityTimeout > 0) 266 ? mUserActivityTimeout 267 : 30); // No good value to restore, use 30 seconds. 268 screenOnLock.release(); 269 } 270 }); 271 } 272 startTest()273 private void startTest() { 274 clearNetworkCallback(); 275 stopAnyExistingTestingThread(); 276 mTextView.setText(""); 277 logAndUpdate("Starting test..."); 278 279 mCM.registerNetworkCallback( 280 new NetworkRequest.Builder() 281 .addTransportType(TRANSPORT_WIFI) 282 .addCapability(NET_CAPABILITY_INTERNET) 283 .build(), 284 createNetworkCallback()); 285 286 new BackgroundTestingThread().start(); 287 } 288 289 /** 290 * TODO(ek): Evaluate reworking the code roughly as follows: 291 * - Move all the shared state here, including mWaitForPowerDisconnected 292 * (and mTestingThread). 293 * - Move from synchronizing on mLock to synchronizing on this since the 294 * AppState object is final, and delete mLock. 295 * - Synchronize the methods below, and add some required new methods. 296 * - Remove copying entire state into the BackgroundTestingThread. 297 */ 298 class AppState { 299 Network mNetwork; 300 LinkProperties mLinkProperties; 301 long mScreenOffTime; 302 long mPowerDisconnectTime; 303 boolean mPassedInitialIPv6Check; 304 setNetwork(Network network)305 void setNetwork(Network network) { 306 mNetwork = network; 307 mLinkProperties = null; 308 mPassedInitialIPv6Check = false; 309 } 310 setScreenOn()311 void setScreenOn() { mScreenOffTime = 0; } setScreenOff()312 void setScreenOff() { mScreenOffTime = SystemClock.elapsedRealtime(); } validScreenStateForTesting()313 boolean validScreenStateForTesting() { return (mScreenOffTime > 0); } 314 setPowerConnected()315 void setPowerConnected() { mPowerDisconnectTime = 0; } setPowerDisconnected()316 void setPowerDisconnected() { mPowerDisconnectTime = SystemClock.elapsedRealtime(); } validPowerStateForTesting()317 boolean validPowerStateForTesting() { 318 return !mWaitForPowerDisconnected || (mPowerDisconnectTime > 0); 319 } 320 } 321 322 class ScreenAndPlugStateReceiver extends BroadcastReceiver { 323 @Override onReceive(Context context, Intent intent)324 public void onReceive(Context context, Intent intent) { 325 String action = intent.getAction(); 326 if (Intent.ACTION_SCREEN_ON.equals(action)) { 327 Log.d(TAG, "got ACTION_SCREEN_ON"); 328 synchronized (mLock) { 329 mState.setScreenOn(); 330 mLock.notify(); 331 } 332 } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { 333 Log.d(TAG, "got ACTION_SCREEN_OFF"); 334 synchronized (mLock) { 335 mState.setScreenOff(); 336 mLock.notify(); 337 } 338 } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { 339 Log.d(TAG, "got ACTION_POWER_CONNECTED"); 340 synchronized (mLock) { 341 mState.setPowerConnected(); 342 mLock.notify(); 343 } 344 } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { 345 Log.d(TAG, "got ACTION_POWER_DISCONNECTED"); 346 synchronized (mLock) { 347 mState.setPowerDisconnected(); 348 mLock.notify(); 349 } 350 } 351 } 352 } 353 createNetworkCallback()354 private NetworkCallback createNetworkCallback() { 355 return new NetworkCallback() { 356 @Override 357 public void onAvailable(Network network) { 358 synchronized (mLock) { 359 mState.setNetwork(network); 360 mLock.notify(); 361 } 362 } 363 364 @Override 365 public void onLost(Network network) { 366 synchronized (mLock) { 367 if (network.equals(mState.mNetwork)) { 368 mState.setNetwork(null); 369 mLock.notify(); 370 } 371 } 372 } 373 374 @Override 375 public void onLinkPropertiesChanged(Network network, LinkProperties newLp) { 376 synchronized (mLock) { 377 if (network.equals(mState.mNetwork)) { 378 mState.mLinkProperties = newLp; 379 mLock.notify(); 380 } 381 } 382 } 383 }; 384 } 385 386 private class BackgroundTestingThread extends Thread { 387 final int POLLING_INTERVAL_MS = 5000; 388 final int CONNECTIVITY_CHECKING_INTERVAL_MS = 1000 + 100 * (new Random().nextInt(20)); 389 final int MAX_CONNECTIVITY_CHECKS = 3; 390 final AppState localState = new AppState(); 391 final AtomicBoolean isRunning = new AtomicBoolean(false); 392 int numConnectivityChecks = 0; 393 int numConnectivityChecksPassing = 0; 394 395 @Override 396 public void run() { 397 Log.d(TAG, getId() + " started"); 398 399 maybeWaitForPreviousThread(); 400 401 try { 402 mainLoop(); 403 } finally { 404 runOnUiThread(new Runnable() { 405 @Override 406 public void run() { 407 getStartButton().setEnabled(true); 408 } 409 }); 410 tryScreenOn(); 411 } 412 413 synchronized (mLock) { mTestingThread = null; } 414 415 Log.d(TAG, getId() + " exiting"); 416 } 417 418 private void mainLoop() { 419 int nextSleepDurationMs = 0; 420 421 while (stillRunning()) { 422 awaitNotification(nextSleepDurationMs); 423 if (!stillRunning()) { break; } 424 nextSleepDurationMs = POLLING_INTERVAL_MS; 425 426 if (localState.mNetwork == null) { 427 logAndUpdate("waiting for available network"); 428 continue; 429 } 430 431 if (localState.mLinkProperties == null) { 432 synchronized (mLock) { 433 mState.mLinkProperties = mCM.getLinkProperties(mState.mNetwork); 434 dupStateLocked(); 435 } 436 } 437 438 if (!localState.mPassedInitialIPv6Check) { 439 if (!hasBasicIPv6Connectivity()) { 440 logAndUpdate("waiting for basic IPv6 connectivity"); 441 continue; 442 } 443 synchronized (mLock) { 444 mState.mPassedInitialIPv6Check = true; 445 } 446 } 447 448 if (!localState.validPowerStateForTesting()) { 449 resetConnectivityCheckStatistics(); 450 logAndUpdate("waiting for ACTION_POWER_DISCONNECTED"); 451 continue; 452 } 453 454 if (!localState.validScreenStateForTesting()) { 455 resetConnectivityCheckStatistics(); 456 tryScreenOff(); 457 logAndUpdate("waiting for ACTION_SCREEN_OFF"); 458 continue; 459 } 460 461 if (mWaitForPowerDisconnected) { 462 final long delta = SystemClock.elapsedRealtime() - localState.mPowerDisconnectTime; 463 if (delta < MIN_POWER_DISCONNECT_MS) { 464 nextSleepDurationMs = (int) (MIN_POWER_DISCONNECT_MS - delta); 465 // Not a lot of point in going to sleep for fewer than 500ms. 466 if (nextSleepDurationMs > 500) { 467 Log.d(TAG, "waiting for power to be disconnected for at least " 468 + MIN_POWER_DISCONNECT_MS + "ms, " 469 + nextSleepDurationMs + "ms left."); 470 continue; 471 } 472 } 473 } 474 475 final long delta = SystemClock.elapsedRealtime() - localState.mScreenOffTime; 476 if (delta < MIN_SCREEN_OFF_MS) { 477 nextSleepDurationMs = (int) (MIN_SCREEN_OFF_MS - delta); 478 // Not a lot of point in going to sleep for fewer than 500ms. 479 if (nextSleepDurationMs > 500) { 480 Log.d(TAG, "waiting for screen to be off for at least " 481 + MIN_SCREEN_OFF_MS + "ms, " 482 + nextSleepDurationMs + "ms left."); 483 continue; 484 } 485 } 486 487 numConnectivityChecksPassing += hasGlobalIPv6Connectivity() ? 1 : 0; 488 numConnectivityChecks++; 489 if (numConnectivityChecks >= MAX_CONNECTIVITY_CHECKS) { 490 break; 491 } 492 nextSleepDurationMs = CONNECTIVITY_CHECKING_INTERVAL_MS; 493 } 494 495 if (!stillRunning()) { return; } 496 497 // We require that 100% of IPv6 HTTPS queries succeed. 498 if (numConnectivityChecksPassing == MAX_CONNECTIVITY_CHECKS) { 499 setTestPassing(); 500 } else { 501 logAndUpdate("Test FAILED with score: " 502 + numConnectivityChecksPassing + "/" + MAX_CONNECTIVITY_CHECKS); 503 } 504 } 505 506 private boolean stillRunning() { 507 return isRunning.get(); 508 } 509 510 public void setStopped() { 511 isRunning.set(false); 512 } 513 514 private void maybeWaitForPreviousThread() { 515 BackgroundTestingThread previousThread; 516 synchronized (mLock) { 517 previousThread = mTestingThread; 518 } 519 520 if (previousThread != null) { 521 previousThread.setStopped(); 522 try { 523 previousThread.join(); 524 } catch (InterruptedException ignored) {} 525 } 526 527 synchronized (mLock) { 528 if (mTestingThread == null || mTestingThread == previousThread) { 529 mTestingThread = this; 530 isRunning.set(true); 531 } 532 } 533 } 534 535 private void dupStateLocked() { 536 localState.mNetwork = mState.mNetwork; 537 localState.mLinkProperties = mState.mLinkProperties; 538 localState.mScreenOffTime = mState.mScreenOffTime; 539 localState.mPowerDisconnectTime = mState.mPowerDisconnectTime; 540 localState.mPassedInitialIPv6Check = mState.mPassedInitialIPv6Check; 541 } 542 543 private void awaitNotification(int timeoutMs) { 544 synchronized (mLock) { 545 if (timeoutMs > 0) { 546 try { 547 mLock.wait(timeoutMs); 548 } catch (InterruptedException e) {} 549 } 550 dupStateLocked(); 551 } 552 } 553 554 private void resetConnectivityCheckStatistics() { 555 numConnectivityChecks = 0; 556 numConnectivityChecksPassing = 0; 557 } 558 559 boolean hasBasicIPv6Connectivity() { 560 final HttpResult result = getHttpResource(localState.mNetwork, V6CONN_URL, true); 561 if (result.rcode != 204) { 562 if (result.msg != null && !result.msg.isEmpty()) { 563 logAndUpdate(result.msg); 564 } 565 return false; 566 } 567 return true; 568 } 569 570 boolean hasGlobalIPv6Connectivity() { 571 final boolean doClose = ((numConnectivityChecks % 2) == 0); 572 final HttpResult result = getHttpResource(localState.mNetwork, V6ADDR_URL, doClose); 573 if (result.rcode != 200) { 574 if (result.msg != null && !result.msg.isEmpty()) { 575 logAndUpdate(result.msg); 576 } 577 return false; 578 } 579 580 InetAddress reflectedIp; 581 try { 582 // TODO: replace with Os.inet_pton(). 583 reflectedIp = InetAddress.getByName(result.msg); 584 } catch (UnknownHostException e) { 585 logAndUpdate("Failed to parse '" + result.msg + "' as an IP address"); 586 return false; 587 } 588 if (!(reflectedIp instanceof Inet6Address)) { 589 logAndUpdate(reflectedIp.getHostAddress() + " is not a valid IPv6 address"); 590 return false; 591 } 592 593 for (LinkAddress linkAddr : localState.mLinkProperties.getLinkAddresses()) { 594 if (linkAddr.getAddress().equals(reflectedIp)) { 595 logAndUpdate("Found reflected IP " + linkAddr.getAddress().getHostAddress()); 596 return true; 597 } 598 } 599 600 logAndUpdate("Link IP addresses do not include: " + reflectedIp.getHostAddress()); 601 return false; 602 } 603 } 604 605 private static class HttpResult { 606 public final int rcode; 607 public final String msg; 608 609 public HttpResult(int rcode, String msg) { 610 this.rcode = rcode; 611 this.msg = msg; 612 } 613 } 614 615 private static HttpResult getHttpResource( 616 final Network network, final String url, boolean doClose) { 617 int rcode = -1; 618 String msg = null; 619 620 try { 621 final HttpURLConnection conn = 622 (HttpURLConnection) network.openConnection(new URL(url)); 623 conn.setConnectTimeout(10 * 1000); 624 conn.setReadTimeout(10 * 1000); 625 if (doClose) { conn.setRequestProperty("connection", "close"); } 626 rcode = conn.getResponseCode(); 627 if (rcode >= 200 && rcode <= 299) { 628 msg = new BufferedReader(new InputStreamReader(conn.getInputStream())).readLine(); 629 } 630 if (doClose) { conn.disconnect(); } // try not to have reusable sessions 631 } catch (IOException e) { 632 msg = "HTTP GET of '" + url + "' encountered " + e; 633 } 634 635 return new HttpResult(rcode, msg); 636 } 637 638 private boolean isLeanback() { 639 final PackageManager pm = this.getPackageManager(); 640 return (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)); 641 } 642 } 643