2013年10月24日木曜日

2.x と 3.x+ の Recent Apps からの起動は Intent に付く Flag が異なる

Suica Reader で試した結果なので、launchMode の設定によって変わってくるかもしれません。
Suica Reader では launchMode に singleTask を設定しています。

■ 2.3.6 (Nexus S)
NfcAdapter.ACTION_TECH_DISCOVERED による起動
flag = 268435456 = 0x10000000
FLAG_ACTIVITY_NEW_TASK

上記のあと、Recent Apps から起動
flag = 269484032 = 0x10100000
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY


■ 4.3 (初代 Nexus 7)
NfcAdapter.ACTION_TECH_DISCOVERED による起動
flag = 0

上記のあと、Recent Apps から起動
flag = 269500416 = 0x10104000
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY | FLAG_ACTIVITY_TASK_ON_HOME


2.3.6 との違いは FLAG_ACTIVITY_TASK_ON_HOME です。この Flag は API Level 11 からです。

2.3.6 と 4.3 では NfcAdapter.ACTION_TECH_DISCOVERED による起動時の Flag も違っていました。

Recent から起動したときの Intent は前回起動したときの Intent になります。
つまり、カードをかざして起動したあと、Recent から起動すると、カードをかざしていないのに Intent の Action は NfcAdapter.ACTION_NDEF_DISCOVEREDNfcAdapter.ACTION_TECH_DISCOVERED などになります。
NDEF データは Intent に保持されるので、それを利用する場合は問題ないのですが、カード検出後にカードと通信して直接データを読み取る場合は困ります。
カードがかざされたことによる起動なのか、Recent からの起動なのか調べるために Flag が 0 より大きいかどうかでチェックしていたのですが、2.3.6 では NfcAdapter.ACTION_TECH_DISCOVERED による起動時の Flag が 0 ではないことがわかってしまい、ちゃんと FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY をチェックしないとダメでしたね。。。



2013年10月23日水曜日

AppCompat は Dialog をサポートしていない

AppCompat には Dialog 系のテーマがありません。

AppCompat で用意されているテーマ
  • Theme.AppCompat
  • Theme.AppCompat.Light
  • Theme.AppCompat.Light.DarkActionBar


次のように自分でダイアログのテーマを作ることはできます。
android:windowBackground に指定する画像(下記だと @drawable/dialog_full_holo_light)を platform から取ってこないといけないですが。 <resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="AppBaseTheme" parent="Theme.AppCompat.Light"></style> <style name="AppTheme" parent="AppBaseTheme"> <item name="android:windowBackground">@drawable/bg</item> </style> <style name="AppTheme.Dialog"> <item name="android:windowFrame">@null</item> <item name="android:windowBackground">@drawable/dialog_full_holo_light</item> <item name="android:windowIsFloating">true</item> <item name="android:windowContentOverlay">@null</item> <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item> <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> <item name="android:windowActionModeOverlay">true</item> <item name="android:windowCloseOnTouchOutside">false</item> </style> </resources> ただし、 <item name="android:windowActionBar">true</item> <item name="android:windowNoTitle">false</item> とすると、IllegalStateException が発生します。 java.lang.IllegalStateException: ActionBarImpl can only be used with a compatible window decor layout つまり、Dialog で AppCompat の ActionBar は利用できないようになっている、ということです。


