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