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