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