2013年10月23日水曜日

Android Beam を Foreground Dispatch で受けとるときの注意点

Chrome から送られてくる Android Beam を Foreground Dispatch で受けとるにはちょっと注意が必要です。

Chrome から URL を送ったときの Android Beam の Intent は次のようになっています。

getAction() = android.nfc.action.NDEF_DISCOVERED
getType() = null
getScheme() = http
getData() = http://www.tensaikojo.com/
getCategories() = null

一方、アプリから NdefRecord.createMime() を使って、テキストデータを Android Beam で送った場合の Intent は次のようになっています。

getAction() = android.nfc.action.NDEF_DISCOVERED
getType() = application/vnd.net.yanzm.example
getScheme() = null
getData() = null
getCategories() = null

Advanced NFC | Android Developers のコードのように */* を DataType に指定した IntentFilter では、2番目の Android Beam は受けとれますが Chrome からの Android Beam は受けとれません(Android Beam を送ると、このアプリをすり抜けて Chrome アプリが起動します)。
  1. NfcAdapter mNfcAdapter;  
  2.   
  3. @Override  
  4. protected void onResume() {  
  5.     super.onResume();  
  6.     if (mNfcAdapter != null) {  
  7.         PendingIntent pendingIntent = PendingIntent.getActivity(this0,  
  8.                 getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);  
  9.   
  10.         IntentFilter ndef = IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "*/*");  
  11.         IntentFilter[] intentFiltersArray = new IntentFilter[] { ndef };  
  12.   
  13.         mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null);  
  14.     }  
  15. }  
  16.   
  17. @Override  
  18. protected void onPause() {  
  19.     super.onPause();  
  20.     if (mNfcAdapter != null) {  
  21.         mNfcAdapter.disableForegroundDispatch(this);  
  22.     }  
  23. }  
次のように enableForegroundDispatch() の第3引数と第4引数に null を渡すと、ともかく全部拾ってくれるようになります。
  1. @Override  
  2. protected void onResume() {  
  3.     super.onResume();  
  4.     if (mNfcAdapter != null) {  
  5.         PendingIntent pendingIntent = PendingIntent.getActivity(this0,  
  6.                 getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);  
  7.   
  8.         // catch all ndef  
  9.         mNfcAdapter.enableForegroundDispatch(this, pendingIntent, nullnull);  
  10.     }  
  11. }  
IntentFilter[] を指定して Chrome の AndroidBeam も受けとるには、次のように scheme を指定した IntentFilter も追加します。
  1. @Override  
  2. protected void onResume() {  
  3.     super.onResume();  
  4.     if (mNfcAdapter != null) {  
  5.         PendingIntent pendingIntent = PendingIntent.getActivity(this0,  
  6.                 getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);  
  7.           
  8.         IntentFilter ndef = IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "*/*");  
  9.   
  10.         IntentFilter ndef2 = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);  
  11.         ndef2.addDataScheme("");  
  12.         ndef2.addDataScheme("http");  
  13.         ndef2.addDataScheme("https");  
  14.           
  15.         IntentFilter[] intentFiltersArray = new IntentFilter[] { ndef, ndef2 };  
  16.   
  17.         mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null);  
  18.     }  
  19. }  



---------------------------------------------


ここからは解説というか内部コードのメモです。

NfcAdapter の enableForegroundDispatch() では、IntentFilter[] はそのまま NfcService の setForegroundDispatch() に渡されています。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/nfc/NfcAdapter.java#1011
  1. 1011     public void enableForegroundDispatch(Activity activity, PendingIntent intent,  
  2. 1012             IntentFilter[] filters, String[][] techLists) {  
  3. 1013         if (activity == null || intent == null) {  
  4. 1014             throw new NullPointerException();  
  5. 1015         }  
  6. 1016         if (!activity.isResumed()) {  
  7. 1017             throw new IllegalStateException("Foreground dispatch can only be enabled " +  
  8. 1018                     "when your activity is resumed");  
  9. 1019         }  
  10. 1020         try {  
  11. 1021             TechListParcel parcel = null;  
  12. 1022             if (techLists != null && techLists.length > 0) {  
  13. 1023                 parcel = new TechListParcel(techLists);  
  14. 1024             }  
  15. 1025             ActivityThread.currentActivityThread().registerOnActivityPausedListener(activity,  
  16. 1026                     mForegroundDispatchListener);  
  17. 1027             sService.setForegroundDispatch(intent, filters, parcel);  
  18. 1028         } catch (RemoteException e) {  
  19. 1029             attemptDeadServiceRecovery(e);  
  20. 1030         }  
  21. 1031     }  
  22. 1032   


