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