Home | History | Annotate | Download | only in net
      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