NfcService でも IntentFilter[] は内部の null チェックがされるだけで、そのまま NfcDispatcher の setForegroundDispatch() に渡されます。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/packages/apps/Nfc/src/com/android/nfc/NfcService.java#837
  1. 837         public void setForegroundDispatch(PendingIntent intent,  
  2. 838                 IntentFilter[] filters, TechListParcel techListsParcel) {  
  3. 839             mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);  
  4. 840   
  5. 841             // Short-cut the disable path  
  6. 842             if (intent == null && filters == null && techListsParcel == null) {  
  7. 843                 mNfcDispatcher.setForegroundDispatch(nullnullnull);  
  8. 844                 return;  
  9. 845             }  
  10. 846   
  11. 847             // Validate the IntentFilters  
  12. 848             if (filters != null) {  
  13. 849                 if (filters.length == 0) {  
  14. 850                     filters = null;  
  15. 851                 } else {  
  16. 852                     for (IntentFilter filter : filters) {  
  17. 853                         if (filter == null) {  
  18. 854                             throw new IllegalArgumentException("null IntentFilter");  
  19. 855                         }  
  20. 856                     }  
  21. 857                 }  
  22. 858             }  
  23. 859   
  24. 860             // Validate the tech lists  
  25. 861             String[][] techLists = null;  
  26. 862             if (techListsParcel != null) {  
  27. 863                 techLists = techListsParcel.getTechLists();  
  28. 864             }  
  29. 865   
  30. 866             mNfcDispatcher.setForegroundDispatch(intent, filters, techLists);  
  31. 867         }  


setForegroundDispatch() に渡された IntentFilter[] は mOverrideFilters に保持され、dispatchTag() メソッドで利用されます。dispatchTag() は、タグをどのIntentに割り当てるか決めるメソッドです。この中で、Foreground Dispatch に対応するかどうかを tryOverrides() を呼び出して判定しています。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/packages/apps/Nfc/src/com/android/nfc/NfcDispatcher.java#81
  1.  81     public synchronized void setForegroundDispatch(PendingIntent intent,  
  2.  82             IntentFilter[] filters, String[][] techLists) {  
  3.  83         if (DBG) Log.d(TAG, "Set Foreground Dispatch");  
  4.  84         mOverrideIntent = intent;  
  5.  85         mOverrideFilters = filters;  
  6.  86         mOverrideTechLists = techLists;  
  7.  87     }  
  8.   
  9. 182     /** Returns false if no activities were found to dispatch to */  
  10. 183     public boolean dispatchTag(Tag tag) {  
  11. 184         NdefMessage message = null;  
  12. 185         Ndef ndef = Ndef.get(tag);  
  13. 186         if (ndef != null) {  
  14. 187             message = ndef.getCachedNdefMessage();  
  15. 188         }  
  16. 189         if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message);  
  17. 190   
  18. 191         PendingIntent overrideIntent;  
  19. 192         IntentFilter[] overrideFilters;  
  20. 193         String[][] overrideTechLists;  
  21. 194   
  22. 195         DispatchInfo dispatch = new DispatchInfo(mContext, tag, message);  
  23. 196         synchronized (this) {  
  24. 197             overrideFilters = mOverrideFilters;  
  25. 198             overrideIntent = mOverrideIntent;  
  26. 199             overrideTechLists = mOverrideTechLists;  
  27. 200         }  
  28. 201   
  29. 202         resumeAppSwitches();  
  30. 203   
  31. 204         if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters, overrideTechLists)) {  
  32. 205             return true;  
  33. 206         }  
  34. 207   
  35. 208         if (mHandoverManager.tryHandover(message)) {  
  36. 209             if (DBG) Log.i(TAG, "matched BT HANDOVER");  
  37. 210             return true;  
  38. 211         }  
  39. 212   
  40. 213         if (tryNdef(dispatch, message)) {  
  41. 214             return true;  
  42. 215         }  
  43. 216   
  44. 217         if (tryTech(dispatch, tag)) {  
  45. 218             return true;  
  46. 219         }  
  47. 220   
  48. 221         dispatch.setTagIntent();  
  49. 222         if (dispatch.tryStartActivity()) {  
  50. 223             if (DBG) Log.i(TAG, "matched TAG");  
  51. 224             return true;  
  52. 225         }  
  53. 226   
  54. 227         if (DBG) Log.i(TAG, "no match");  
  55. 228         return false;  
  56. 229     }  


