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 アプリが起動します)。 NfcAdapter mNfcAdapter; @Override protected void onResume() { super.onResume(); if (mNfcAdapter != null) { PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter ndef = IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "*/*"); IntentFilter[] intentFiltersArray = new IntentFilter[] { ndef }; mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null); } } @Override protected void onPause() { super.onPause(); if (mNfcAdapter != null) { mNfcAdapter.disableForegroundDispatch(this); } } 次のように enableForegroundDispatch() の第3引数と第4引数に null を渡すと、ともかく全部拾ってくれるようになります。 @Override protected void onResume() { super.onResume(); if (mNfcAdapter != null) { PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); // catch all ndef mNfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null); } } IntentFilter[] を指定して Chrome の AndroidBeam も受けとるには、次のように scheme を指定した IntentFilter も追加します。 @Override protected void onResume() { super.onResume(); if (mNfcAdapter != null) { PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, getPendingIntent().addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); IntentFilter ndef = IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "*/*"); IntentFilter ndef2 = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED); ndef2.addDataScheme(""); ndef2.addDataScheme("http"); ndef2.addDataScheme("https"); IntentFilter[] intentFiltersArray = new IntentFilter[] { ndef, ndef2 }; mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null); } }


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


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

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 1011 public void enableForegroundDispatch(Activity activity, PendingIntent intent, 1012 IntentFilter[] filters, String[][] techLists) { 1013 if (activity == null || intent == null) { 1014 throw new NullPointerException(); 1015 } 1016 if (!activity.isResumed()) { 1017 throw new IllegalStateException("Foreground dispatch can only be enabled " + 1018 "when your activity is resumed"); 1019 } 1020 try { 1021 TechListParcel parcel = null; 1022 if (techLists != null && techLists.length > 0) { 1023 parcel = new TechListParcel(techLists); 1024 } 1025 ActivityThread.currentActivityThread().registerOnActivityPausedListener(activity, 1026 mForegroundDispatchListener); 1027 sService.setForegroundDispatch(intent, filters, parcel); 1028 } catch (RemoteException e) { 1029 attemptDeadServiceRecovery(e); 1030 } 1031 } 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 837 public void setForegroundDispatch(PendingIntent intent, 838 IntentFilter[] filters, TechListParcel techListsParcel) { 839 mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR); 840 841 // Short-cut the disable path 842 if (intent == null && filters == null && techListsParcel == null) { 843 mNfcDispatcher.setForegroundDispatch(null, null, null); 844 return; 845 } 846 847 // Validate the IntentFilters 848 if (filters != null) { 849 if (filters.length == 0) { 850 filters = null; 851 } else { 852 for (IntentFilter filter : filters) { 853 if (filter == null) { 854 throw new IllegalArgumentException("null IntentFilter"); 855 } 856 } 857 } 858 } 859 860 // Validate the tech lists 861 String[][] techLists = null; 862 if (techListsParcel != null) { 863 techLists = techListsParcel.getTechLists(); 864 } 865 866 mNfcDispatcher.setForegroundDispatch(intent, filters, techLists); 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 81 public synchronized void setForegroundDispatch(PendingIntent intent, 82 IntentFilter[] filters, String[][] techLists) { 83 if (DBG) Log.d(TAG, "Set Foreground Dispatch"); 84 mOverrideIntent = intent; 85 mOverrideFilters = filters; 86 mOverrideTechLists = techLists; 87 } 182 /** Returns false if no activities were found to dispatch to */ 183 public boolean dispatchTag(Tag tag) { 184 NdefMessage message = null; 185 Ndef ndef = Ndef.get(tag); 186 if (ndef != null) { 187 message = ndef.getCachedNdefMessage(); 188 } 189 if (DBG) Log.d(TAG, "dispatch tag: " + tag.toString() + " message: " + message); 190 191 PendingIntent overrideIntent; 192 IntentFilter[] overrideFilters; 193 String[][] overrideTechLists; 194 195 DispatchInfo dispatch = new DispatchInfo(mContext, tag, message); 196 synchronized (this) { 197 overrideFilters = mOverrideFilters; 198 overrideIntent = mOverrideIntent; 199 overrideTechLists = mOverrideTechLists; 200 } 201 202 resumeAppSwitches(); 203 204 if (tryOverrides(dispatch, tag, message, overrideIntent, overrideFilters, overrideTechLists)) { 205 return true; 206 } 207 208 if (mHandoverManager.tryHandover(message)) { 209 if (DBG) Log.i(TAG, "matched BT HANDOVER"); 210 return true; 211 } 212 213 if (tryNdef(dispatch, message)) { 214 return true; 215 } 216 217 if (tryTech(dispatch, tag)) { 218 return true; 219 } 220 221 dispatch.setTagIntent(); 222 if (dispatch.tryStartActivity()) { 223 if (DBG) Log.i(TAG, "matched TAG"); 224 return true; 225 } 226 227 if (DBG) Log.i(TAG, "no match"); 228 return false; 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 231 boolean tryOverrides(DispatchInfo dispatch, Tag tag, NdefMessage message, PendingIntent overrideIntent, 232 IntentFilter[] overrideFilters, String[][] overrideTechLists) { 233 if (overrideIntent == null) { 234 return false; 235 } 236 Intent intent; 237 238 // NDEF 239 if (message != null) { 240 intent = dispatch.setNdefIntent(); 241 if (intent != null && 242 isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 243 try { 244 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 245 if (DBG) Log.i(TAG, "matched NDEF override"); 246 return true; 247 } catch (CanceledException e) { 248 return false; 249 } 250 } 251 } 252 253 // TECH 254 intent = dispatch.setTechIntent(); 255 if (isTechMatch(tag, overrideTechLists)) { 256 try { 257 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 258 if (DBG) Log.i(TAG, "matched TECH override"); 259 return true; 260 } catch (CanceledException e) { 261 return false; 262 } 263 } 264 265 // TAG 266 intent = dispatch.setTagIntent(); 267 if (isFilterMatch(intent, overrideFilters, overrideTechLists != null)) { 268 try { 269 overrideIntent.send(mContext, Activity.RESULT_OK, intent); 270 if (DBG) Log.i(TAG, "matched TAG override"); 271 return true; 272 } catch (CanceledException e) { 273 return false; 274 } 275 } 276 return false; 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 279 boolean isFilterMatch(Intent intent, IntentFilter[] filters, boolean hasTechFilter) { 280 if (filters != null) { 281 for (IntentFilter filter : filters) { 282 if (filter.match(mContentResolver, intent, false, TAG) >= 0) { 283 return true; 284 } 285 } 286 } else if (!hasTechFilter) { 287 return true; // always match if both filters and techlists are null 288 } 289 return false; 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 1068 public final int match(ContentResolver resolver, Intent intent, 1069 boolean resolve, String logTag) { 1070 String type = resolve ? intent.resolveType(resolver) : intent.getType(); 1071 return match(intent.getAction(), type, intent.getScheme(), 1072 intent.getData(), intent.getCategories(), logTag); 1073 } 1103 public final int match(String action, String type, String scheme, 1104 Uri data, Set categories, String logTag) { 1105 if (action != null && !matchAction(action)) { 1106 if (false) Log.v( 1107 logTag, "No matching action " + action + " for " + this); 1108 return NO_MATCH_ACTION; 1109 } 1110 1111 int dataMatch = matchData(type, scheme, data); 1112 if (dataMatch < 0) { 1113 if (false) { 1114 if (dataMatch == NO_MATCH_TYPE) { 1115 Log.v(logTag, "No matching type " + type 1116 + " for " + this); 1117 } 1118 if (dataMatch == NO_MATCH_DATA) { 1119 Log.v(logTag, "No matching scheme/path " + data 1120 + " for " + this); 1121 } 1122 } 1123 return dataMatch; 1124 } 1125 1126 String categoryMismatch = matchCategories(categories); 1127 if (categoryMismatch != null) { 1128 if (false) { 1129 Log.v(logTag, "No matching category " + categoryMismatch + " for " + this); 1130 } 1131 return NO_MATCH_CATEGORY; 1132 } 1133 1134 // It would be nice to treat container activities as more 1135 // important than ones that can be embedded, but this is not the way... 1136 if (false) { 1137 if (categories != null) { 1138 dataMatch -= mCategories.size() - categories.size(); 1139 } 1140 } 1141 1142 return dataMatch; 1143 } 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 899 public final int matchData(String type, String scheme, Uri data) { 900 final ArrayList types = mDataTypes; 901 final ArrayList schemes = mDataSchemes; 902 final ArrayList authorities = mDataAuthorities; 903 final ArrayList paths = mDataPaths; 904 905 int match = MATCH_CATEGORY_EMPTY; 906 907 if (types == null && schemes == null) { 908 return ((type == null && data == null) 909 ? (MATCH_CATEGORY_EMPTY+MATCH_ADJUSTMENT_NORMAL) : NO_MATCH_DATA); 910 } 911 912 if (schemes != null) { 913 if (schemes.contains(scheme != null ? scheme : "")) { 914 match = MATCH_CATEGORY_SCHEME; 915 } else { 916 return NO_MATCH_DATA; 917 } 918 919 if (authorities != null) { 920 int authMatch = matchDataAuthority(data); 921 if (authMatch >= 0) { 922 if (paths == null) { 923 match = authMatch; 924 } else if (hasDataPath(data.getPath())) { 925 match = MATCH_CATEGORY_PATH; 926 } else { 927 return NO_MATCH_DATA; 928 } 929 } else { 930 return NO_MATCH_DATA; 931 } 932 } 933 } else { 934 // Special case: match either an Intent with no data URI, 935 // or with a scheme: URI. This is to give a convenience for 936 // the common case where you want to deal with data in a 937 // content provider, which is done by type, and we don't want 938 // to force everyone to say they handle content: or file: URIs. 939 if (scheme != null && !"".equals(scheme) 940 && !"content".equals(scheme) 941 && !"file".equals(scheme)) { 942 return NO_MATCH_DATA; 943 } 944 } 945 946 if (types != null) { 947 if (findMimeType(type)) { 948 match = MATCH_CATEGORY_TYPE; 949 } else { 950 return NO_MATCH_TYPE; 951 } 952 } else { 953 // If no MIME types are specified, then we will only match against 954 // an Intent that does not have a MIME type. 955 if (type != null) { 956 return NO_MATCH_TYPE; 957 } 958 } 959 960 return match + MATCH_ADJUSTMENT_NORMAL; 961 }

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

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



0 件のコメント:

コメントを投稿