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" >

誰か回避方法を知っていたら教えてください。


これは極端なサンプルですが、日本語表示してるとこれに近い状況がよく発生してパフォーマンスが悪くなったりするんですよね。
たぶん英語みたいに文字が少ない環境だと、ぜんぜん問題ないんだと思いますが。

追記

Nexus 7Android 4.2 になってからものすごく改善しました。
7fps程度しか出なかったところが60fps出てます。