2024年3月31日日曜日

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

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

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

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

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

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

ImageLoader.Builder の build() で RealImageLoader が作られ、NetworkObserver が作られ、 RealNetworkObserver() が作られます。
RealNetworkObserver の init で registerNetworkCallback() しています。
  1. private class RealNetworkObserver(  
  2.     private val connectivityManager: ConnectivityManager,  
  3.     private val listener: Listener  
  4. ) : NetworkObserver {  
  5.   
  6.     ...  
  7.   
  8.     init {  
  9.         val request = NetworkRequest.Builder()  
  10.             .addCapability(NET_CAPABILITY_INTERNET)  
  11.             .build()  
  12.         connectivityManager.registerNetworkCallback(request, networkCallback)  
  13.     }  
  14.     ...  
  15. }  
RealNetworkObserver のインスタンスを作るところで try-catch しているので、ここでは問題に気づかず、WorkManager の方で発覚したということでした。
  1. internal fun NetworkObserver(  
  2.     context: Context,  
  3.     listener: Listener,  
  4.     logger: Logger?  
  5. ): NetworkObserver {  
  6.     val connectivityManager: ConnectivityManager? = context.getSystemService()  
  7.     ...  
  8.   
  9.     return try {  
  10.         RealNetworkObserver(connectivityManager, listener)  
  11.     } catch (e: Exception) {  
  12.         logger?.log(TAG, RuntimeException("Failed to register network observer.", e))  
  13.         EmptyNetworkObserver()  
  14.     }  
  15. }  
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)) では、
  1. val intent = Intent(Intent.ACTION_SEND)  
  2.     .putExtra(Intent.EXTRA_EMAIL, arrayOf(address))  
  3.     .putExtra(Intent.EXTRA_SUBJECT, subject)  
  4.     .putExtra(Intent.EXTRA_TEXT, text)  
  5.     .apply {  
  6.         selector = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))  
  7.     }  
  8.   
  9. startActivity(intent)  
この Intent は ActivityNotFoundException になってしまう。

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