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