2015/01/27

ActionBarActivity#setSupportActionBar() で NoClassDefFoundError が発生する端末がある

私が Google Play で公開しているアプリのクラッシュ報告に、以下のような Stack Trace が送られてきました。
java.lang.NoClassDefFoundError: android.support.v7.internal.view.menu.MenuBuilder
at android.support.v7.widget.ActionMenuView.getMenu(ProGuard:620)
at android.support.v7.widget.Toolbar.ensureMenu(ProGuard:825)
at android.support.v7.widget.Toolbar.getMenu(ProGuard:817)
at android.support.v7.internal.app.ToolbarActionBar.getMenu(ProGuard:554)
at android.support.v7.internal.app.ToolbarActionBar.setListMenuPresenter(ProGuard:558)
at android.support.v7.app.ActionBarActivityDelegateBase.setSupportActionBar(ProGuard:178)
at android.support.v7.app.ActionBarActivity.setSupportActionBar(ProGuard:92)
at com.kokufu.android.apps.divelogbook.MainActivity.onCreate(ProGuard:84)
at android.app.Activity.performCreate(Activity.java:5122)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1150)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2315)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2403)
at android.app.ActivityThread.access$600(ActivityThread.java:165)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1373)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5391)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
at dalvik.system.NativeStart.main(Native Method)

原因

調べてみると、Samsung やその他一部の端末で発生するバグらしい。

対策

これ、appcompat-v7 のバグではなく、OS のバグだと思われます。 OS に古い appcompat-v7 が組み込まれているのが問題ではないかと。
つまり、アプリ側ではどうしようもない。 …と諦めかけてたのですが、ProGuard を使って回避するナイスなアイデアが!

難読化をしない場合、以下の一行を ProGuard の設定ファイルに記述すればオッケーです。
-keep class !android.support.v7.internal.view.menu.**,** {*;}

他にも難読化したい場合には、以下の方が無難でしょう。
-keep class !android.support.v7.internal.view.menu.**,android.support.v7.** { *; }
-keep interface !android.support.v7.internal.view.menu.**,android.support.v7.** { *; }

出力された mapping ファイルを確認し、MenuBuilder の名前が変わっていることを確認しておきます。

build/outputs/mapping/release/mapping.txt
android.support.v7.internal.view.menu.MenuBuilder -> android.support.v7.internal.view.menu.i:

私の手元には、この問題が起こる端末が無いため、動作確認出来ていません。
名前変えただけで、class path の問題を回避できるんだか、ちょっと不安です。
もし、確認された方がいらっしゃいましたら、是非教えてください。
2015/01/20

Android 5.0 で LVL を使うと IllegalArgumentException が発生する

Android Developer としては大分遅いほうだと思いますが、最近やっと手持ちの Nexus 7 を Android 5.0 にしました。
そこで、LVL を適用したアプリを立ち上げてみたところ、以下のようなエラーが。

Caused by: java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.android.vending.licensing.ILicensingService }
       at android.app.ContextImpl.validateServiceIntent(ContextImpl.java:1674)
       at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:1773)
       at android.app.ContextImpl.bindService(ContextImpl.java:1751)
       at android.content.ContextWrapper.bindService(ContextWrapper.java:538)
       at com.google.android.a.a.i.a(ProGuard:150)
       at com.kokufu.android.apps.sqliteviewer.MainActivity.l(ProGuard:69)
       at com.kokufu.android.apps.sqliteviewer.MainActivity.onCreate(ProGuard:65)
       at android.app.Activity.performCreate(Activity.java:5933)
       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1105)
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2251)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
       at android.app.ActivityThread.access$800(ActivityThread.java:144)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:135)
       at android.app.ActivityThread.main(ActivityThread.java:5221)
       at java.lang.reflect.Method.invoke(Native Method)
       at java.lang.reflect.Method.invoke(Method.java:372)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

調べてみると、バグらしい

結構、重大なバグだと思うのですが、これまで放って置かれているのは、LVL がソース提供だからでしょうか少なくとも 2015/1/20 現在の Google Play Licensing Library Rev. 2 では修正されていません。
リンクにもあるとおり、以下のように LVL の中身を修正することで問題は解決します。

