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.bluetooth.BluetoothAdapter; 20 21 import com.android.nfc.RegisteredComponentCache.ComponentInfo; 22 import com.android.nfc.handover.HandoverDataParser; 23 import com.android.nfc.handover.PeripheralHandoverService; 24 25 import android.app.Activity; 26 import android.app.ActivityManager; 27 import android.app.ActivityManagerNative; 28 import android.app.IActivityManager; 29 import android.app.PendingIntent; 30 import android.app.PendingIntent.CanceledException; 31 import android.content.ComponentName; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentFilter; 36 import android.content.pm.PackageManager; 37 import android.content.pm.PackageManager.NameNotFoundException; 38 import android.content.pm.ResolveInfo; 39 import android.content.res.Resources.NotFoundException; 40 import android.net.Uri; 41 import android.nfc.NdefMessage; 42 import android.nfc.NdefRecord; 43 import android.nfc.NfcAdapter; 44 import android.nfc.Tag; 45 import android.nfc.tech.Ndef; 46 import android.os.RemoteException; 47 import android.os.UserHandle; 48 import android.util.Log; 49 50 import java.io.FileDescriptor; 51 import java.io.PrintWriter; 52 import java.nio.charset.StandardCharsets; 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.LinkedList; 56 import java.util.List; 57 58 /** 59 * Dispatch of NFC events to start activities 60 */ 61 class NfcDispatcher { 62 private static final boolean DBG = false; 63 private static final String TAG = "NfcDispatcher"; 64 65 static final int DISPATCH_SUCCESS = 1; 66 static final int DISPATCH_FAIL = 2; 67 static final int DISPATCH_UNLOCK = 3; 68 69 private final Context mContext; 70 private final IActivityManager mIActivityManager; 71 private final RegisteredComponentCache mTechListFilters; 72 private final ContentResolver mContentResolver; 73 private final HandoverDataParser mHandoverDataParser; 74 private final String[] mProvisioningMimes; 75 private final ScreenStateHelper mScreenStateHelper; 76 private final NfcUnlockManager mNfcUnlockManager; 77 private final boolean mDeviceSupportsBluetooth; 78 79 // Locked on this 80 private PendingIntent mOverrideIntent; 81 private IntentFilter[] mOverrideFilters; 82 private String[][] mOverrideTechLists; 83 private boolean mProvisioningOnly; 84 85 NfcDispatcher(Context context, 86 HandoverDataParser handoverDataParser, 87 boolean provisionOnly) { 88 mContext = context; 89 mIActivityManager = ActivityManagerNative.getDefault(); 90 mTechListFilters = new RegisteredComponentCache(mContext, 91 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); 92 mContentResolver = context.getContentResolver(); 93 mHandoverDataParser = handoverDataParser; 94 mScreenStateHelper = new ScreenStateHelper(context); 95 mNfcUnlockManager = NfcUnlockManager.getInstance(); 96 mDeviceSupportsBluetooth = BluetoothAdapter.getDefaultAdapter() != null; 97 98 synchronized (this) { 99 mProvisioningOnly = provisionOnly; 100 } 101 String[] provisionMimes = null; 102 if (provisionOnly) { 103 try { 104 // Get accepted mime-types 105 provisionMimes = context.getResources(). 106 getStringArray(R.array.provisioning_mime_types); 107 } catch (NotFoundException e) { 108 provisionMimes = null; 109 } 110 } 111 mProvisioningMimes = provisionMimes; 112 } 113 114 public synchronized void setForegroundDispatch(PendingIntent intent, 115 IntentFilter[] filters, String[][] techLists) { 116 if (DBG) Log.d(TAG, "Set Foreground Dispatch"); 117 mOverrideIntent = intent; 118 mOverrideFilters = filters; 119 mOverrideTechLists = techLists; 120 } 121 122 public synchronized void disableProvisioningMode() { 123 mProvisioningOnly = false; 124 } 125 126 /** 127 * Helper for re-used objects and methods during a single tag dispatch. 128 */ 129 static class DispatchInfo { 130 public final Intent intent; 131 132 final Intent rootIntent; 133 final Uri ndefUri; 134 final String ndefMimeType; 135 final PackageManager packageManager; 136 final Context context; 137 138 public DispatchInfo(Context context, Tag tag, NdefMessage message) { 139 intent = new Intent(); 140 intent.putExtra(NfcAdapter.EXTRA_TAG, tag); 141 intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId()); 142 if (message != null) { 143 intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message}); 144 ndefUri = message.getRecords()[0].toUri(); 145 ndefMimeType = message.getRecords()[0].toMimeType(); 146 } else { 147 ndefUri = null; 148 ndefMimeType = null; 149 } 150 151 rootIntent = new Intent(context, NfcRootActivity.class); 152 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent); 153 rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 154 155 this.context = context; 156 packageManager = context.getPackageManager(); 157 } 158 159 public Intent setNdefIntent() { 160 intent.setAction(NfcAdapter.ACTION_NDEF_DISCOVERED); 161 if (ndefUri != null) { 162 intent.setData(ndefUri); 163 return intent; 164 } else if (ndefMimeType != null) { 165 intent.setType(ndefMimeType); 166 return intent; 167 } 168 return null; 169 } 170 171 public Intent setTechIntent() { 172 intent.setData(null); 173 intent.setType(null); 174 intent.setAction(NfcAdapter.ACTION_TECH_DISCOVERED); 175 return intent; 176 } 177 178 public Intent setTagIntent() { 179 intent.setData(null); 180 intent.setType(null); 181 intent.setAction(NfcAdapter.ACTION_TAG_DISCOVERED); 182 return intent; 183 } 184 185 /** 186 * Launch the activity via a (single) NFC root task, so that it 187 * creates a new task stack instead of interfering with any existing 188 * task stack for that activity. 189 * NfcRootActivity acts as the task root, it immediately calls 190 * start activity on the intent it is passed. 191 */ 192 boolean tryStartActivity() { 193 // Ideally we'd have used startActivityForResult() to determine whether the 194 // NfcRootActivity was able to launch the intent, but startActivityForResult() 195 // is not available on Context. Instead, we query the PackageManager beforehand 196 // to determine if there is an Activity to handle this intent, and base the 197 // result of off that. 198 List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(intent, 0, 199 ActivityManager.getCurrentUser()); 200 if (activities.size() > 0) { 201 context.startActivityAsUser(rootIntent, UserHandle.CURRENT); 202 return true; 203 } 204 return false; 205 } 206 207 boolean tryStartActivity(Intent intentToStart) { 208 List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser( 209 intentToStart, 0, ActivityManager.getCurrentUser()); 210 if (activities.size() > 0) { 211 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intentToStart); 212 context.startActivityAsUser(rootIntent, UserHandle.CURRENT); 213 return true; 214 } 215 return false; 216 } 217 } 218 219 /** Returns: 220 * <ul> 221 * <li /> DISPATCH_SUCCESS if dispatched to an activity, 222 * <li /> DISPATCH_FAIL if no activities were found to dispatch to, 223 * <li /> DISPATCH_UNLOCK if the tag was used to unlock the device 224 * </ul> 225 */ 226 public int dispatchTag(Tag tag) { 227 PendingIntent overrideIntent; 228 IntentFilter[] overrideFilters; 229 String[][] overrideTechLists; 230 boolean provisioningOnly; 231 232 synchronized (this) { 233 overrideFilters = mOverrideFilters; 234 overrideIntent = mOverrideIntent; 235 overrideTechLists = mOverrideTechLists; 236 provisioningOnly = mProvisioningOnly; 237 } 238 239 boolean screenUnlocked = false; 240 if (!provisioningOnly && 241 mScreenStateHelper.checkScreenState() == ScreenStateHelper.SCREEN_STATE_ON_LOCKED) { 242 screenUnlocked = handleNfcUnlock(tag); 243 if (!screenUnlocked) { 244 return DISPATCH_FAIL; 245 } 246 } 247 248 NdefMessage message = null; 249 Ndef ndef = Ndef.get(tag); 250 if (ndef != null) { 251 message = ndef.getCachedNdefMessage(); 252 } 253 254 if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message); 255 256 DispatchInfo dispatch = new DispatchInfo(mContext, tag, message); 257 258 resumeAppSwitches(); 259 260 if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters, 261 overrideTechLists)) { 262 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 263 } 264 265 if (tryPeripheralHandover(message)) { 266 if (DBG) Log.i(TAG, "matched BT HANDOVER"); 267 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 268 } 269 270 if (NfcWifiProtectedSetup.tryNfcWifiSetup(ndef, mContext)) { 271 if (DBG) Log.i(TAG, "matched NFC WPS TOKEN"); 272 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 273 } 274 275 if (tryNdef(dispatch, message, provisioningOnly)) { 276 return screenUnlocked ? DISPATCH_UNLOCK : DISPATCH_SUCCESS; 277 } 278 279 if (screenUnlocked) { 280 // We only allow NDEF-based mimeType matching in case of an unlock 281 return DISPATCH_UNLOCK; 282 } 283 284 if (provisioningOnly) { 285 // We only allow NDEF-based mimeType matching 286 return DISPATCH_FAIL; 287 } 288 289 // Only allow NDEF-based mimeType matching for unlock tags 290 if (tryTech(dispatch, tag)) { 291 return DISPATCH_SUCCESS; 292 } 293 294 dispatch.setTagIntent(); 295 if (dispatch.tryStartActivity()) { 296 if (DBG) Log.i(TAG, "matched TAG"); 297 return DISPATCH_SUCCESS; 298 } 299 300 if (DBG) Log.i(TAG, "no match"); 301 return DISPATCH_FAIL; 302 } 303 304 private boolean handleNfcUnlock(Tag tag) { 305 return mNfcUnlockManager.tryUnlock(tag); 306 } 307 308 boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent, 309 IntentFilter[] overrideFilters, String[][] overrideTechLists) { 310 if (overrideIntent == null) { 311 return false; 312 } 313 Intent intent; 314 315 // NDEF 316 if (message != null) { 317 intent = dispatch.setNdefIntent(); 318 if (intent != null && 319 isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 320 try { 321 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 322 if (DBG) Log.i(TAG, "matched NDEF override"); 323 return true; 324 } catch (CanceledException e) { 325 return false; 326 } 327 } 328 } 329 330 // TECH 331 intent = dispatch.setTechIntent(); 332 if (isTechMatch(tag, overrideTechLists)) { 333 try { 334 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 335 if (DBG) Log.i(TAG, "matched TECH override"); 336 return true; 337 } catch (CanceledException e) { 338 return false; 339 } 340 } 341 342 // TAG 343 intent = dispatch.setTagIntent(); 344 if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 345 try { 346 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 347 if (DBG) Log.i(TAG, "matched TAG override"); 348 return true; 349 } catch (CanceledException e) { 350 return false; 351 } 352 } 353 return false; 354 } 355 356 boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) { 357 if (filters != null) { 358 for (IntentFilter filter : filters) { 359 if (filter.match(mContentResolver, intent, false, TAG) >= 0) { 360 return true; 361 } 362 } 363 } else if (!hasTechFilter) { 364 return true; // always match if both filters and techlists are null 365 } 366 return false; 367 } 368 369 boolean isTechMatch(Tag tag, String[][] techLists) { 370 if (techLists == null) { 371 return false; 372 } 373 374 String[] tagTechs = tag.getTechList(); 375 Arrays.sort(tagTechs); 376 for (String[] filterTechs : techLists) { 377 if (filterMatch(tagTechs, filterTechs)) { 378 return true; 379 } 380 } 381 return false; 382 } 383 384 boolean tryNdef(DispatchInfo dispatch, NdefMessage message, boolean provisioningOnly) { 385 if (message == null) { 386 return false; 387 } 388 Intent intent = dispatch.setNdefIntent(); 389 390 // Bail out if the intent does not contain filterable NDEF data 391 if (intent == null) return false; 392 393 if (provisioningOnly) { 394 if (mProvisioningMimes == null || 395 !(Arrays.asList(mProvisioningMimes).contains(intent.getType()))) { 396 Log.e(TAG, "Dropping NFC intent in provisioning mode."); 397 return false; 398 } 399 } 400 401 // Try to start AAR activity with matching filter 402 List<String> aarPackages = extractAarPackages(message); 403 for (String pkg : aarPackages) { 404 dispatch.intent.setPackage(pkg); 405 if (dispatch.tryStartActivity()) { 406 if (DBG) Log.i(TAG, "matched AAR to NDEF"); 407 return true; 408 } 409 } 410 411 // Try to perform regular launch of the first AAR 412 if (aarPackages.size() > 0) { 413 String firstPackage = aarPackages.get(0); 414 PackageManager pm; 415 try { 416 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); 417 pm = mContext.createPackageContextAsUser("android", 0, 418 currentUser).getPackageManager(); 419 } catch (NameNotFoundException e) { 420 Log.e(TAG, "Could not create user package context"); 421 return false; 422 } 423 Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage); 424 if (appLaunchIntent != null && dispatch.tryStartActivity(appLaunchIntent)) { 425 if (DBG) Log.i(TAG, "matched AAR to application launch"); 426 return true; 427 } 428 // Find the package in Market: 429 Intent marketIntent = getAppSearchIntent(firstPackage); 430 if (marketIntent != null && dispatch.tryStartActivity(marketIntent)) { 431 if (DBG) Log.i(TAG, "matched AAR to market launch"); 432 return true; 433 } 434 } 435 436 // regular launch 437 dispatch.intent.setPackage(null); 438 if (dispatch.tryStartActivity()) { 439 if (DBG) Log.i(TAG, "matched NDEF"); 440 return true; 441 } 442 443 return false; 444 } 445 446 static List<String> extractAarPackages(NdefMessage message) { 447 List<String> aarPackages = new LinkedList<String>(); 448 for (NdefRecord record : message.getRecords()) { 449 String pkg = checkForAar(record); 450 if (pkg != null) { 451 aarPackages.add(pkg); 452 } 453 } 454 return aarPackages; 455 } 456 457 boolean tryTech(DispatchInfo dispatch, Tag tag) { 458 dispatch.setTechIntent(); 459 460 String[] tagTechs = tag.getTechList(); 461 Arrays.sort(tagTechs); 462 463 // Standard tech dispatch path 464 ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>(); 465 List<ComponentInfo> registered = mTechListFilters.getComponents(); 466 467 PackageManager pm; 468 try { 469 UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser()); 470 pm = mContext.createPackageContextAsUser("android", 0, 471 currentUser).getPackageManager(); 472 } catch (NameNotFoundException e) { 473 Log.e(TAG, "Could not create user package context"); 474 return false; 475 } 476 // Check each registered activity to see if it matches 477 for (ComponentInfo info : registered) { 478 // Don't allow wild card matching 479 if (filterMatch(tagTechs, info.techs) && 480 isComponentEnabled(pm, info.resolveInfo)) { 481 // Add the activity as a match if it's not already in the list 482 if (!matches.contains(info.resolveInfo)) { 483 matches.add(info.resolveInfo); 484 } 485 } 486 } 487 488 if (matches.size() == 1) { 489 // Single match, launch directly 490 ResolveInfo info = matches.get(0); 491 dispatch.intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); 492 if (dispatch.tryStartActivity()) { 493 if (DBG) Log.i(TAG, "matched single TECH"); 494 return true; 495 } 496 dispatch.intent.setComponent(null); 497 } else if (matches.size() > 1) { 498 // Multiple matches, show a custom activity chooser dialog 499 Intent intent = new Intent(mContext, TechListChooserActivity.class); 500 intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent); 501 intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, 502 matches); 503 if (dispatch.tryStartActivity(intent)) { 504 if (DBG) Log.i(TAG, "matched multiple TECH"); 505 return true; 506 } 507 } 508 return false; 509 } 510 511 public boolean tryPeripheralHandover(NdefMessage m) { 512 if (m == null || !mDeviceSupportsBluetooth) return false; 513 514 if (DBG) Log.d(TAG, "tryHandover(): " + m.toString()); 515 516 HandoverDataParser.BluetoothHandoverData handover = mHandoverDataParser.parseBluetooth(m); 517 if (handover == null || !handover.valid) return false; 518 519 Intent intent = new Intent(mContext, PeripheralHandoverService.class); 520 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_DEVICE, handover.device); 521 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_NAME, handover.name); 522 intent.putExtra(PeripheralHandoverService.EXTRA_PERIPHERAL_TRANSPORT, handover.transport); 523 mContext.startServiceAsUser(intent, UserHandle.CURRENT); 524 525 return true; 526 } 527 528 529 /** 530 * Tells the ActivityManager to resume allowing app switches. 531 * 532 * If the current app called stopAppSwitches() then our startActivity() can 533 * be delayed for several seconds. This happens with the default home 534 * screen. As a system service we can override this behavior with 535 * resumeAppSwitches(). 536 */ 537 void resumeAppSwitches() { 538 try { 539 mIActivityManager.resumeAppSwitches(); 540 } catch (RemoteException e) { } 541 } 542 543 /** Returns true if the tech list filter matches the techs on the tag */ 544 boolean filterMatch(String[] tagTechs, String[] filterTechs) { 545 if (filterTechs == null || filterTechs.length == 0) return false; 546 547 for (String tech : filterTechs) { 548 if (Arrays.binarySearch(tagTechs, tech) < 0) { 549 return false; 550 } 551 } 552 return true; 553 } 554 555 static String checkForAar(NdefRecord record) { 556 if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE && 557 Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) { 558 return new String(record.getPayload(), StandardCharsets.US_ASCII); 559 } 560 return null; 561 } 562 563 /** 564 * Returns an intent that can be used to find an application not currently 565 * installed on the device. 566 */ 567 static Intent getAppSearchIntent(String pkg) { 568 Intent market = new Intent(Intent.ACTION_VIEW); 569 market.setData(Uri.parse("market://details?id=" + pkg)); 570 return market; 571 } 572 573 static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) { 574 boolean enabled = false; 575 ComponentName compname = new ComponentName( 576 info.activityInfo.packageName, info.activityInfo.name); 577 try { 578 // Note that getActivityInfo() will internally call 579 // isEnabledLP() to determine whether the component 580 // enabled. If it's not, null is returned. 581 if (pm.getActivityInfo(compname,0) != null) { 582 enabled = true; 583 } 584 } catch (PackageManager.NameNotFoundException e) { 585 enabled = false; 586 } 587 if (!enabled) { 588 Log.d(TAG, "Component not enabled: " + compname); 589 } 590 return enabled; 591 } 592 593 void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 594 synchronized (this) { 595 pw.println("mOverrideIntent=" + mOverrideIntent); 596 pw.println("mOverrideFilters=" + mOverrideFilters); 597 pw.println("mOverrideTechLists=" + mOverrideTechLists); 598 } 599 } 600 } 601