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