LicenseChecker.java (修正前)
                    boolean bindResult = mContext
                            .bindService(
                                    new Intent(
                                            new String(
                                                    Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
                                    this, // ServiceConnection.
                                    Context.BIND_AUTO_CREATE);

                    if (bindResult) {
                        mPendingChecks.offer(validator);
                    } else {
                        Log.e(TAG, "Could not bind to service.");
                        handleServiceConnectionError(validator);
                    }

LicenseChecker.java (修正後)
                    Intent serviceIntent = new Intent(
                            new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")));
                    serviceIntent.setPackage("com.android.vending");

                    boolean bindResult = mContext
                            .bindService(
                                    serviceIntent,
                                    this, // ServiceConnection.
                                    Context.BIND_AUTO_CREATE);


2014/12/20

Google Maps Android API v2 の Marker は Drag 後に Position がアップデートされない

Google Maps Android API v2 では以下のようにドラッグ可能な Marker を作ることができます。

Marker marker = mGoogleMap.addMarker(
        new MarkerOptions()
                .position(location)
                .draggable(true));


GoogleMap クラスは Marker の情報を保存しておいてくれないので、上記のように作成した Marker のインスタンスは何らかの形で自分で保持しておかないといけないという制約があります。
ところが、Marker をドラッグ後に Marker#getPosition() を呼んでも、ドラッグ前の Position しか取得できません。 「ドラッグした」というイベントを捕まえて、自分で更新しないといけないようですちょっとイマイチな仕様だと思うのは私だけでしょうか…?


具体的には以下のような実装で、ドラッグ後のインスタンスに差し替えればよいかと思います。
// Marker を保持するための Map
private Map<String, Marker> mMarkers = new HashMap<>();

@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    mMapView.getMapAsync(new OnMapReadyCallback() {
        @Override
        public void onMapReady(GoogleMap googleMap) {
            mMap = googleMap;

            // ドラッグ検知用のリスナ登録
            googleMap.setOnMarkerDragListener(mOnMarkerDragListener);

            // 地図上に Marker を表示し、mMarkers に保持する
            for (LatLng location : mLocations) {
                Marker marker = googleMap.addMarker(
                        new MarkerOptions()
                                .position(location)
                                .draggable(true));
                mMarkers.put(marker.getId(), marker);
            }
        }
    });
}

private final GoogleMap.OnMarkerDragListener mOnMarkerDragListener = 
        new GoogleMap.OnMarkerDragListener() {
    @Override
    public void onMarkerDragStart(Marker marker) {
    }

    @Override
    public void onMarkerDrag(Marker marker) {
    }

    @Override
    public void onMarkerDragEnd(Marker marker) {
        // ドラッグが終わったら、インスタンスを差し替える
        mMarkers.put(marker.getId(), marker);
    }
};


2014/12/11

Google Play Services 6.5 でライブラリが分割されたらしいけど、今のところ正しく動作しない様子

2014/12/28 追記
2014/12/28 現在、この問題は既に解決済みです。
どうも、play-services-base への依存関係が各ライブラリに記述されていなかったようです。

表題のように Google Play Services 6.5 でライブラリが分割されたとのこと。
これまでは、Map が使いたいだけなのに Ads とか Drive とかの API も同梱しないといけなかったのが、個別に別れたわけですね。

というわけで、早速使ってみたのですが、残念ながらうまく動作しませんでした。

Setting Up Google Play Services | Android Developers によると

build.gradle
dependencies {
    compile 'com.google.android.gms:play-services:6.5.+'
}



dependencies {
    compile 'com.google.android.gms:play-services-maps:6.5.+'
    compile 'com.google.android.gms:play-services-ads:6.5.+'
}

みたいに書き換えるとオッケーと書いてあるのですが、実際にそのようにして実行してみると

Caused by: java.lang.NoSuchFieldError: com.google.android.gms.R$styleable.AdsAttrs
       at com.google.android.gms.internal.bb.(Unknown Source)
       at com.google.android.gms.internal.bh.(Unknown Source)
       at com.google.android.gms.internal.bh.(Unknown Source)
       at com.google.android.gms.internal.bh.(Unknown Source)
       at com.google.android.gms.ads.AdView.(Unknown Source)
       at java.lang.reflect.Constructor.constructNative(Native Method)
       at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
       at android.view.LayoutInflater.createView(LayoutInflater.java:586)
       at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:680)
       at android.view.LayoutInflater.rInflate(LayoutInflater.java:739)
       at android.view.LayoutInflater.rInflate(LayoutInflater.java:742)
       at android.view.LayoutInflater.rInflate(LayoutInflater.java:742)
       at android.view.LayoutInflater.inflate(LayoutInflater.java:489)
       at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
       at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
       at android.support.v7.app.ActionBarActivityDelegateBase.setContentView(ActionBarActivityDelegateBase.java:228)
       at android.support.v7.app.ActionBarActivity.setContentView(ActionBarActivity.java:102)
       at com.kokufu.test.MainActivity.onCreate(MainActivity.java:84)
       at android.app.Activity.performCreate(Activity.java:4465)
       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1931)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1992)
       at android.app.ActivityThread.access$600(ActivityThread.java:127)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1158)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:137)
       at android.app.ActivityThread.main(ActivityThread.java:4441)
       at java.lang.reflect.Method.invokeNative(Native Method)
       at java.lang.reflect.Method.invoke(Method.java:511)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:823)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:590)
       at dalvik.system.NativeStart.main(Native Method)

