Hatena::ブログ(Diary)

gae+eyoの日記

2017年10月23日(月)

[] FileProviderの<external-cache-path>の罠

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] だけど...

f:id:gae:20171023130005p:image:w360

端末に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 での話。

2017年10月4日(水)

[] 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を入れてしまっていたために、ここでクラッシュしていた。

2015年2月24日(火)

[] Androidをマウスで操作したときのMotionEventのメモ

API Level 14 から MotionEvent#getButtonState() でどのボタンが押されたか取得できる。

ボタンを押して、マウスを移動して、ボタンを放したときのイベント。

操作actionbuttonState
左ボタンを押すACTION_DOWNBUTTON_PRIMARY
マウス移動ACTION_MOVEBUTTON_PRIMARY
左ボタンを放すACTION_UP0

2つのボタンを同時に押したときのイベント。2つ押してもACTION_DOWNとACTION_UPは1回だけ。

操作actionbuttonState
左ボタンを押すACTION_DOWNBUTTON_PRIMARY
右ボタンを押すACTION_MOVEBUTTON_PRIMARY+BUTTON_SECONDARY
マウス移動ACTION_MOVEBUTTON_PRIMAR+BUTTON_SECONDARY
左ボタンを放すACTION_MOVEBUTTON_SECONDARY
右ボタンを放すACTION_UP0

マウスのhoverなどは API Level 12 から、View#setOnGenericMotionListener() が追加されているのでこれで捕まえられる。

2013年8月1日(木)

[] 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());

2012年12月13日(木)

[] Android 4.0〜4.2 でTextViewの singleLine とか maxLines を設定するとテキストや背景がずれて、テキストが上寄りに見える

ボタンのテキストが上に寄っているマヌケなアプリを見かける理由はフォントの問題だと思っていたけど、違っていたみたい。

singleLine=true とか maxLines で行数を指定すると、テキストや background の位置がずれて困る。

f:id:gae:20121213233055p:image

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

わけがわからない...。

Connection: close