FileProviderのの罠
FileProvider.java
if (TAG_EXTERNAL_CACHE.equals(tag)) { File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context); if (externalCacheDirs.length > 0) { target = externalCacheDirs[0]; } }
file_paths.xml に設定された external-cache-path の情報を登録するとき、getExternalCacheDirs()[0] を登録している。
通常は getExternalCacheDir() == getExternalCacheDirs()[0] だけど...
端末にSDカードがささっていて、デフォルトの保存先の設定(よくわからんけどHuaweiの端末にはある)が「SDカード」に設定されていてると、getExternalCacheDir() の値は getExternalCacheDirs()[1] の値になる。
なので getExternalCacheDir() で取得したパスで FileProvider#getUriForFile() を呼び出すと、
Caused by java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/0123-4567/Android/data/com.example.externalcachedirtest/cache/file.tmp
となる。
まとめ。
getExternalCacheDirs()[0]: /storage/emulated/0/Android/data/jp.syoboi.externalcachedirtest/cache
getExternalCacheDirs()[1]: /storage/C8F7-13FD/Android/data/jp.syoboi.externalcachedirtest/cache
で、デフォルトの内部ストレージだと
getExternalCacheDir(): /storage/emulated/0/Android/data/jp.syoboi.externalcachedirtest/cache
保存先がSDカードだと
getExternalCacheDir(): /storage/C8F7-13FD/Android/data/jp.syoboi.externalcachedirtest/cache
external-cache-dir は getExternalCacheDirs()[0]
これは com.android.support:support-v4:26.1.0 での話。
Android 8.0 で java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
Previewのときからクラッシュレポートが来ていたけど、何が原因かわからず困っていた件が、やっと解決したので超久しぶりに日記に。
問題のエラー。
Fatal Exception: java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0 at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:1314) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:680) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:672) at android.view.accessibility.AccessibilityNodeInfo.setText(AccessibilityNodeInfo.java:2474) at android.widget.TextView.onInitializeAccessibilityNodeInfoInternal(TextView.java:10357) at android.view.View.onInitializeAccessibilityNodeInfo(View.java:7307) at android.view.View.createAccessibilityNodeInfoInternal(View.java:7266) at android.view.View.createAccessibilityNodeInfo(View.java:7251) at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:146) at android.view.accessibility.AccessibilityRecord.setSource(AccessibilityRecord.java:119) at android.view.View.onInitializeAccessibilityEventInternal(View.java:7203) at android.widget.TextView.onInitializeAccessibilityEventInternal(TextView.java:10338) at android.view.View.onInitializeAccessibilityEvent(View.java:7191) at android.view.View.sendAccessibilityEventUncheckedInternal(View.java:7053) at android.view.View.sendAccessibilityEventUnchecked(View.java:7038) at android.view.View$SendViewStateChangedAccessibilityEvent.run(View.java:26026) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
エラーが起こっている AccessibilityNodeInfo.java の中身。
public void setText(CharSequence text) { enforceNotSealed(); mOriginalText = text; // Replace any ClickableSpans in mText with placeholders if (text instanceof Spanned) { ClickableSpan[] spans = ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); if (spans.length > 0) { Spannable spannable = new SpannableStringBuilder(text); for (int i = 0; i < spans.length; i++) { ClickableSpan span = spans[i]; if ((span instanceof AccessibilityClickableSpan) || (span instanceof AccessibilityURLSpan)) { // We've already done enough break; } int spanToReplaceStart = spannable.getSpanStart(span); int spanToReplaceEnd = spannable.getSpanEnd(span); int spanToReplaceFlags = spannable.getSpanFlags(span); spannable.removeSpan(span); ClickableSpan replacementSpan = (span instanceof URLSpan) ? new AccessibilityURLSpan((URLSpan) span) : new AccessibilityClickableSpan(span.getId()); spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd, spanToReplaceFlags); } mText = spannable; return; } } mText = (text == null) ? null : text.subSequence(0, text.length()); }
text の ClickableSpan を AccessibilityClickableSpan に置き換えて返しているのだが、spannable.getSpanStart(span) の span が存在しないので -1 が返ってきてクラッシュしていた。
SpannableString と SpannableStringBuilder のわかりにくい違いとして、SpannableStringBuilder は NoCopySpan がコピーされないという違いがあるけど、これは関係なかった。
もう一つわかりにくい違いがあって、SpannableStringBuilder は setSpan() で長さ 0 の Span を設定しようとすると、設定されずに捨てられるということ。
SpannableStringBuilder#setSpan() で長さ0のClickableSpanを入れてしまっていたために、ここでクラッシュしていた。
Androidをマウスで操作したときのMotionEventのメモ
API Level 14 から MotionEvent#getButtonState() でどのボタンが押されたか取得できる。
ボタンを押して、マウスを移動して、ボタンを放したときのイベント。
操作 | action | buttonState |
---|---|---|
左ボタンを押す | ACTION_DOWN | BUTTON_PRIMARY |
マウス移動 | ACTION_MOVE | BUTTON_PRIMARY |
左ボタンを放す | ACTION_UP | 0 |
2つのボタンを同時に押したときのイベント。2つ押してもACTION_DOWNとACTION_UPは1回だけ。
操作 | action | buttonState |
---|---|---|
左ボタンを押す | ACTION_DOWN | BUTTON_PRIMARY |
右ボタンを押す | ACTION_MOVE | BUTTON_PRIMARY+BUTTON_SECONDARY |
マウス移動 | ACTION_MOVE | BUTTON_PRIMAR+BUTTON_SECONDARY |
左ボタンを放す | ACTION_MOVE | BUTTON_SECONDARY |
右ボタンを放す | ACTION_UP | 0 |
マウスのhoverなどは API Level 12 から、View#setOnGenericMotionListener() が追加されているのでこれで捕まえられる。
Bundle の putSerializable() で List や Map や CharSequence を実装したオブジェクトを渡すのはやばい
今まで気付かなかった...orz
Bundle の putSerializable() で List や Map や CharSequence を実装したオブジェクトを渡すと、永続化するときに元がどんなクラスであろうが List を実装していれば ArrayList に、Map を実装していればは HashMap に、CharSequence を実装していれば String になってしまう...。(Parcel.java の writeValue() あたり)
public class MainActivity extends Activity { public static class OreMap extends HashMap<String,Object> { } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { Object oreMap = savedInstanceState.getSerializable("oreMap"); Log.v("", "oreMap: " + oreMap.getClass().getName()); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable("oreMap", new OreMap()); } }
アプリ起動後にkillしてもう一度アプリ起動したときの結果。
oreMap: java.util.HashMap
OreMap が HashMap に変わってしまう。
Bundle が永続化されるまでは、Bundle は単純なマップでオブジェクトを持ってるだけだから問題は起こらないけど、killされて戻ってきたときなんかに、getSerializable() すると他のクラスに化けていて、ClassCastException など起こしたり。
ダサいけど Object [] にして回避するのが手っ取り早い...。
outState.putSerializable("oreMap", new Object [] { new OreMap() });
Object oreMap = ((Object[]) savedInstanceState.getSerializable("oreMap"))[0]; Log.v("", "oreMap: " + oreMap.getClass().getName());
Android 4.0〜4.2 でTextViewの singleLine とか maxLines を設定するとテキストや背景がずれて、テキストが上寄りに見える
ボタンのテキストが上に寄っているマヌケなアプリを見かける理由はフォントの問題だと思っていたけど、違っていたみたい。
singleLine=true とか maxLines で行数を指定すると、テキストや background の位置がずれて困る。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="LinearLayoutで横に並べる \n(右がsingleLine=true)" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#8ff" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="あうあ" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:text="あうあ" /> </LinearLayout> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="RelativeLayoutで横に並べる" /> <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#ff8" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="あうあ" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@+id/button1" android:singleLine="true" android:text="あうあ" /> </RelativeLayout> </LinearLayout>
わけがわからない...。
内部ストレージとSystemストレージ
内部ストレージが1GBくらいあれば、ゲームとかしないかぎり不足することは無いですな! と思っていたら容量不足に。
「設定→アプリ」を見ると 476MB の空き容量があるように見えるけど、「設定→microSDと端末容量」を見ると空き容量は 114MB という表示。
わかりにくいですな。
Quick System Infoでキャッシュをまとめて消すと350MBほど確保できた。これでアップデートも無事にできた。
ブラウザのキャッシュはすぐに数百MBになってしまうので、内部ストレージが1GBくらいあっても結構簡単に無くなってしまいますな。標準ブラウザとChromeを使い分けたりしているとキャッシュも別々だから、かなり容量くってる。
Android 4.0以降でテキストの表示がとても遅くなる件
Android 4.0でハードウェアアクセラレーションが有効な状態で、たくさんの文字種を表示するとパフォーマンスが激しく悪くなる件。
テスト用のAPK。Android 4.0以降用で試してください。(4.0未満だと意味がありません)
0〜100行目までは滑らかにスクロールします。
100〜200行目まではスクロールがものすごく遅くなります。(Nexus 7で10fps以下)
再現するためのコードはこんな感じです。
AndroidManifest.xml。
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="16" />
Activityのコード。
0〜100行目は各行1種類の文字だけを表示。
100〜200行目は1行に50種類の文字を表示しています。
public class ListViewTestActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ListView listView = new ListView(this); setContentView(listView); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1); char x = 0x4E9C; // 100行目までは1行に1種類の文字を100文字表示 for (int j=0; j<100; j++) { String s = ""; for (int k=0; k<100; k++) { s += Character.toString(x); } adapter.add(s); x++; } // 100行目以降は1行に50種類の文字を50文字表示 for (int j=0; j<100; j++) { String s = ""; for (int k=0; k<50; k++) { s += Character.toString(x); x++; } adapter.add(s); } listView.setAdapter(adapter); } }
ハードウェアアクセラレーションを無効にすると、30fps程度はでます。
<activity android:name=".ListViewTestActivity" android:hardwareAccelerated="false" android:label="@string/app_name" >
誰か回避方法を知っていたら教えてください。
これは極端なサンプルですが、日本語表示してるとこれに近い状況がよく発生してパフォーマンスが悪くなったりするんですよね。
たぶん英語みたいに文字が少ない環境だと、ぜんぜん問題ないんだと思いますが。