NoSuchFieldError が出てしまいました。
明らかに分割ミスであると思われます。うーん残念。

この問題、力技で回避する強者もいるみたいですが、
これまで通り全部コミコミにしておけば(以下)正常に動作しますので、修正されるのを待つのが得策かと思います。

build.gradle
dependencies {
    compile 'com.google.android.gms:play-services:6.5.+'
}
2014/10/05

AndroidStudio でビルドしたら MissingTranslation エラーが出た時の対処方法

Android Studio で Build → Generate Signed APK... とすると、以下のようなエラーが出てビルドできませんでした。

Error:(4) Error: "app_name" is not translated in "ja" (Japanese) [MissingTranslation]

「app_name を日本語に翻訳していないよ」というエラーなので、意味はわかります。
しかし、これはあえてなんです。全部の文言を日本語に翻訳するのは現実的ではありません。
せめて Warning にしておいてくれればいいのに。
きっと、これを Error にしたのは実際には Translation をあまり使わない英語圏の人たちなんでしょうねぇ。

というわけで、回避する方法を調べました。

個別に回避する方法1

以下のように string タグへ個別に translatable="false" をつけていきます。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name"
        translatable="false">Test App</string>



</resources >

ある意味、正攻法と言える方法です。
しかし、多言語展開すると「日本語には翻訳するけどフランス語には翻訳しない」等、単純にいかないことも多そうです。
その場合、フランス語(翻訳しない言語)にも同じ文言を書くのが正しい方法なのだと思いますが、数が増えてくると無駄が多くなり、修正も大変になってきます。

なお、この方法は「本来翻訳しないはずのものを翻訳しちゃった」場合に Warning を出してくれます。

個別に回避する方法2

以下のように string タグへ個別に ignore 属性をつけていきます。

res/values/string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources 
    xmlns:tools="http://schemas.android.com/tools">

    <string name="app_name" 
        tools:ignore="MissingTranslation">Test App</string>



</resources >

この方法は translatable を使った方法と同じように見えますが、「本来翻訳しないはずのものを翻訳しちゃった」場合に何の警告も出してくれないので、translatable を使った方が良いでしょう。

まとめて回避する方法1

以下のように ignore 属性をつける場所を親の resources タグにします。

res/values/string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources 
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="MissingTranslation">

    <string name="app_name">Test App</string>



</resources >

これで、各ファイル毎にまとめて回避できるようになります。
string_no_translate.xml とか別ファイルにしてグループ分けすると便利かもしれません。
それでも多言語展開した時の問題は残るわけですが。

まとめて回避する方法2

どうせ個々やグループ化で回避しても意味が無いのであれば、本当に全体を回避してしまうのも手かもしれませんとはいえ、lint を完全に切ってしまうのはお勧めは出来ません
以下のように、build.gradle に追記します。

app/build.gradle
android {
    compileSdkVersion 20
    buildToolsVersion '20.0.0'

    lintOptions {
        disable 'MissingTranslation'
    }

    // 省略
}

まとめて回避する方法3

以下のように lint.xml をモジュール直下に配置する方法でも回避可能です。

app/lint.xml
<?xml version="1.0" encoding="UTF-8"?>

<lint>

    <issue id="MissingTranslation" severity="ignore" />

</lint>

この方法、本来ならば severity を warning 等に変えることで、警告レベルを変更できるはずですが、現在のところAndroid Studio (Beta) 0.8.6
Android SDK Tools 23.0.2
どう指定しても ignore になってしまうようです。