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.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.pm.PackageManager; 31import android.content.pm.ResolveInfo; 32import android.net.Uri; 33import android.nfc.FormatException; 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.nio.charset.Charsets; 42import java.util.ArrayList; 43import java.util.Arrays; 44import java.util.List; 45 46/** 47 * Dispatch of NFC events to start activities 48 */ 49public class NfcDispatcher { 50 private static final boolean DBG = NfcService.DBG; 51 private static final String TAG = NfcService.TAG; 52 53 private final Context mContext; 54 private final IActivityManager mIActivityManager; 55 private final RegisteredComponentCache mTechListFilters; 56 57 private PackageManager mPackageManager; 58 59 // Locked on this 60 private PendingIntent mOverrideIntent; 61 private IntentFilter[] mOverrideFilters; 62 private String[][] mOverrideTechLists; 63 64 public NfcDispatcher(Context context, P2pLinkManager p2pManager) { 65 mContext = context; 66 mIActivityManager = ActivityManagerNative.getDefault(); 67 mTechListFilters = new RegisteredComponentCache(mContext, 68 NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); 69 mPackageManager = context.getPackageManager(); 70 } 71 72 public synchronized void setForegroundDispatch(PendingIntent intent, 73 IntentFilter[] filters, String[][] techLists) { 74 if (DBG) Log.d(TAG, "Set Foreground Dispatch"); 75 mOverrideIntent = intent; 76 mOverrideFilters = filters; 77 mOverrideTechLists = techLists; 78 } 79 80 /** Returns false if no activities were found to dispatch to */ 81 public boolean dispatchTag(Tag tag, NdefMessage[] msgs) { 82 if (DBG) { 83 Log.d(TAG, "Dispatching tag"); 84 Log.d(TAG, tag.toString()); 85 } 86 87 IntentFilter[] overrideFilters; 88 PendingIntent overrideIntent; 89 String[][] overrideTechLists; 90 synchronized (this) { 91 overrideFilters = mOverrideFilters; 92 overrideIntent = mOverrideIntent; 93 overrideTechLists = mOverrideTechLists; 94 } 95 96 // First look for dispatch overrides 97 if (overrideIntent != null) { 98 if (DBG) Log.d(TAG, "Attempting to dispatch tag with override"); 99 try { 100 if (dispatchTagInternal(tag, msgs, overrideIntent, overrideFilters, 101 overrideTechLists)) { 102 if (DBG) Log.d(TAG, "Dispatched to override"); 103 return true; 104 } 105 Log.w(TAG, "Dispatch override registered, but no filters matched"); 106 } catch (CanceledException e) { 107 Log.w(TAG, "Dispatch overrides pending intent was canceled"); 108 synchronized (this) { 109 mOverrideFilters = null; 110 mOverrideIntent = null; 111 mOverrideTechLists = null; 112 } 113 } 114 } 115 116 // Try normal dispatch. 117 try { 118 return dispatchTagInternal(tag, msgs, null, null, null); 119 } catch (CanceledException e) { 120 Log.e(TAG, "CanceledException unexpected here", e); 121 return false; 122 } 123 } 124 125 private Intent buildTagIntent(Tag tag, NdefMessage[] msgs, String action) { 126 Intent intent = new Intent(action); 127 intent.putExtra(NfcAdapter.EXTRA_TAG, tag); 128 intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId()); 129 intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, msgs); 130 return intent; 131 } 132 133 /** This method places the launched activity in a (single) NFC 134 * root task. We use NfcRootActivity as the root of the task, 135 * which launches the passed-in intent as soon as it's created. 136 */ 137 private boolean startRootActivity(Intent intent) { 138 Intent rootIntent = new Intent(mContext, NfcRootActivity.class); 139 rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent); 140 rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 141 // Ideally we'd have used startActivityForResult() to determine whether the 142 // NfcRootActivity was able to launch the intent, but startActivityForResult() 143 // is not available on Context. Instead, we query the PackageManager beforehand 144 // to determine if there is an Activity to handle this intent, and base the 145 // result of off that. 146 List<ResolveInfo> activities = mPackageManager.queryIntentActivities(intent, 0); 147 // Try to start the activity regardless of the result. 148 mContext.startActivity(rootIntent); 149 if (activities.size() > 0) { 150 return true; 151 } else { 152 return false; 153 } 154 } 155 156 // Dispatch to either an override pending intent or a standard startActivity() 157 private boolean dispatchTagInternal(Tag tag, NdefMessage[] msgs, 158 PendingIntent overrideIntent, IntentFilter[] overrideFilters, 159 String[][] overrideTechLists) 160 throws CanceledException{ 161 Intent intent; 162 163 // 164 // Try the NDEF content specific dispatch 165 // 166 if (msgs != null && msgs.length > 0) { 167 NdefMessage msg = msgs[0]; 168 NdefRecord[] records = msg.getRecords(); 169 if (records.length > 0) { 170 // Found valid NDEF data, try to dispatch that first 171 NdefRecord record = records[0]; 172 173 intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_NDEF_DISCOVERED); 174 if (setTypeOrDataFromNdef(intent, record)) { 175 // The record contains filterable data, try to start a matching activity 176 if (startDispatchActivity(intent, overrideIntent, overrideFilters, 177 overrideTechLists, records)) { 178 // If an activity is found then skip further dispatching 179 return true; 180 } else { 181 if (DBG) Log.d(TAG, "No activities for NDEF handling of " + intent); 182 } 183 } 184 } 185 } 186 187 // 188 // Try the technology specific dispatch 189 // 190 String[] tagTechs = tag.getTechList(); 191 Arrays.sort(tagTechs); 192 193 if (overrideIntent != null) { 194 // There are dispatch overrides in place 195 if (overrideTechLists != null) { 196 for (String[] filterTechs : overrideTechLists) { 197 if (filterMatch(tagTechs, filterTechs)) { 198 // An override matched, send it to the foreground activity. 199 intent = buildTagIntent(tag, msgs, 200 NfcAdapter.ACTION_TECH_DISCOVERED); 201 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 202 return true; 203 } 204 } 205 } 206 } else { 207 // Standard tech dispatch path 208 ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>(); 209 ArrayList<ComponentInfo> registered = mTechListFilters.getComponents(); 210 211 // Check each registered activity to see if it matches 212 for (ComponentInfo info : registered) { 213 // Don't allow wild card matching 214 if (filterMatch(tagTechs, info.techs) && 215 isComponentEnabled(mPackageManager, info.resolveInfo)) { 216 // Add the activity as a match if it's not already in the list 217 if (!matches.contains(info.resolveInfo)) { 218 matches.add(info.resolveInfo); 219 } 220 } 221 } 222 223 if (matches.size() == 1) { 224 // Single match, launch directly 225 intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED); 226 ResolveInfo info = matches.get(0); 227 intent.setClassName(info.activityInfo.packageName, info.activityInfo.name); 228 if (startRootActivity(intent)) { 229 return true; 230 } 231 } else if (matches.size() > 1) { 232 // Multiple matches, show a custom activity chooser dialog 233 intent = new Intent(mContext, TechListChooserActivity.class); 234 intent.putExtra(Intent.EXTRA_INTENT, 235 buildTagIntent(tag, msgs, NfcAdapter.ACTION_TECH_DISCOVERED)); 236 intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, 237 matches); 238 if (startRootActivity(intent)) { 239 return true; 240 } 241 } else { 242 // No matches, move on 243 if (DBG) Log.w(TAG, "No activities for technology handling"); 244 } 245 } 246 247 // 248 // Try the generic intent 249 // 250 intent = buildTagIntent(tag, msgs, NfcAdapter.ACTION_TAG_DISCOVERED); 251 if (startDispatchActivity(intent, overrideIntent, overrideFilters, overrideTechLists, 252 null)) { 253 return true; 254 } else { 255 Log.e(TAG, "No tag fallback activity found for " + intent); 256 return false; 257 } 258 } 259 260 /* Starts the package main activity if it's already installed, or takes you to its 261 * market page if not. 262 * returns whether an activity was started. 263 */ 264 private boolean startActivityOrMarket(String packageName) { 265 Intent intent = mPackageManager.getLaunchIntentForPackage(packageName); 266 if (intent != null) { 267 return (startRootActivity(intent)); 268 } else { 269 // Find the package in Market: 270 Intent market = getAppSearchIntent(packageName); 271 return(startRootActivity(market)); 272 } 273 } 274 275 private boolean startDispatchActivity(Intent intent, PendingIntent overrideIntent, 276 IntentFilter[] overrideFilters, String[][] overrideTechLists, NdefRecord[] records) 277 throws CanceledException { 278 if (overrideIntent != null) { 279 boolean found = false; 280 if (overrideFilters == null && overrideTechLists == null) { 281 // No filters means to always dispatch regardless of match 282 found = true; 283 } else if (overrideFilters != null) { 284 for (IntentFilter filter : overrideFilters) { 285 if (filter.match(mContext.getContentResolver(), intent, false, TAG) >= 0) { 286 found = true; 287 break; 288 } 289 } 290 } 291 292 if (found) { 293 Log.i(TAG, "Dispatching to override intent " + overrideIntent); 294 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 295 return true; 296 } else { 297 return false; 298 } 299 } else { 300 resumeAppSwitches(); 301 if (records != null) { 302 String firstPackage = null; 303 for (NdefRecord record : records) { 304 if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE) { 305 if (Arrays.equals(record.getType(), NdefRecord.RTD_ANDROID_APP)) { 306 String pkg = new String(record.getPayload(), Charsets.US_ASCII); 307 if (firstPackage == null) { 308 firstPackage = pkg; 309 } 310 intent.setPackage(pkg); 311 if (startRootActivity(intent)) { 312 return true; 313 } 314 } 315 } 316 } 317 if (firstPackage != null) { 318 // Found an Android package, but could not handle ndef intent. 319 // If the application is installed, call its main activity, 320 // or otherwise go to Market. 321 if (startActivityOrMarket(firstPackage)) { 322 return true; 323 } 324 } 325 } 326 return(startRootActivity(intent)); 327 } 328 } 329 330 /** 331 * Tells the ActivityManager to resume allowing app switches. 332 * 333 * If the current app called stopAppSwitches() then our startActivity() can 334 * be delayed for several seconds. This happens with the default home 335 * screen. As a system service we can override this behavior with 336 * resumeAppSwitches(). 337 */ 338 void resumeAppSwitches() { 339 try { 340 mIActivityManager.resumeAppSwitches(); 341 } catch (RemoteException e) { } 342 } 343 344 /** Returns true if the tech list filter matches the techs on the tag */ 345 private boolean filterMatch(String[] tagTechs, String[] filterTechs) { 346 if (filterTechs == null || filterTechs.length == 0) return false; 347 348 for (String tech : filterTechs) { 349 if (Arrays.binarySearch(tagTechs, tech) < 0) { 350 return false; 351 } 352 } 353 return true; 354 } 355 356 private boolean setTypeOrDataFromNdef(Intent intent, NdefRecord record) { 357 short tnf = record.getTnf(); 358 byte[] type = record.getType(); 359 try { 360 switch (tnf) { 361 case NdefRecord.TNF_MIME_MEDIA: { 362 intent.setType(new String(type, Charsets.US_ASCII)); 363 return true; 364 } 365 366 case NdefRecord.TNF_ABSOLUTE_URI: { 367 intent.setData(Uri.parse(new String(type, Charsets.UTF_8))); 368 return true; 369 } 370 371 case NdefRecord.TNF_WELL_KNOWN: { 372 byte[] payload = record.getPayload(); 373 if (payload == null || payload.length == 0) return false; 374 if (Arrays.equals(type, NdefRecord.RTD_TEXT)) { 375 intent.setType("text/plain"); 376 return true; 377 } else if (Arrays.equals(type, NdefRecord.RTD_SMART_POSTER)) { 378 // Parse the smart poster looking for the URI 379 try { 380 NdefMessage msg = new NdefMessage(record.getPayload()); 381 for (NdefRecord subRecord : msg.getRecords()) { 382 short subTnf = subRecord.getTnf(); 383 if (subTnf == NdefRecord.TNF_WELL_KNOWN 384 && Arrays.equals(subRecord.getType(), 385 NdefRecord.RTD_URI)) { 386 intent.setData(NdefRecord.parseWellKnownUriRecord(subRecord)); 387 return true; 388 } else if (subTnf == NdefRecord.TNF_ABSOLUTE_URI) { 389 intent.setData(Uri.parse(new String(subRecord.getType(), 390 Charsets.UTF_8))); 391 return true; 392 } 393 } 394 } catch (FormatException e) { 395 return false; 396 } 397 } else if (Arrays.equals(type, NdefRecord.RTD_URI)) { 398 intent.setData(NdefRecord.parseWellKnownUriRecord(record)); 399 return true; 400 } 401 return false; 402 } 403 404 case NdefRecord.TNF_EXTERNAL_TYPE: { 405 intent.setData(Uri.parse("vnd.android.nfc://ext/" + 406 new String(record.getType(), Charsets.US_ASCII))); 407 return true; 408 } 409 } 410 return false; 411 } catch (Exception e) { 412 Log.e(TAG, "failed to parse record", e); 413 return false; 414 } 415 } 416 417 /** 418 * Returns an intent that can be used to find an application not currently 419 * installed on the device. 420 */ 421 private static Intent getAppSearchIntent(String pkg) { 422 Intent market = new Intent(Intent.ACTION_VIEW); 423 market.setData(Uri.parse("market://details?id=" + pkg)); 424 return market; 425 } 426 427 private static boolean isComponentEnabled(PackageManager pm, ResolveInfo info) { 428 boolean enabled = false; 429 ComponentName compname = new ComponentName( 430 info.activityInfo.packageName, info.activityInfo.name); 431 try { 432 // Note that getActivityInfo() will internally call 433 // isEnabledLP() to determine whether the component 434 // enabled. If it's not, null is returned. 435 if (pm.getActivityInfo(compname,0) != null) { 436 enabled = true; 437 } 438 } catch (PackageManager.NameNotFoundException e) { 439 enabled = false; 440 } 441 if (!enabled) { 442 Log.d(TAG, "Component not enabled: " + compname); 443 } 444 return enabled; 445 } 446} 447