• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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