LocationManagerでAbstractMethodErrorが発生する件


現在、Androidで位置情報を取得する方法としては、FusedLocationProviderClientを使用するのが一般的かと思いますが、一部のGMSの使えない環境も考慮してLocationManagerを使用する場合もあるかと思います。このLocationManagerでAbstractMethodErrorが発生するという現象に遭遇したので書いておきます。

何が起こるのか

パーミッションの取得とか諸々置いておいて、位置情報を取得しようとするとLocationManagerのインスタンスを取得して、requestLocationUpdatesにパラメータ付きでLocationListenerを登録します。
位置情報が取得できればonLocationChangedがコールされるので、ここでlocationを使ってごにょごにょします。

val manager: LocationManager = getSystemService()!!
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1, 1f, object: LocationListener {
    override fun onLocationChanged(location: Location) {
        // locationの利用
    }
})

最後のリスナーはラムダにすることもできます。(AndroidStudioではサジェストが出ますね)

val manager: LocationManager = getSystemService()!!
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1, 1f) {
}

はい、Android Studioの補完機能を使ってなんの問題も無く実装できますし、なんのエラーも出ません。
では実行しましょう。

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.android.myapplication, PID: 3391
    java.lang.AbstractMethodError: abstract method "void android.location.LocationListener.onStatusChanged(java.lang.String, int, android.os.Bundle)"
        at android.location.LocationManager$ListenerTransport._handleMessage(LocationManager.java:304)
        at android.location.LocationManager$ListenerTransport.-wrap0(LocationManager.java)
        at android.location.LocationManager$ListenerTransport$1.handleMessage(LocationManager.java:242)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

oh...

どういうことか?

LocationListnerの実装を見てみましょう。(コメントなどは省略)

LocationListener.java
public interface LocationListener {
    void onLocationChanged(@NonNull Location location);
    @Deprecated
    default void onStatusChanged(String provider, int status, Bundle extras) {}
    default void onProviderEnabled(@NonNull String provider) {}
    default void onProviderDisabled(@NonNull String provider) {}
}

ラムダ化できるのでメソッド一つの関数型インターフェースのように見えますが、4つのメソッドがあり、onLocationChanged以外はdefaultで空実装されています。
しかし、defaultが使えるのはJava8以降ですね。ちょっとコードを遡ってみましょう。
(実は前述のエラーを発生させたのはAndroid7の環境です)

Android 10と11の変化

といってもそれほど遡る必要は無かったですね。Android10の時点のソースコードを見てみます。以下のようにdefaultはついていません。

LocationListener.java
public interface LocationListener {
    void onLocationChanged(Location location);
    @Deprecated
    void onStatusChanged(String provider, int status, Bundle extras);
    void onProviderEnabled(String provider);
    void onProviderDisabled(String provider);
}

Android11でdefaultが追加されていますね。

interfaceにdefaultがあるので、onLocationChanged以外の実装を行わなくてもコンパイルが通りました。しかし、LocationListenerは実行環境が提供するクラスです。つまり、Andorid10以下では、実行環境側のクラスにdefaultの実装が存在しないため、AbstructMethodのままになっていて、そのメソッドがコールされてしまったために、AbstractMethodErrorが発生したというわけですね。
特にonStatusChangedはAndroid10からdeprecatedになっていて、default実装が提供されているので、もともと実装されていても、警告の修正などの際につい削除してしまいかねないメソッドですね。

まとめ

実行環境提供のインターフェースに途中からdefault実装が追加されてしまったために、メソッドをすべて実装しなくともコンパイルが通るようになってしまったが、下位バージョンでの実行時はdefault実装が存在しないために問題が発生してしまっていました。

通常利用しないメソッドをdefaultで空実装しておくのは良いことかもしれませんが、実行環境のクラスでそれをやられると、Androidの用に複数バージョンでの動作をさせないといけない場合、問題が起こってしまいますね。
正直この変更は失敗で、API/interfaceを新設するなりJetpack等の外部ライブラリで対応すべき修正だったと思います。

Androidのシステムクラスを使う場合にdefault実装を見かけたら注意しましょう。ぐらいしか言いようがありませんが、このような変更が他にないことを祈るばかりです。

以上です。