2024年3月31日日曜日

WorkManager で android.net.ConnectivityManager$TooManyRequestsException が起こった場合、Coil の使い方が良くない場合がある

WorkManager の Worker の Constraints として val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = OneTimeWorkRequestBuilder<MyWorker>() .setConstraints(constraints) .build() のように RequiredNetworkType を指定すると、ConnectivityManager の registerDefaultNetworkCallback() が呼ばれます。

ここで TooManyRequestsException が起こったときに、Coil の使い方が原因になってることがありました。

registerDefaultNetworkCallback() の実装を見ると、同時に 100 を超える NetworkCallback を register すると exception が投げられると書いてあります。 /** * ... * * To avoid performance issues due to apps leaking callbacks, the system will limit the * number of outstanding requests to 100 per app (identified by their UID), ... If this limit is * exceeded, an exception will be thrown. ... */ @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback) { registerDefaultNetworkCallback(networkCallback, getDefaultHandler()); } つまり、どこかで同時に大量に NetworkCallback を登録してるところがあるということです。

それがどこか探したところ、coil の ImageLoader.kt にいる @JvmName("create") fun ImageLoader(context: Context): ImageLoader { return ImageLoader.Builder(context).build() } をリクエストごとに呼び出している箇所が原因でした。

coil では ImageLoader ごとに registerDefaultNetworkCallback() が呼ばれます。

ImageLoader.Builder の build() で RealImageLoader が作られ、NetworkObserver が作られ、 RealNetworkObserver() が作られます。
RealNetworkObserver の init で registerNetworkCallback() しています。 private class RealNetworkObserver( private val connectivityManager: ConnectivityManager, private val listener: Listener ) : NetworkObserver { ... init { val request = NetworkRequest.Builder() .addCapability(NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, networkCallback) } ... } RealNetworkObserver のインスタンスを作るところで try-catch しているので、ここでは問題に気づかず、WorkManager の方で発覚したということでした。 internal fun NetworkObserver( context: Context, listener: Listener, logger: Logger? ): NetworkObserver { val connectivityManager: ConnectivityManager? = context.getSystemService() ... return try { RealNetworkObserver(connectivityManager, listener) } catch (e: Exception) { logger?.log(TAG, RuntimeException("Failed to register network observer.", e)) EmptyNetworkObserver() } } ImageLoader のインスタンスを取得するときは ImageLoader.kt にいる ImageLoader() ではなく、Coil.imageLoader() を使うようにしましょう!

こちらは共通の ImageLoader インスタンスが返されるので registerNetworkCallback() を呼び過ぎることにはなりません。


参考 : https://issuetracker.google.com/issues/231499040#comment3


2024年3月30日土曜日

Gmail が Intent.selector に反応しない場合がある

手元の Pixel 8 では発生しないのだが、ユーザーさんの moto g13 (motorola penangf)(Android 13(SDK 33)) では、 val intent = Intent(Intent.ACTION_SEND) .putExtra(Intent.EXTRA_EMAIL, arrayOf(address)) .putExtra(Intent.EXTRA_SUBJECT, subject) .putExtra(Intent.EXTRA_TEXT, text) .apply { selector = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) } startActivity(intent) この Intent は ActivityNotFoundException になってしまう。

selector を使わない次の方法なら ActivityNotFoundException にならない。 val intent = Intent(Intent.ACTION_SENDTO) .setData(Uri.parse("mailto:")) .putExtra(Intent.EXTRA_EMAIL, arrayOf(address)) .putExtra(Intent.EXTRA_SUBJECT, subject) .putExtra(Intent.EXTRA_TEXT, text) startActivity(intent)

2024年2月27日火曜日

Android Studio (IntelliJ IDEA) の Replace の正規表現モードを使う

例えば assertThat(answer).isEqualTo(2) assertEquals(2, answer) に置き換えたい場合、Replace の正規表現モードを使うことで簡単に変換できる。

Cmd + Shift + R で Replace in Files window を開き、
(そのファイルだけ置き換えたいときは Cmd + R)
変換元を入力するフィールドの右端の 「.*」 部分をオンにする。

変換元に assertThat\((.*)\)\.isEqualTo\((.*)\) 変換先に assertEquals\($2, $1\) と入れて、Replace All ボタンを押すと全部置き換わる。便利!