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 android.os.cts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assert.fail; 24 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.ServiceConnection; 29 import android.content.pm.PackageManager; 30 import android.net.TrafficStats; 31 import android.net.Uri; 32 import android.os.IBinder; 33 import android.os.RemoteException; 34 import android.os.StrictMode; 35 import android.os.StrictMode.ThreadPolicy.Builder; 36 import android.os.StrictMode.ViolationInfo; 37 import android.os.strictmode.CustomViolation; 38 import android.os.strictmode.FileUriExposedViolation; 39 import android.os.strictmode.UntaggedSocketViolation; 40 import android.os.strictmode.Violation; 41 import android.support.test.InstrumentationRegistry; 42 import android.support.test.runner.AndroidJUnit4; 43 import android.system.Os; 44 import android.system.OsConstants; 45 import android.util.Log; 46 47 import org.junit.After; 48 import org.junit.Before; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 52 import java.io.File; 53 import java.io.FileDescriptor; 54 import java.io.FileInputStream; 55 import java.io.FileNotFoundException; 56 import java.io.FileOutputStream; 57 import java.io.IOException; 58 import java.net.HttpURLConnection; 59 import java.net.Socket; 60 import java.net.URL; 61 import java.util.ArrayList; 62 import java.util.List; 63 import java.util.concurrent.ArrayBlockingQueue; 64 import java.util.concurrent.BlockingQueue; 65 import java.util.concurrent.CountDownLatch; 66 import java.util.concurrent.ExecutionException; 67 import java.util.concurrent.LinkedBlockingQueue; 68 import java.util.concurrent.TimeUnit; 69 import java.util.function.Consumer; 70 71 /** Tests for {@link StrictMode} */ 72 @RunWith(AndroidJUnit4.class) 73 public class StrictModeTest { 74 private static final String TAG = "StrictModeTest"; 75 private static final String REMOTE_SERVICE_ACTION = "android.app.REMOTESERVICE"; 76 77 private StrictMode.ThreadPolicy mThreadPolicy; 78 private StrictMode.VmPolicy mVmPolicy; 79 getContext()80 private Context getContext() { 81 return InstrumentationRegistry.getContext(); 82 } 83 84 @Before setUp()85 public void setUp() { 86 mThreadPolicy = StrictMode.getThreadPolicy(); 87 mVmPolicy = StrictMode.getVmPolicy(); 88 } 89 90 @After tearDown()91 public void tearDown() { 92 StrictMode.setThreadPolicy(mThreadPolicy); 93 StrictMode.setVmPolicy(mVmPolicy); 94 } 95 96 public interface ThrowingRunnable { run()97 void run() throws Exception; 98 } 99 100 @Test testThreadBuilder()101 public void testThreadBuilder() throws Exception { 102 StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().detectDiskReads().penaltyLog().build(); 103 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(policy).build()); 104 105 final File test = File.createTempFile("foo", "bar"); 106 inspectViolation( 107 test::exists, 108 violation -> { 109 assertThat(violation.getViolationDetails()).isNull(); 110 assertThat(violation.getStackTrace()).contains("DiskReadViolation"); 111 }); 112 } 113 114 @Test testUnclosedCloseable()115 public void testUnclosedCloseable() throws Exception { 116 StrictMode.setVmPolicy( 117 new StrictMode.VmPolicy.Builder().detectLeakedClosableObjects().build()); 118 119 inspectViolation( 120 () -> leakCloseable("leaked.txt"), 121 info -> { 122 assertThat(info.getViolationDetails()) 123 .isEqualTo( 124 "A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks."); 125 assertThat(info.getStackTrace()) 126 .contains("Explicit termination method 'close' not called"); 127 assertThat(info.getStackTrace()).contains("leakCloseable"); 128 assertPolicy(info, StrictMode.DETECT_VM_CLOSABLE_LEAKS); 129 }); 130 } 131 leakCloseable(String fileName)132 private void leakCloseable(String fileName) throws InterruptedException { 133 final CountDownLatch finalizedSignal = new CountDownLatch(1); 134 try { 135 new FileOutputStream(new File(getContext().getFilesDir(), fileName)) { 136 @Override 137 protected void finalize() throws IOException { 138 super.finalize(); 139 finalizedSignal.countDown(); 140 } 141 }; 142 } catch (FileNotFoundException e) { 143 throw new RuntimeException(e); 144 } 145 Runtime.getRuntime().gc(); 146 Runtime.getRuntime().runFinalization(); 147 // Sometimes it needs extra prodding. 148 if (!finalizedSignal.await(5, TimeUnit.SECONDS)) { 149 Runtime.getRuntime().gc(); 150 Runtime.getRuntime().runFinalization(); 151 } 152 } 153 154 @Test testClassInstanceLimit()155 public void testClassInstanceLimit() throws Exception { 156 StrictMode.setVmPolicy( 157 new StrictMode.VmPolicy.Builder() 158 .setClassInstanceLimit(LimitedClass.class, 1) 159 .build()); 160 List<LimitedClass> references = new ArrayList<>(); 161 assertNoViolation(() -> references.add(new LimitedClass())); 162 references.add(new LimitedClass()); 163 inspectViolation( 164 StrictMode::conditionallyCheckInstanceCounts, 165 info -> assertPolicy(info, StrictMode.DETECT_VM_INSTANCE_LEAKS)); 166 } 167 168 private static final class LimitedClass {} 169 170 /** Insecure connection should be detected */ 171 @Test testCleartextNetwork()172 public void testCleartextNetwork() throws Exception { 173 if (!hasInternetConnection()) { 174 Log.i(TAG, "testCleartextNetwork() ignored on device without Internet"); 175 return; 176 } 177 178 StrictMode.setVmPolicy( 179 new StrictMode.VmPolicy.Builder().detectCleartextNetwork().penaltyLog().build()); 180 181 inspectViolation( 182 () -> 183 ((HttpURLConnection) new URL("http://example.com/").openConnection()) 184 .getResponseCode(), 185 info -> { 186 assertThat(info.getViolationDetails()) 187 .contains("Detected cleartext network traffic from UID"); 188 assertThat(info.getViolationDetails()) 189 .startsWith(StrictMode.CLEARTEXT_DETECTED_MSG); 190 assertPolicy(info, StrictMode.DETECT_VM_CLEARTEXT_NETWORK); 191 }); 192 } 193 194 /** Secure connection should be ignored */ 195 @Test testEncryptedNetwork()196 public void testEncryptedNetwork() throws Exception { 197 if (!hasInternetConnection()) { 198 Log.i(TAG, "testEncryptedNetwork() ignored on device without Internet"); 199 return; 200 } 201 202 StrictMode.setVmPolicy( 203 new StrictMode.VmPolicy.Builder().detectCleartextNetwork().penaltyLog().build()); 204 205 assertNoViolation( 206 () -> 207 ((HttpURLConnection) new URL("https://example.com/").openConnection()) 208 .getResponseCode()); 209 } 210 211 @Test testFileUriExposure()212 public void testFileUriExposure() throws Exception { 213 StrictMode.setVmPolicy( 214 new StrictMode.VmPolicy.Builder().detectFileUriExposure().penaltyLog().build()); 215 216 final Uri badUri = Uri.fromFile(new File("/sdcard/meow.jpg")); 217 inspectViolation( 218 () -> { 219 Intent intent = new Intent(Intent.ACTION_VIEW); 220 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 221 intent.setDataAndType(badUri, "image/jpeg"); 222 getContext().startActivity(intent); 223 }, 224 violation -> { 225 assertThat(violation.getStackTrace()).contains(badUri + " exposed beyond app"); 226 }); 227 228 final Uri goodUri = Uri.parse("content://com.example/foobar"); 229 assertNoViolation( 230 () -> { 231 Intent intent = new Intent(Intent.ACTION_VIEW); 232 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 233 intent.setDataAndType(goodUri, "image/jpeg"); 234 getContext().startActivity(intent); 235 }); 236 } 237 238 @Test testContentUriWithoutPermission()239 public void testContentUriWithoutPermission() throws Exception { 240 StrictMode.setVmPolicy( 241 new StrictMode.VmPolicy.Builder() 242 .detectContentUriWithoutPermission() 243 .penaltyLog() 244 .build()); 245 246 final Uri uri = Uri.parse("content://com.example/foobar"); 247 inspectViolation( 248 () -> { 249 Intent intent = new Intent(Intent.ACTION_VIEW); 250 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 251 intent.setDataAndType(uri, "image/jpeg"); 252 getContext().startActivity(intent); 253 }, 254 violation -> 255 assertThat(violation.getStackTrace()) 256 .contains(uri + " exposed beyond app")); 257 258 assertNoViolation( 259 () -> { 260 Intent intent = new Intent(Intent.ACTION_VIEW); 261 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 262 intent.setDataAndType(uri, "image/jpeg"); 263 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 264 getContext().startActivity(intent); 265 }); 266 } 267 268 @Test testUntaggedSocketsHttp()269 public void testUntaggedSocketsHttp() throws Exception { 270 if (!hasInternetConnection()) { 271 Log.i(TAG, "testUntaggedSockets() ignored on device without Internet"); 272 return; 273 } 274 275 StrictMode.setVmPolicy( 276 new StrictMode.VmPolicy.Builder().detectUntaggedSockets().penaltyLog().build()); 277 278 inspectViolation( 279 () -> 280 ((HttpURLConnection) new URL("http://example.com/").openConnection()) 281 .getResponseCode(), 282 violation -> 283 assertThat(violation.getStackTrace()) 284 .contains(UntaggedSocketViolation.MESSAGE)); 285 286 assertNoViolation( 287 () -> { 288 TrafficStats.setThreadStatsTag(0xDECAFBAD); 289 try { 290 ((HttpURLConnection) new URL("http://example.com/").openConnection()) 291 .getResponseCode(); 292 } finally { 293 TrafficStats.clearThreadStatsTag(); 294 } 295 }); 296 } 297 298 @Test testUntaggedSocketsRaw()299 public void testUntaggedSocketsRaw() throws Exception { 300 if (!hasInternetConnection()) { 301 Log.i(TAG, "testUntaggedSockets() ignored on device without Internet"); 302 return; 303 } 304 305 StrictMode.setVmPolicy( 306 new StrictMode.VmPolicy.Builder().detectUntaggedSockets().penaltyLog().build()); 307 308 assertNoViolation( 309 () -> { 310 TrafficStats.setThreadStatsTag(0xDECAFBAD); 311 try (Socket socket = new Socket("example.com", 80)) { 312 socket.getOutputStream().close(); 313 } finally { 314 TrafficStats.clearThreadStatsTag(); 315 } 316 }); 317 318 inspectViolation( 319 () -> { 320 try (Socket socket = new Socket("example.com", 80)) { 321 socket.getOutputStream().close(); 322 } 323 }, 324 violation -> 325 assertThat(violation.getStackTrace()) 326 .contains(UntaggedSocketViolation.MESSAGE)); 327 } 328 329 private static final int PERMISSION_USER_ONLY = 0600; 330 331 @Test testRead()332 public void testRead() throws Exception { 333 final File test = File.createTempFile("foo", "bar"); 334 final File dir = test.getParentFile(); 335 336 final FileInputStream is = new FileInputStream(test); 337 final FileDescriptor fd = 338 Os.open(test.getAbsolutePath(), OsConstants.O_RDONLY, PERMISSION_USER_ONLY); 339 340 StrictMode.setThreadPolicy( 341 new StrictMode.ThreadPolicy.Builder().detectDiskReads().penaltyLog().build()); 342 inspectViolation( 343 test::exists, 344 violation -> { 345 assertThat(violation.getViolationDetails()).isNull(); 346 assertThat(violation.getStackTrace()).contains("DiskReadViolation"); 347 }); 348 349 Consumer<ViolationInfo> assertDiskReadPolicy = 350 violation -> assertPolicy(violation, StrictMode.DETECT_DISK_READ); 351 inspectViolation(test::exists, assertDiskReadPolicy); 352 inspectViolation(test::length, assertDiskReadPolicy); 353 inspectViolation(dir::list, assertDiskReadPolicy); 354 inspectViolation(is::read, assertDiskReadPolicy); 355 356 inspectViolation(() -> new FileInputStream(test), assertDiskReadPolicy); 357 inspectViolation( 358 () -> Os.open(test.getAbsolutePath(), OsConstants.O_RDONLY, PERMISSION_USER_ONLY), 359 assertDiskReadPolicy); 360 inspectViolation(() -> Os.read(fd, new byte[10], 0, 1), assertDiskReadPolicy); 361 } 362 363 @Test testWrite()364 public void testWrite() throws Exception { 365 File file = File.createTempFile("foo", "bar"); 366 367 final FileOutputStream os = new FileOutputStream(file); 368 final FileDescriptor fd = 369 Os.open(file.getAbsolutePath(), OsConstants.O_RDWR, PERMISSION_USER_ONLY); 370 371 StrictMode.setThreadPolicy( 372 new StrictMode.ThreadPolicy.Builder().detectDiskWrites().penaltyLog().build()); 373 374 inspectViolation( 375 file::createNewFile, 376 violation -> { 377 assertThat(violation.getViolationDetails()).isNull(); 378 assertThat(violation.getStackTrace()).contains("DiskWriteViolation"); 379 }); 380 381 Consumer<ViolationInfo> assertDiskWritePolicy = 382 violation -> assertPolicy(violation, StrictMode.DETECT_DISK_WRITE); 383 384 inspectViolation(() -> File.createTempFile("foo", "bar"), assertDiskWritePolicy); 385 inspectViolation(() -> new FileOutputStream(file), assertDiskWritePolicy); 386 inspectViolation(file::delete, assertDiskWritePolicy); 387 inspectViolation(file::createNewFile, assertDiskWritePolicy); 388 inspectViolation(() -> os.write(32), assertDiskWritePolicy); 389 390 inspectViolation( 391 () -> Os.open(file.getAbsolutePath(), OsConstants.O_RDWR, PERMISSION_USER_ONLY), 392 assertDiskWritePolicy); 393 inspectViolation(() -> Os.write(fd, new byte[10], 0, 1), assertDiskWritePolicy); 394 inspectViolation(() -> Os.fsync(fd), assertDiskWritePolicy); 395 inspectViolation( 396 () -> file.renameTo(new File(file.getParent(), "foobar")), assertDiskWritePolicy); 397 } 398 399 @Test testNetwork()400 public void testNetwork() throws Exception { 401 if (!hasInternetConnection()) { 402 Log.i(TAG, "testUntaggedSockets() ignored on device without Internet"); 403 return; 404 } 405 406 StrictMode.setThreadPolicy( 407 new StrictMode.ThreadPolicy.Builder().detectNetwork().penaltyLog().build()); 408 409 inspectViolation( 410 () -> { 411 try (Socket socket = new Socket("example.com", 80)) { 412 socket.getOutputStream().close(); 413 } 414 }, 415 violation -> assertPolicy(violation, StrictMode.DETECT_NETWORK)); 416 inspectViolation( 417 () -> 418 ((HttpURLConnection) new URL("http://example.com/").openConnection()) 419 .getResponseCode(), 420 violation -> assertPolicy(violation, StrictMode.DETECT_NETWORK)); 421 } 422 423 @Test testViolationAcrossBinder()424 public void testViolationAcrossBinder() throws Exception { 425 runWithRemoteServiceBound( 426 getContext(), 427 service -> { 428 StrictMode.setThreadPolicy( 429 new Builder().detectDiskWrites().penaltyLog().build()); 430 431 try { 432 inspectViolation( 433 () -> service.performDiskWrite(), 434 (violation) -> { 435 assertPolicy(violation, StrictMode.DETECT_DISK_WRITE); 436 assertThat(violation.getViolationDetails()) 437 .isNull(); // Disk write has no message. 438 assertThat(violation.getStackTrace()) 439 .contains("DiskWriteViolation"); 440 assertThat(violation.getStackTrace()) 441 .contains( 442 "at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk"); 443 assertThat(violation.getStackTrace()) 444 .contains("# via Binder call with stack:"); 445 assertThat(violation.getStackTrace()) 446 .contains( 447 "at android.os.cts.ISecondary$Stub$Proxy.performDiskWrite"); 448 }); 449 assertNoViolation(() -> service.getPid()); 450 } catch (Exception e) { 451 throw new RuntimeException(e); 452 } 453 }); 454 } 455 checkNonSdkApiUsageViolation(boolean blacklist, String className, String methodName, Class<?>... paramTypes)456 private void checkNonSdkApiUsageViolation(boolean blacklist, String className, 457 String methodName, Class<?>... paramTypes) throws Exception { 458 Class<?> clazz = Class.forName(className); 459 inspectViolation( 460 () -> { 461 try { 462 java.lang.reflect.Method m = clazz.getDeclaredMethod(methodName, paramTypes); 463 if (blacklist) { 464 fail(); 465 } 466 } catch (NoSuchMethodException expected) { 467 if (!blacklist) { 468 fail(); 469 } 470 } 471 }, 472 violation -> { 473 assertThat(violation).isNotNull(); 474 assertPolicy(violation, StrictMode.DETECT_VM_NON_SDK_API_USAGE); 475 assertThat(violation.getViolationDetails()).contains(methodName); 476 assertThat(violation.getStackTrace()).contains("checkNonSdkApiUsageViolation"); 477 } 478 ); 479 } 480 481 @Test testNonSdkApiUsage()482 public void testNonSdkApiUsage() throws Exception { 483 StrictMode.VmPolicy oldVmPolicy = StrictMode.getVmPolicy(); 484 StrictMode.ThreadPolicy oldThreadPolicy = StrictMode.getThreadPolicy(); 485 try { 486 StrictMode.setVmPolicy( 487 new StrictMode.VmPolicy.Builder().detectNonSdkApiUsage().build()); 488 checkNonSdkApiUsageViolation( 489 true, "dalvik.system.VMRuntime", "setHiddenApiExemptions", String[].class); 490 // verify that mutliple uses of a light greylist API are detected. 491 checkNonSdkApiUsageViolation(false, "dalvik.system.VMRuntime", "getRuntime"); 492 checkNonSdkApiUsageViolation(false, "dalvik.system.VMRuntime", "getRuntime"); 493 494 // Verify that the VM policy is turned off after a call to permitNonSdkApiUsage. 495 StrictMode.setVmPolicy( 496 new StrictMode.VmPolicy.Builder().permitNonSdkApiUsage().build()); 497 assertNoViolation(() -> { 498 Class<?> clazz = Class.forName("dalvik.system.VMRuntime"); 499 try { 500 clazz.getDeclaredMethod("getRuntime"); 501 } catch (NoSuchMethodException maybe) { 502 } 503 }); 504 } finally { 505 StrictMode.setVmPolicy(oldVmPolicy); 506 StrictMode.setThreadPolicy(oldThreadPolicy); 507 } 508 } 509 510 @Test testThreadPenaltyListener()511 public void testThreadPenaltyListener() throws Exception { 512 final BlockingQueue<Violation> violations = new ArrayBlockingQueue<>(1); 513 StrictMode.setThreadPolicy( 514 new StrictMode.ThreadPolicy.Builder().detectCustomSlowCalls() 515 .penaltyListener(getContext().getMainExecutor(), (v) -> { 516 violations.add(v); 517 }).build()); 518 519 StrictMode.noteSlowCall("foo"); 520 521 final Violation v = violations.poll(5, TimeUnit.SECONDS); 522 assertTrue(v instanceof CustomViolation); 523 } 524 525 @Test testVmPenaltyListener()526 public void testVmPenaltyListener() throws Exception { 527 final BlockingQueue<Violation> violations = new ArrayBlockingQueue<>(1); 528 StrictMode.setVmPolicy( 529 new StrictMode.VmPolicy.Builder().detectFileUriExposure() 530 .penaltyListener(getContext().getMainExecutor(), (v) -> { 531 violations.add(v); 532 }).build()); 533 534 Intent intent = new Intent(Intent.ACTION_VIEW); 535 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 536 intent.setDataAndType(Uri.fromFile(new File("/sdcard/meow.jpg")), "image/jpeg"); 537 getContext().startActivity(intent); 538 539 final Violation v = violations.poll(5, TimeUnit.SECONDS); 540 assertTrue(v instanceof FileUriExposedViolation); 541 } 542 runWithRemoteServiceBound(Context context, Consumer<ISecondary> consumer)543 private static void runWithRemoteServiceBound(Context context, Consumer<ISecondary> consumer) 544 throws ExecutionException, InterruptedException, RemoteException { 545 BlockingQueue<IBinder> binderHolder = new ArrayBlockingQueue<>(1); 546 ServiceConnection secondaryConnection = 547 new ServiceConnection() { 548 public void onServiceConnected(ComponentName className, IBinder service) { 549 binderHolder.add(service); 550 } 551 552 public void onServiceDisconnected(ComponentName className) { 553 binderHolder.drainTo(new ArrayList<>()); 554 } 555 }; 556 Intent intent = new Intent(REMOTE_SERVICE_ACTION); 557 intent.setPackage(context.getPackageName()); 558 559 Intent secondaryIntent = new Intent(ISecondary.class.getName()); 560 secondaryIntent.setPackage(context.getPackageName()); 561 assertThat( 562 context.bindService( 563 secondaryIntent, secondaryConnection, Context.BIND_AUTO_CREATE)) 564 .isTrue(); 565 IBinder binder = binderHolder.take(); 566 assertThat(binder.queryLocalInterface(binder.getInterfaceDescriptor())).isNull(); 567 consumer.accept(ISecondary.Stub.asInterface(binder)); 568 context.unbindService(secondaryConnection); 569 context.stopService(intent); 570 } 571 assertViolation(String expected, ThrowingRunnable r)572 private static void assertViolation(String expected, ThrowingRunnable r) throws Exception { 573 inspectViolation(r, violation -> assertThat(violation.getStackTrace()).contains(expected)); 574 } 575 assertNoViolation(ThrowingRunnable r)576 private static void assertNoViolation(ThrowingRunnable r) throws Exception { 577 inspectViolation( 578 r, violation -> assertWithMessage("Unexpected violation").that(violation).isNull()); 579 } 580 assertPolicy(ViolationInfo info, int policy)581 private void assertPolicy(ViolationInfo info, int policy) { 582 assertWithMessage("Policy bit incorrect").that(info.getViolationBit()).isEqualTo(policy); 583 } 584 inspectViolation( ThrowingRunnable violating, Consumer<ViolationInfo> consume)585 private static void inspectViolation( 586 ThrowingRunnable violating, Consumer<ViolationInfo> consume) throws Exception { 587 final LinkedBlockingQueue<ViolationInfo> violations = new LinkedBlockingQueue<>(); 588 StrictMode.setViolationLogger(violations::add); 589 590 try { 591 violating.run(); 592 consume.accept(violations.poll(5, TimeUnit.SECONDS)); 593 } finally { 594 StrictMode.setViolationLogger(null); 595 } 596 } 597 hasInternetConnection()598 private boolean hasInternetConnection() { 599 final PackageManager pm = getContext().getPackageManager(); 600 return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) 601 || pm.hasSystemFeature(PackageManager.FEATURE_WIFI) 602 || pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET); 603 } 604 } 605