1 /* 2 * Copyright (C) 2011 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.nfc; 18 19 import android.Manifest; 20 import android.app.ActivityManager; 21 import android.bluetooth.BluetoothAdapter; 22 import android.os.UserManager; 23 24 import com.android.nfc.RegisteredComponentCache.ComponentInfo; 25 import com.android.nfc.handover.HandoverDataParser; 26 import com.android.nfc.handover.PeripheralHandoverService; 27 28 import android.app.Activity; 29 import android.app.ActivityManager; 30 import android.app.AlertDialog; 31 import android.app.IActivityManager; 32 import android.app.PendingIntent; 33 import android.app.PendingIntent.CanceledException; 34 import android.content.BroadcastReceiver; 35 import android.content.ComponentName; 36 import android.content.ContentResolver; 37 import android.content.Context; 38 import android.content.DialogInterface; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.content.pm.PackageManager; 42 import android.content.pm.PackageManager.NameNotFoundException; 43 import android.content.pm.ResolveInfo; 44 import android.content.res.Resources.NotFoundException; 45 import android.net.Uri; 46 import android.nfc.NdefMessage; 47 import android.nfc.NdefRecord; 48 import android.nfc.NfcAdapter; 49 import android.nfc.Tag; 50 import android.nfc.tech.Ndef; 51 import android.nfc.tech.NfcBarcode; 52 import android.os.Handler; 53 import android.os.Message; 54 import android.os.Messenger; 55 import android.os.RemoteException; 56 import android.os.UserHandle; 57 import android.util.Log; 58 import android.view.LayoutInflater; 59 import android.view.View; 60 import android.view.WindowManager; 61 import android.widget.TextView; 62 63 import java.io.FileDescriptor; 64 import java.io.PrintWriter; 65 import java.nio.charset.StandardCharsets; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.concurrent.atomic.AtomicBoolean; 69 import java.util.LinkedList; 70 import java.util.List; 71 import java.util.Locale; 72 73 /** 74 * Dispatch of NFC events to start activities 75 */ 76 class NfcDispatcher { 77 private static final boolean DBG = false; 78 private static final String TAG = "NfcDispatcher"; 79 80 static final int DISPATCH_SUCCESS = 1; 81 static final int DISPATCH_FAIL = 2; 82 static final int DISPATCH_UNLOCK = 3; 83 84 private final Context mContext; 85 private final IActivityManager mIActivityManager; 86 private final RegisteredComponentCache mTechListFilters; 87 private final ContentResolver mContentResolver; 88 private final HandoverDataParser mHandoverDataParser; 89 private final String[] mProvisioningMimes; 90 private final String[] mLiveCaseMimes; 91 private final ScreenStateHelper mScreenStateHelper; 92 private final NfcUnlockManager mNfcUnlockManager; 93 private final boolean mDeviceSupportsBluetooth; 94 private final Handler mMessageHandler = new MessageHandler(); 95 private final Messenger mMessenger = new Messenger(mMessageHandler); 96 private AtomicBoolean mBluetoothEnabledByNfc = new AtomicBoolean(); 97 98 // Locked on this 99 private PendingIntent mOverrideIntent; 100 private IntentFilter[] mOverrideFilters; 101 private String[][] mOverrideTechLists; 102 private boolean mProvisioningOnly; 103 104 NfcDispatcher(Context context, 105 HandoverDataParser handoverDataParser, 106 boolean provisionOnly, 107 boolean isLiveCaseEnabled) { 108 mContext = context; 109 mIActivityManager = ActivityManager.getService(); 110 mTechListFilters = new RegisteredComponentCache(mContext, 111 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); 112 mContentResolver = context.getContentResolver(); 113 mHandoverDataParser = handoverDataParser; 114 mScreenStateHelper = new ScreenStateHelper(context); 115 mNfcUnlockManager = NfcUnlockManager.getInstance(); 116 mDeviceSupportsBluetooth = BluetoothAdapter.getDefaultAdapter() != null; 117 118 synchronized (this) { 119 mProvisioningOnly = provisionOnly; 120 } 121 String[] provisionMimes = null; 122 if (provisionOnly) { 123 try { 124 // Get accepted mime-types 125 provisionMimes = context.getResources(). 126 getStringArray(R.array.provisioning_mime_types); 127 } catch (NotFoundException e) { 128 provisionMimes = null; 129 } 130 } 131 mProvisioningMimes = provisionMimes; 132 133 String[] liveCaseMimes = null; 134 if (isLiveCaseEnabled) { 135 try { 136 // Get accepted mime-types 137 liveCaseMimes = context.getResources(). 138 getStringArray(R.array.live_case_mime_types); 139 } catch (NotFoundException e) { 140 liveCaseMimes = null; 141 } 142 } 143 mLiveCaseMimes = liveCaseMimes; 144 145 IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 146 mContext.registerReceiver(mBluetoothStatusReceiver, filter); 147 } 148 149 @Override 150 protected void finalize() throws Throwable { 151 mContext.unregisterReceiver(mBluetoothStatusReceiver); 152 super.finalize(); 153 } 154 155 public synchronized void setForegroundDispatch(PendingIntent intent, 156 IntentFilter[] filters, String[][] techLists) { 157 if (DBG) Log.d(TAG, "Set Foreground Dispatch"); 158 mOverrideIntent = intent; 159 mOverrideFilters = filters; 160 mOverrideTechLists = techLists; 161 } 162 163 public synchronized void disableProvisioningMode() { 164 mProvisioningOnly = false; 165 } 166 167 /** 168 * Helper for re-used objects and methods during a single tag dispatch. 169 */ 170 static class DispatchInfo { 171 public final Intent intent; 172 173 final Intent rootIntent; 174 final Uri ndefUri; 175 final String ndefMimeType; 176 final PackageManager packageManager; 177 final Context context; 178 179 public DispatchInfo(Context context, Tag tag, NdefMessage message) { 180 intent = new Intent(); 181 intent.putExtra(NfcAdapter.EXTRA_TAG, tag); 182 intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId()); 183 if (message != null) { 184 intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message}); 185 ndefUri = message.getRecords()[0].toUri(); 186 ndefMimeType = message.getRecords()[0].toMimeType(); 187 } else { 188 ndefUri = null; 189 ndefMimeType = null; 190 } 191 192 rootIntent = new Intent(context, NfcRootActivity.class); 193 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent); 194 rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 195 196 this.context = context; 197 packageManager = context.getPackageManager(); 198 } 199 200 public Intent setNdefIntent() { 201 intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED); 202 if (ndefUri != null) { 203 intent.setData(ndefUri); 204 return intent; 205 } else if (ndefMimeType != null) { 206 intent.setType(ndefMimeType); 207 return intent; 208 } 209 return null; 210 } 211 212 public Intent setTechIntent() { 213 intent.setData(null); 214 intent.setType(null); 215 intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED); 216 return intent; 217 } 218 219 public Intent setTagIntent() { 220 intent.setData(null); 221 intent.setType(null); 222 intent.setAction(NfcAdapter.ACTION_TAG_DISCOVERED); 223 return intent; 224 } 225 226 public boolean isWebIntent() { 227 return ndefUri != null && ndefUri.normalizeScheme().getScheme() != null && 228 ndefUri.normalizeScheme().getScheme().startsWith("http"); 229 } 230 231 public String getUri() { 232 return ndefUri.toString(); 233 } 234 235 /** 236 * Launch the activity via a (single) NFC root task, so that it 237 * creates a new task stack instead of interfering with any existing 238 * task stack for that activity. 239 * NfcRootActivity acts as the task root, it immediately calls 240 * start activity on the intent it is passed. 241 */ 242 boolean tryStartActivity() { 243 // Ideally we'd have used startActivityForResult() to determine whether the 244 // NfcRootActivity was able to launch the intent, but startActivityForResult() 245 // is not available on Context. Instead, we query the PackageManager beforehand 246 // to determine if there is an Activity to handle this intent, and base the 247 // result of off that. 248 List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(intent, 0, 249 ActivityManager.getCurrentUser()); 250 if (activities.size() > 0) { 251 context.startActivityAsUser(rootIntent, UserHandle.CURRENT); 252 return true; 253 } 254 return false; 255 } 256 257 boolean tryStartActivity(Intent intentToStart) { 258 List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser( 259 intentToStart, 0, ActivityManager.getCurrentUser()); 260 if (activities.size() > 0) { 261 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intentToStart); 262 context.startActivityAsUser(rootIntent, UserHandle.CURRENT); 263 return true; 264 } 265 return false; 266 } 267 } 268 269 /** Returns: 270 * <ul> 271 * <li /> DISPATCH_SUCCESS if dispatched to an activity, 272 * <li /> DISPATCH_FAIL if no activities were found to dispatch to, 273 * <li /> DISPATCH_UNLOCK if the tag was used to unlock the device 274 * </ul> 275 */ 276 public int dispatchTag(Tag tag) { 277 PendingIntent overrideIntent; 278 IntentFilter[] overrideFilters; 279 String[][] overrideTechLists; 280 String[] provisioningMimes; 281 String[] liveCaseMimes; 282 NdefMessage message = null; 283 boolean provisioningOnly; 284 285 synchronized (this) { 286 overrideFilters = mOverrideFilters; 287 overrideIntent = mOverrideIntent; 288 overrideTechLists = mOverrideTechLists; 289 provisioningOnly = mProvisioningOnly; 290 provisioningMimes = mProvisioningMimes; 291 liveCaseMimes = mLiveCaseMimes; 292 } 293 294 boolean screenUnlocked = false; 295 boolean liveCaseDetected = false; 296 Ndef ndef = Ndef.get(tag); 297 if (!provisioningOnly && 298 mScreenStateHelper.checkScreenState() == ScreenStateHelper.SCREEN_STATE_ON_LOCKED) { 299 screenUnlocked = handleNfcUnlock(tag); 300 301 if (ndef != null) { 302 message = ndef.getCachedNdefMessage(); 303 if (message != null) { 304 String ndefMimeType = message.getRecords()[0].toMimeType(); 305 if (liveCaseMimes != null && 306 Arrays.asList(liveCaseMimes).contains(ndefMimeType)) { 307 liveCaseDetected = true; 308 } 309 } 310 } 311 312 if (!screenUnlocked && !liveCaseDetected) 313 return DISPATCH_FAIL; 314 } 315 316 if (ndef != null) { 317 message = ndef.getCachedNdefMessage(); 318 } else { 319 NfcBarcode nfcBarcode = NfcBarcode.get(tag); 320 if (nfcBarcode != null && nfcBarcode.getType() == NfcBarcode.TYPE_KOVIO) { 321 message = decodeNfcBarcodeUri(nfcBarcode); 322 } 323 } 324 325 if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message); 326 327 DispatchInfo dispatch = new DispatchInfo(mContext, tag, message); 328 329 resumeAppSwitches(); 330 331 if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters, 332 overrideTechLists)) { 333 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 334 } 335 336 if (tryPeripheralHandover(message)) { 337 if (DBG) Log.i(TAG, "matched BT HANDOVER"); 338 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 339 } 340 341 if (NfcWifiProtectedSetup.tryNfcWifiSetup(ndef, mContext)) { 342 if (DBG) Log.i(TAG, "matched NFC WPS TOKEN"); 343 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 344 } 345 346 if (provisioningOnly) { 347 if (message == null) { 348 // We only allow NDEF-message dispatch in provisioning mode 349 return DISPATCH_FAIL; 350 } 351 // Restrict to mime-types in whitelist. 352 String ndefMimeType = message.getRecords()[0].toMimeType(); 353 if (provisioningMimes == null || 354 !(Arrays.asList(provisioningMimes).contains(ndefMimeType))) { 355 Log.e(TAG, "Dropping NFC intent in provisioning mode."); 356 return DISPATCH_FAIL; 357 } 358 } 359 360 if (tryNdef(dispatch, message)) { 361 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 362 } 363 364 if (screenUnlocked) { 365 // We only allow NDEF-based mimeType matching in case of an unlock 366 return DISPATCH_UNLOCK; 367 } 368 369 // Only allow NDEF-based mimeType matching for unlock tags 370 if (tryTech(dispatch, tag)) { 371 return DISPATCH_SUCCESS; 372 } 373 374 dispatch.setTagIntent(); 375 if (dispatch.tryStartActivity()) { 376 if (DBG) Log.i(TAG, "matched TAG"); 377 return DISPATCH_SUCCESS; 378 } 379 380 if (DBG) Log.i(TAG, "no match"); 381 return DISPATCH_FAIL; 382 } 383 384 private boolean handleNfcUnlock(Tag tag) { 385 return mNfcUnlockManager.tryUnlock(tag); 386 } 387 388 /** 389 * Checks for the presence of a URL stored in a tag with tech NfcBarcode. 390 * If found, decodes URL and returns NdefMessage message containing an 391 * NdefRecord containing the decoded URL. If not found, returns null. 392 * 393 * URLs are decoded as follows: 394 * 395 * Ignore first byte (which is 0x80 ORd with a manufacturer ID, corresponding 396 * to ISO/IEC 7816-6). 397 * The second byte describes the payload data format. There are four defined data 398 * format values that identify URL data. Depending on the data format value, the 399 * associated prefix is appended to the URL data: 400 * 401 * 0x01: URL with "http://www." prefix 402 * 0x02: URL with "https://www." prefix 403 * 0x03: URL with "http://" prefix 404 * 0x04: URL with "https://" prefix 405 * 406 * Other data format values do not identify URL data and are not handled by this function. 407 * URL payload is encoded in US-ASCII, following the limitations defined in RFC3987. 408 * see http://www.ietf.org/rfc/rfc3987.txt 409 * 410 * The final two bytes of a tag with tech NfcBarcode are always reserved for CRC data, 411 * and are therefore not part of the payload. They are ignored in the decoding of a URL. 412 * 413 * The default assumption is that the URL occupies the entire payload of the NfcBarcode 414 * ID and all bytes of the NfcBarcode payload are decoded until the CRC (final two bytes) 415 * is reached. However, the OPTIONAL early terminator byte 0xfe can be used to signal 416 * an early end of the URL. Once this function reaches an early terminator byte 0xfe, 417 * URL decoding stops and the NdefMessage is created and returned. Any payload data after 418 * the first early terminator byte is ignored for the purposes of URL decoding. 419 */ 420 private NdefMessage decodeNfcBarcodeUri(NfcBarcode nfcBarcode) { 421 final byte URI_PREFIX_HTTP_WWW = (byte) 0x01; // "http://www." 422 final byte URI_PREFIX_HTTPS_WWW = (byte) 0x02; // "https://www." 423 final byte URI_PREFIX_HTTP = (byte) 0x03; // "http://" 424 final byte URI_PREFIX_HTTPS = (byte) 0x04; // "https://" 425 426 NdefMessage message = null; 427 byte[] tagId = nfcBarcode.getTag().getId(); 428 // All tags of NfcBarcode technology and Kovio type have lengths of a multiple of 16 bytes 429 if (tagId.length >= 4 430 && (tagId[1] == URI_PREFIX_HTTP_WWW || tagId[1] == URI_PREFIX_HTTPS_WWW 431 || tagId[1] == URI_PREFIX_HTTP || tagId[1] == URI_PREFIX_HTTPS)) { 432 // Look for optional URI terminator (0xfe), used to indicate the end of a URI prior to 433 // the end of the full NfcBarcode payload. No terminator means that the URI occupies the 434 // entire length of the payload field. Exclude checking the CRC in the final two bytes 435 // of the NfcBarcode tagId. 436 int end = 2; 437 for (; end < tagId.length - 2; end++) { 438 if (tagId[end] == (byte) 0xfe) { 439 break; 440 } 441 } 442 byte[] payload = new byte[end - 1]; // Skip also first byte (manufacturer ID) 443 System.arraycopy(tagId, 1, payload, 0, payload.length); 444 NdefRecord uriRecord = new NdefRecord( 445 NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, tagId, payload); 446 message = new NdefMessage(uriRecord); 447 } 448 return message; 449 } 450 451 boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent, 452 IntentFilter[] overrideFilters, String[][] overrideTechLists) { 453 if (overrideIntent == null) { 454 return false; 455 } 456 Intent intent; 457 458 // NDEF 459 if (message != null) { 460 intent = dispatch.setNdefIntent(); 461 if (intent != null && 462 isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 463 try { 464 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 465 if (DBG) Log.i(TAG, "matched NDEF override"); 466 return true; 467 } catch (CanceledException e) { 468 return false; 469 } 470 } 471 } 472 473 // TECH 474 intent = dispatch.setTechIntent(); 475 if (isTechMatch(tag, overrideTechLists)) { 476 try { 477 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 478 if (DBG) Log.i(TAG, "matched TECH override"); 479 return true; 480 } catch (CanceledException e) { 481 return false; 482 } 483 } 484 485 // TAG 486 intent = dispatch.setTagIntent(); 487 if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 488 try { 489 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 490 if (DBG) Log.i(TAG, "matched TAG override"); 491 return true; 492 } catch (CanceledException e) { 493 return false; 494 } 495 } 496 return false; 497 } 498 499 boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) { 500 if (filters != null) { 501 for (IntentFilter filter : filters) { 502 if (filter.match(mContentResolver, intent, false, TAG) >= 0) { 503 return true; 504 } 505 } 506 } else if (!hasTechFilter) { 507 return true; // always match if both filters and techlists are null 508 } 509 return false; 510 } 511 512 boolean isTechMatch(Tag tag, String[][] techLists) { 513 if (techLists == null) { 514 return false; 515 } 516 517 String[] tagTechs = tag.getTechList(); 518 Arrays.sort(tagTechs); 519 for (String[] filterTechs : techLists) { 520 if (filterMatch(tagTechs, filterTechs)) { 521 return true; 522 } 523 } 524 return false; 525 } 526 527 boolean tryNdef(DispatchInfo dispatch, NdefMessage message) { 528 if (message == null) { 529 return false; 530 } 531 Intent intent = dispatch.setNdefIntent(); 532 533 // Bail out if the intent does not contain filterable NDEF data 534 if (intent == null) return false; 535 536 // Try to start AAR activity with matching filter 537 List<String> aarPackages = extractAarPackages(message); 538 for (String pkg : aarPackages) { 539 dispatch.intent.setPackage(pkg); 540 if (dispatch.tryStartActivity()) { 541 if (DBG) Log.i(TAG, "matched AAR to NDEF"); 542 return true; 543 } 544 } 545 546 // Try to perform regular launch of the first AAR 547 if (aarPackages.size() > 0) { 548 String firstPackage = aarPackages.get(0); 549 PackageManager pm; 550 try { 551 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); 552 pm = mContext.createPackageContextAsUser("android", 0, 553 currentUser).getPackageManager(); 554 } catch (NameNotFoundException e) { 555 Log.e(TAG, "Could not create user package context"); 556 return false; 557 } 558 Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage); 559 if (appLaunchIntent != null) { 560 ResolveInfo ri = pm.resolveActivity(appLaunchIntent, 0); 561 if (ri != null && ri.activityInfo != null && ri.activityInfo.exported && 562 dispatch.tryStartActivity(appLaunchIntent)) { 563 if (DBG) Log.i(TAG, "matched AAR to application launch"); 564 return true; 565 } 566 } 567 // Find the package in Market: 568 Intent marketIntent = getAppSearchIntent(firstPackage); 569 if (marketIntent != null && dispatch.tryStartActivity(marketIntent)) { 570 if (DBG) Log.i(TAG, "matched AAR to market launch"); 571 return true; 572 } 573 } 574 575 // regular launch 576 dispatch.intent.setPackage(null); 577 578 if (dispatch.isWebIntent()) { 579 if (DBG) Log.i(TAG, "matched Web link - prompting user"); 580 showWebLinkConfirmation(dispatch); 581 return true; 582 } 583 584 try { 585 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); 586 PackageManager pm = mContext.createPackageContextAsUser("android", 0, 587 currentUser).getPackageManager(); 588 ResolveInfo ri = pm.resolveActivity(intent, 0); 589 590 if (ri != null && ri.activityInfo != null && ri.activityInfo.exported && dispatch.tryStartActivity()) { 591 if (DBG) Log.i(TAG, "matched NDEF"); 592 return true; 593 } 594 } catch (NameNotFoundException ignore) { 595 Log.e(TAG, "Could not create user package context"); 596 } 597 598 return false; 599 } 600 601 static List<String> extractAarPackages(NdefMessage message) { 602 List<String> aarPackages = new LinkedList<String>(); 603 for (NdefRecord record : message.getRecords()) { 604 String pkg = checkForAar(record); 605 if (pkg != null) { 606 aarPackages.add(pkg); 607 } 608 } 609 return aarPackages; 610 } 611 612 boolean tryTech(DispatchInfo dispatch, Tag tag) { 613 dispatch.setTechIntent(); 614 615 String[] tagTechs = tag.getTechList(); 616 Arrays.sort(tagTechs); 617 618 // Standard tech dispatch path 619 ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>(); 620 List<ComponentInfo> registered = mTechListFilters.getComponents(); 621 622 PackageManager pm; 623 try { 624 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); 625 pm = mContext.createPackageContextAsUser("android", 0, 626 currentUser).getPackageManager(); 627 } catch (NameNotFoundException e) { 628 Log.e(TAG, "Could not create user package context"); 629 return false; 630 } 631 // Check each registered activity to see if it matches 632 for (ComponentInfo info : registered) { 633 // Don't allow wild card matching 634 if (filterMatch(tagTechs, info.techs) && 635 isComponentEnabled(pm, info.resolveInfo)) { 636 // Add the activity as a match if it's not already in the list 637 // Check if exported flag is not explicitly set to false to prevent 638 // SecurityExceptions. 639 if (!matches.contains(info.resolveInfo) && info.resolveInfo.activityInfo.exported) { 640 matches.add(info.resolveInfo); 641 } 642 } 643 } 644 645 if (matches.size() == 1) { 646 // Single match, launch directly 647 ResolveInfo info = matches.get(0); 648 dispatch.intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); 649 if (dispatch.tryStartActivity()) { 650 if (DBG) Log.i(TAG, "matched single TECH"); 651 return true; 652 } 653 dispatch.intent.setComponent(null); 654 } else if (matches.size() > 1) { 655 // Multiple matches, show a custom activity chooser dialog 656 Intent intent = new Intent(mContext, TechListChooserActivity.class); 657 intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent); 658 intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, 659 matches); 660 if (dispatch.tryStartActivity(intent)) { 661 if (DBG) Log.i(TAG, "matched multiple TECH"); 662 return true; 663 } 664 } 665 return false; 666 } 667 668 public boolean tryPeripheralHandover(NdefMessage m) { 669 if (m == null || !mDeviceSupportsBluetooth) return false; 670 671 if (DBG) Log.d(TAG, "tryHandover(): " + m.toString()); 672 673 HandoverDataParser.BluetoothHandoverData handover = mHandoverDataParser.parseBluetooth(m); 674 if (handover == null || !handover.valid) return false; 675 if (UserManager.get(mContext).hasUserRestriction( 676 UserManager.DISALLOW_CONFIG_BLUETOOTH, 677 // hasUserRestriction does not support UserHandle.CURRENT 678 UserHandle.of(ActivityManager.getCurrentUser()))) { 679 return false; 680 } 681 682 Intent intent = new Intent(mContext, PeripheralHandoverService.class); 683 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_DEVICE, handover.device); 684 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_NAME, handover.name); 685 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_TRANSPORT, handover.transport); 686 if (handover.oobData != null) { 687 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_OOB_DATA, handover.oobData); 688 } 689 if (handover.uuids != null) { 690 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_UUIDS, handover.uuids); 691 } 692 if (handover.btClass != null) { 693 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_CLASS, handover.btClass); 694 } 695 intent.putExtra(PeripheralHandoverService.EXTRA_BT_ENABLED, mBluetoothEnabledByNfc.get()); 696 intent.putExtra(PeripheralHandoverService.EXTRA_CLIENT, mMessenger); 697 mContext.startServiceAsUser(intent, UserHandle.CURRENT); 698 699 return true; 700 } 701 702 703 /** 704 * Tells the ActivityManager to resume allowing app switches. 705 * 706 * If the current app called stopAppSwitches() then our startActivity() can 707 * be delayed for several seconds. This happens with the default home 708 * screen. As a system service we can override this behavior with 709 * resumeAppSwitches(). 710 */ 711 void resumeAppSwitches() { 712 try { 713 mIActivityManager.resumeAppSwitches(); 714 } catch (RemoteException e) { } 715 } 716 717 /** Returns true if the tech list filter matches the techs on the tag */ 718 boolean filterMatch(String[] tagTechs, String[] filterTechs) { 719 if (filterTechs == null || filterTechs.length == 0) return false; 720 721 for (String tech : filterTechs) { 722 if (Arrays.binarySearch(tagTechs, tech) < 0) { 723 return false; 724 } 725 } 726 return true; 727 } 728 729 static String checkForAar(NdefRecord record) { 730 if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE && 731 Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) { 732 return new String(record.getPayload(), StandardCharsets.US_ASCII); 733 } 734 return null; 735 } 736 737 /** 738 * Returns an intent that can be used to find an application not currently 739 * installed on the device. 740 */ 741 static Intent getAppSearchIntent(String pkg) { 742 Intent market = new Intent(Intent.ACTION_VIEW); 743 market.setData(Uri.parse("market://details?id=" + pkg)); 744 return market; 745 } 746 747 static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) { 748 boolean enabled = false; 749 ComponentName compname = new ComponentName( 750 info.activityInfo.packageName, info.activityInfo.name); 751 try { 752 // Note that getActivityInfo() will internally call 753 // isEnabledLP() to determine whether the component 754 // enabled. If it's not, null is returned. 755 if (pm.getActivityInfo(compname,0) != null) { 756 enabled = true; 757 } 758 } catch (PackageManager.NameNotFoundException e) { 759 enabled = false; 760 } 761 if (!enabled) { 762 Log.d(TAG, "Component not enabled: " + compname); 763 } 764 return enabled; 765 } 766 767 void showWebLinkConfirmation(DispatchInfo dispatch) { 768 if (!mContext.getResources().getBoolean(R.bool.enable_nfc_url_open_dialog)) { 769 dispatch.tryStartActivity(); 770 return; 771 } 772 AlertDialog.Builder builder = new AlertDialog.Builder( 773 mContext.getApplicationContext(), 774 android.R.style.Theme_DeviceDefault_Light_Dialog_Alert); 775 builder.setTitle(R.string.title_confirm_url_open); 776 LayoutInflater inflater = LayoutInflater.from(mContext); 777 View view = inflater.inflate(R.layout.url_open_confirmation, null); 778 if (view != null) { 779 TextView url = view.findViewById(R.id.url_open_confirmation_link); 780 if (url != null) { 781 url.setText(dispatch.getUri()); 782 } 783 builder.setView(view); 784 } 785 builder.setNegativeButton(R.string.cancel, (dialog, which) -> {}); 786 builder.setPositiveButton(R.string.action_confirm_url_open, (dialog, which) -> { 787 dispatch.tryStartActivity(); 788 }); 789 AlertDialog dialog = builder.create(); 790 dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 791 dialog.show(); 792 } 793 794 void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 795 synchronized (this) { 796 pw.println("mOverrideIntent=" + mOverrideIntent); 797 pw.println("mOverrideFilters=" + mOverrideFilters); 798 pw.println("mOverrideTechLists=" + mOverrideTechLists); 799 } 800 } 801 802 private class MessageHandler extends Handler { 803 @Override 804 public void handleMessage(Message msg) { 805 if (DBG) Log.d(TAG, "handleMessage: msg=" + msg); 806 807 switch (msg.what) { 808 case PeripheralHandoverService.MSG_HEADSET_CONNECTED: 809 case PeripheralHandoverService.MSG_HEADSET_NOT_CONNECTED: 810 mBluetoothEnabledByNfc.set(msg.arg1 != 0); 811 break; 812 default: 813 break; 814 } 815 } 816 } 817 818 final BroadcastReceiver mBluetoothStatusReceiver = new BroadcastReceiver() { 819 @Override 820 public void onReceive(Context context, Intent intent) { 821 String action = intent.getAction(); 822 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 823 handleBluetoothStateChanged(intent); 824 } 825 } 826 827 private void handleBluetoothStateChanged(Intent intent) { 828 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 829 BluetoothAdapter.ERROR); 830 if (state == BluetoothAdapter.STATE_OFF) { 831 mBluetoothEnabledByNfc.set(false); 832 } 833 } 834 }; 835 } 836