なので、View で実装した DialogFragment を用意することになるのかな。 public class SimpleDialogFragment extends DialogFragment { public static SimpleDialogFragment getInstance(String title, String message) { Bundle args = new Bundle(); args.putString("title", title); args.putString("message", message); SimpleDialogFragment f = new SimpleDialogFragment(); f.setArguments(args); return f; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle args = getArguments(); String title = args.getString("title"); String message = args.getString("message"); View view = LayoutInflater.from(getActivity()) .inflate(R.layout.fragment_dialog, null); TextView tv; // title tv = (TextView) view.findViewById(R.id.title); tv.setText(title); // message tv = (TextView) view.findViewById(R.id.message); tv.setText(message); Dialog dialog = new Dialog(getActivity(), R.style.AppTheme_Dialog); dialog.setContentView(view); return dialog; } } <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > <TextView android:id="@+id/title" style="?attr/actionBarStyle" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:gravity="center_vertical" android:paddingLeft="16dp" android:paddingRight="16dp" android:textAppearance="?android:attr/textAppearanceMedium" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/message" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dip" android:autoLink="web|email" android:linksClickable="true" /> </ScrollView> </LinearLayout>



# ちなみに ActionBar Sherlock は Dialog サポートしてます

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つを指定する必要があるのです。



2013年10月22日火曜日

マルチユーザーのデータベースパスは /data/user/xx/[PACKAGE_NAME]/databases/[DATABASE_NAME]

昔々は
/data/data/[PACKAGE_NAME]/databases/[DATABASE_NAME]
だったのですが、マルチユーザーだとタイトルみたいなパスになります。

ContextWrapper の getDatabasePath() を使っていればちゃんと適切なパスを返してくれます。

/data/data/[PACKAGE_NAME]/databases/[DATABASE_NAME] はオーナーユーザーだと普通に動くっぽいのですが、サブユーザーはこのディレクトリに書込み権限がないので、書込もうとするとエラーになります。



2013年10月21日月曜日

AAR(Android Application Record)メモ

Android以外から Android Beam 送るときに必要なのでメモ。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/nfc/NdefRecord.java#309 139 public static final short TNF_EXTERNAL_TYPE = 0x04; 223 public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes(); 309 public static NdefRecord createApplicationRecord(String packageName) { 310 if (packageName == null) throw new NullPointerException("packageName is null"); 311 if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty"); 312 313 return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, 314 packageName.getBytes(Charsets.UTF_8)); 315 }



2013年10月19日土曜日

ActionBar のタブの高さは 48dp 以上にできない

ActionBar のサイズは 48dp 以上にできるのになー。。。

ActionBar のタブは ScrollingTabContainerView です。
このコンストラクタで setContentHeight() を呼んで高さを設定しています。
高さには ActionBarPolicy の getTabContainerHeight() の値を指定しています。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/com/android/internal/widget/ScrollingTabContainerView.java#71 71 public ScrollingTabContainerView(Context context) { 72 super(context); 73 setHorizontalScrollBarEnabled(false); 74 75 ActionBarPolicy abp = ActionBarPolicy.get(context); 76 setContentHeight(abp.getTabContainerHeight()); 77 mStackedTabMaxWidth = abp.getStackedTabMaxWidth(); 78 79 mTabLayout = createTabLayout(); 80 addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 81 ViewGroup.LayoutParams.MATCH_PARENT)); 82 } 83 ActionBarPolicy の getTabContainerHeight() では、android:actionBarStyle の android:height と R.dimen.action_bar_stacked_max_height の最小値を返しています。
つまり、タブの高さは R.dimen.action_bar_stacked_max_height より大きくならないということです。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/com/android/internal/view/ActionBarPolicy.java#65 65 public int getTabContainerHeight() { 66 TypedArray a = mContext.obtainStyledAttributes(null, R.styleable.ActionBar, 67 com.android.internal.R.attr.actionBarStyle, 0); 68 int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0); 69 Resources r = mContext.getResources(); 70 if (!hasEmbeddedTabs()) { 71 // Stacked tabs; limit the height 72 height = Math.min(height, 73 r.getDimensionPixelSize(R.dimen.action_bar_stacked_max_height)); 74 } 75 a.recycle(); 76 return height; 77 } R.dimen.action_bar_stacked_max_height は 48dp なので、これより大きくできません。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/res/res/values/dimens.xml#229 229 <!-- Maximum height for a stacked tab bar as part of an action bar --> 230 <dimen name="action_bar_stacked_max_height">48dp</dimen>


2013年10月3日木曜日

Android NdefRecord.createMime() をバックポートするときの注意点

API Level 16 から NdefRecord に createMime() というメソッドが追加されています。

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/nfc/NdefRecord.java#409 public static NdefRecord createMime(String mimeType, byte[] mimeData) { if (mimeType == null) throw new NullPointerException("mimeType is null"); // We only do basic MIME type validation: trying to follow the // RFCs strictly only ends in tears, since there are lots of MIME // types in common use that are not strictly valid as per RFC rules mimeType = normalizeMimeType(mimeType); if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); int slashIndex = mimeType.indexOf('/'); if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); if (slashIndex == mimeType.length() - 1) { throw new IllegalArgumentException("mimeType must have minor type"); } // missing '/' is allowed // MIME RFCs suggest ASCII encoding for content-type byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII); return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, typeBytes, null, mimeData); } このメソッドをバックポートするときに、そのままコードを移植すると実行時に IllegalArgumentException が起こる場合があります。

4.3 の HTC One では起こりませんでしたが、4.0.4 の ARROWS V では起こりました。
(Jelly Bean なら起こらない?) 10-03 19:08:40.622: E/AndroidRuntime(2218): java.lang.IllegalArgumentException: Illegal null argument 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.os.Parcel.readException(Parcel.java:1351) 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.os.Parcel.readException(Parcel.java:1301) 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.nfc.INdefPushCallback$Stub$Proxy.createMessage(INdefPushCallback.java:95) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.nfc.P2pLinkManager.prepareMessageToSend(P2pLinkManager.java:241) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.nfc.P2pLinkManager.onLlcpActivated(P2pLinkManager.java:206) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.nfc.NfcService$NfcServiceHandler.llcpActivated(NfcService.java:1845) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.nfc.NfcService$NfcServiceHandler.handleMessage(NfcService.java:1718) 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.os.Handler.dispatchMessage(Handler.java:99) 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.os.Looper.loop(Looper.java:137) 10-03 19:08:40.622: E/AndroidRuntime(2218): at android.app.ActivityThread.main(ActivityThread.java:4479) 10-03 19:08:40.622: E/AndroidRuntime(2218): at java.lang.reflect.Method.invokeNative(Native Method) 10-03 19:08:40.622: E/AndroidRuntime(2218): at java.lang.reflect.Method.invoke(Method.java:511) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784) 10-03 19:08:40.622: E/AndroidRuntime(2218): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551) 10-03 19:08:40.622: E/AndroidRuntime(2218): at dalvik.system.NativeStart.main(Native Method) この現象を避けるには、NdefRecord のコンストラクタの第3引数を null から new byte[0] に変えます。 public static NdefRecord createMime(String mimeType, byte[] mimeData) { ... return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, typeBytes, new byte[0], mimeData); }



2013年10月1日火曜日

Android GoogleCloudMessaging.register() はUIスレッドで呼んではいけない

public static String getRegistrationId(Context context) { GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context); String registrationId = null; try { registrationId = gcm.register(Consts.PROJECT_NUMBER); } catch (IOException e) { e.printStackTrace(); } return registrationId; } を呼んでるんだけど null が返ってくる、なぜだ。。。と思っていたら、IOException が発生していた。。。 10-01 18:17:18.489: W/System.err(6397): java.io.IOException: MAIN_THREAD 10-01 18:17:18.489: W/System.err(6397): at com.google.android.gms.gcm.GoogleCloudMessaging.register(Unknown Source) UIスレッドで呼んじゃいけないのか。リファレンスに書いておいてほしかったなー。。。

と思ったら register() のリファレンスには書いてないのに、なぜかエラーコードのリファレンスにだけ書いてあるとか。ううう。