tryOverrides() では、Android Beam は NDEF なので #239 の if に入ります。
ここでは isFilterMatch() を呼んで対応するものかどうか判定しています。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/packages/apps/Nfc/src/com/android/nfc/NfcDispatcher.java#231
  1. 231     boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent,  
  2. 232             IntentFilter[] overrideFilters, String[][] overrideTechLists) {  
  3. 233         if (overrideIntent == null) {  
  4. 234             return false;  
  5. 235         }  
  6. 236         Intent intent;  
  7. 237   
  8. 238         // NDEF  
  9. 239         if (message != null) {  
  10. 240             intent = dispatch.setNdefIntent();  
  11. 241             if (intent != null &&  
  12. 242                     isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {  
  13. 243                 try {  
  14. 244                     overrideIntent.send(mContext, Activity.RESULT_OK, intent);  
  15. 245                     if (DBG) Log.i(TAG, "matched NDEF override");  
  16. 246                     return true;  
  17. 247                 } catch (CanceledException e) {  
  18. 248                     return false;  
  19. 249                 }  
  20. 250             }  
  21. 251         }  
  22. 252   
  23. 253         // TECH  
  24. 254         intent = dispatch.setTechIntent();  
  25. 255         if (isTechMatch(tag, overrideTechLists)) {  
  26. 256             try {  
  27. 257                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);  
  28. 258                 if (DBG) Log.i(TAG, "matched TECH override");  
  29. 259                 return true;  
  30. 260             } catch (CanceledException e) {  
  31. 261                 return false;  
  32. 262             }  
  33. 263         }  
  34. 264   
  35. 265         // TAG  
  36. 266         intent = dispatch.setTagIntent();  
  37. 267         if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) {  
  38. 268             try {  
  39. 269                 overrideIntent.send(mContext, Activity.RESULT_OK, intent);  
  40. 270                 if (DBG) Log.i(TAG, "matched TAG override");  
  41. 271                 return true;  
  42. 272             } catch (CanceledException e) {  
  43. 273                 return false;  
  44. 274             }  
  45. 275         }  
  46. 276         return false;  
  47. 277     }  


isFilterMatch() を見ると、IntentFilter[] が null で hasTechFilter が false のとき(つまり、String[][] overrideTechLists が null のとき)は true が返ります。
enableForegroundDispatch() の第3引数と第4引数に null を渡すと、全ての Android Beam が受けとれるようになるのは、ここに入るからです。

IntentFilter[] が null ではない場合、各 IntentFilter で match() を呼んでいます。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/packages/apps/Nfc/src/com/android/nfc/NfcDispatcher.java#279
  1. 279     boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) {  
  2. 280         if (filters != null) {  
  3. 281             for (IntentFilter filter : filters) {  
  4. 282                 if (filter.match(mContentResolver, intent, false, TAG) >= 0) {  
  5. 283                     return true;  
  6. 284                 }  
  7. 285             }  
  8. 286         } else if (!hasTechFilter) {  
  9. 287             return true;  // always match if both filters and techlists are null  
  10. 288         }  
  11. 289         return false;  
  12. 290     }  


IntentFilter の match() の第3引数に false を指定しているので、intent.getType() が type として利用されます。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/content/IntentFilter.java#1068
  1. 1068     public final int match(ContentResolver resolver, Intent intent,  
  2. 1069             boolean resolve, String logTag) {  
  3. 1070         String type = resolve ? intent.resolveType(resolver) : intent.getType();  
  4. 1071         return match(intent.getAction(), type, intent.getScheme(),  
  5. 1072                      intent.getData(), intent.getCategories(), logTag);  
  6. 1073     }  
  7.   
  8. 1103     public final int match(String action, String type, String scheme,  
  9. 1104             Uri data, Set<string> categories, String logTag) {  
  10. 1105         if (action != null && !matchAction(action)) {  
  11. 1106             if (false) Log.v(  
  12. 1107                 logTag, "No matching action " + action + " for " + this);  
  13. 1108             return NO_MATCH_ACTION;  
  14. 1109         }  
  15. 1110   
  16. 1111         int dataMatch = matchData(type, scheme, data);  
  17. 1112         if (dataMatch < 0) {  
  18. 1113             if (false) {  
  19. 1114                 if (dataMatch == NO_MATCH_TYPE) {  
  20. 1115                     Log.v(logTag, "No matching type " + type  
  21. 1116                           + " for " + this);  
  22. 1117                 }  
  23. 1118                 if (dataMatch == NO_MATCH_DATA) {  
  24. 1119                     Log.v(logTag, "No matching scheme/path " + data  
  25. 1120                           + " for " + this);  
  26. 1121                 }  
  27. 1122             }  
  28. 1123             return dataMatch;  
  29. 1124         }  
  30. 1125   
  31. 1126         String categoryMismatch = matchCategories(categories);  
  32. 1127         if (categoryMismatch != null) {  
  33. 1128             if (false) {  
  34. 1129                 Log.v(logTag, "No matching category " + categoryMismatch + " for " + this);  
  35. 1130             }  
  36. 1131             return NO_MATCH_CATEGORY;  
  37. 1132         }  
  38. 1133   
  39. 1134         // It would be nice to treat container activities as more  
  40. 1135         // important than ones that can be embedded, but this is not the way...  
  41. 1136         if (false) {  
  42. 1137             if (categories != null) {  
  43. 1138                 dataMatch -= mCategories.size() - categories.size();  
  44. 1139             }  
  45. 1140         }  
  46. 1141   
  47. 1142         return dataMatch;  
  48. 1143     }  
  49. tring>  
