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