Chrome からの Android Beam が上記の IntentFilter で受けとれないのは matchData() の戻り値が 0 未満になるのが原因です。

Chrome からの Android Beam は scheme が http なので、addDataScheme() で http を追加していない IntentFilter では #939 の if に入ってしまいます。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/content/IntentFilter.java#matchData
  1. 899     public final int matchData(String type, String scheme, Uri data) {  
  2. 900         final ArrayList<string> types = mDataTypes;  
  3. 901         final ArrayList<string> schemes = mDataSchemes;  
  4. 902         final ArrayList<authorityentry> authorities = mDataAuthorities;  
  5. 903         final ArrayList<patternmatcher> paths = mDataPaths;  
  6. 904   
  7. 905         int match = MATCH_CATEGORY_EMPTY;  
  8. 906   
  9. 907         if (types == null && schemes == null) {  
  10. 908             return ((type == null && data == null)  
  11. 909                 ? (MATCH_CATEGORY_EMPTY+MATCH_ADJUSTMENT_NORMAL) : NO_MATCH_DATA);  
  12. 910         }  
  13. 911   
  14. 912         if (schemes != null) {  
  15. 913             if (schemes.contains(scheme != null ? scheme : "")) {  
  16. 914                 match = MATCH_CATEGORY_SCHEME;  
  17. 915             } else {  
  18. 916                 return NO_MATCH_DATA;  
  19. 917             }  
  20. 918   
  21. 919             if (authorities != null) {  
  22. 920                 int authMatch = matchDataAuthority(data);  
  23. 921                 if (authMatch >= 0) {  
  24. 922                     if (paths == null) {  
  25. 923                         match = authMatch;  
  26. 924                     } else if (hasDataPath(data.getPath())) {  
  27. 925                         match = MATCH_CATEGORY_PATH;  
  28. 926                     } else {  
  29. 927                         return NO_MATCH_DATA;  
  30. 928                     }  
  31. 929                 } else {  
  32. 930                     return NO_MATCH_DATA;  
  33. 931                 }  
  34. 932             }  
  35. 933         } else {  
  36. 934             // Special case: match either an Intent with no data URI,  
  37. 935             // or with a scheme: URI.  This is to give a convenience for  
  38. 936             // the common case where you want to deal with data in a  
  39. 937             // content provider, which is done by type, and we don't want  
  40. 938             // to force everyone to say they handle content: or file: URIs.  
  41. 939             if (scheme != null && !"".equals(scheme)  
  42. 940                     && !"content".equals(scheme)  
  43. 941                     && !"file".equals(scheme)) {  
  44. 942                 return NO_MATCH_DATA;  
  45. 943             }  
  46. 944         }  
  47. 945   
  48. 946         if (types != null) {  
  49. 947             if (findMimeType(type)) {  
  50. 948                 match = MATCH_CATEGORY_TYPE;  
  51. 949             } else {  
  52. 950                 return NO_MATCH_TYPE;  
  53. 951             }  
  54. 952         } else {  
  55. 953             // If no MIME types are specified, then we will only match against  
  56. 954             // an Intent that does not have a MIME type.  
  57. 955             if (type != null) {  
  58. 956                 return NO_MATCH_TYPE;  
  59. 957             }  
  60. 958         }  
  61. 959   
  62. 960         return match + MATCH_ADJUSTMENT_NORMAL;  
  63. 961     }  
  64. tternmatcher></authorityentry></string></string>  


addDataScheme() で http が追加されていても、addDataType() で */* が指定されていると、Chrome からの Android Beam は type が null なので #950 に入ってしまいます。

よって、type が */* の IntentFilter と scheme が http の IntentFilter の2つを指定する必要があるのです。



0 件のコメント:

コメントを投稿