¿Por qué mi configuration de RxJava está bloqueando mi subprocess de interfaz de usuario? Trabajar con la callback BluetoothAdapter.startLeScan

Estoy luchando para encontrar la acción específica que está bloqueando el hilo de la interfaz de usuario, he intentado con varios operadores de Scheduler, pero no estoy seguro de cómo hacerlo funcionar.

Tengo una IU con button, que onClicked está iniciando el escaneo bluetooth y actualiza el textView con cadenas como un logging (muestra lo que está sucediendo en este momento).

Así que aquí está mi actividad principal:

lateinit var disposable: Disposable val textDataService = TextDataService() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scan_test) buttonScanTestStart.setOnClickListener { if (isBluetoothEnabled()) { textViewLog.text = "" buttonScanTestStop.visibility = View.VISIBLE buttonExportScanTestRaportSummary.visibility = View.GONE buttonExportScanTestRaportFull.visibility = View.GONE buttonScanTestStart.visibility = View.GONE disposable= Scanner() .discoverSingleDevice(this, " ", textViewLog) .doOnError { setText("General error: ${it.message ?: it::class.java}", textViewLog) setLogText("General error: ${it.message ?: it::class.java}") } .repeat(1) .doOnComplete { buttonScanTestStop.visibility = View.GONE } .doOnDispose { log("TEST DISPOSED") } .subscribe() } else { Toast.makeText(this, "Please, enable bluetooth to start test.", Toast.LENGTH_SHORT).show() } } buttonScanTestStop.setOnClickListener { disposable.dispose() buttonScanTestStop.visibility = View.GONE buttonScanTestStart.visibility = View.VISIBLE buttonExportScanTestRaportSummary.visibility = View.VISIBLE buttonExportScanTestRaportFull.visibility = View.VISIBLE textDataService.generateScanGeneralStatisticsLogText(textViewLog) } 

Aquí está la class de escáner:

 class Scanner { private fun scan(context: Context, textView: TextView) = Observable.create<ScannedItem> { emitter -> val bluetoothAdapter = context.getBluetoothAdapter() if (bluetoothAdapter.isEnabled) { val scanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, rssi, _ -> if(bluetoothDevice.name == null){ bluetoothRawLog("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}") scannedOtherDevices.add(bluetoothDevice.address) } else{ if(!scannedDevices.contains(bluetoothDevice.name)){ setText("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}\n", textView) setLogText("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi | time: = ${getCurrentTime()}\n") } scannedDevices.add(bluetoothDevice.name) } bluetoothRawLog("Scanned Item -> ${bluetoothDevice.logText()} | rssi = $rssi") bluetoothDevice.name?.let { emitter.onNext(ScannedItem(it, bluetoothDevice.address)) } } emitter.setCancellable { bluetoothAdapter.stopLeScan(scanCallback) } bluetoothAdapter.startLeScan(scanCallback) } else emitter.onError(IllegalStateException("Bluetooth turned off")) } .doOnNext { log("Scanned -> $it") } .timeout(12, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .doOnError { if (it is TimeoutException) setLogText("Scanner -> no named devices for 12 seconds: resetting...") } .retry { t -> t is TimeoutException } fun discoverSingleDevice(context: Context, searchName: String, textView: TextView): Observable<ScannedItem> = scan(context, textView) .filter { it.name.contains(searchName) } .take(1) 

Y aquí están mis funciones de extensión de Kotlin, que estoy usando para establecer el text:

 fun setLogText(text: String) { logBuffer.append(text) } fun setText(text: String, textView: TextView) { textView.append(text) } 

Aquí también está mi layout:

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

 <LinearLayout android:layout_margin="16dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="horizontal"> <Button android:layout_weight="1" android:id="@+id/buttonScanTestStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Start Test" /> <Button android:layout_weight="1" android:visibility="gone" android:id="@+id/buttonScanTestStop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Stop Test" /> <Button android:layout_weight="1" android:id="@+id/buttonExportScanTestRaportFull" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Export raport" android:visibility="gone" /> <Button android:layout_weight="1" android:id="@+id/buttonExportScanTestRaportSummary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Export summary" android:visibility="gone" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:visibility="gone" android:id="@+id/scanTestStartedLinearLayout" android:gravity="center" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:text="Test running..." android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ProgressBar android:id="@+id/scanTestProgressBar" android:layout_width="16dp" android:layout_height="16dp" /> </LinearLayout> <TextView android:id="@+id/textViewScanTestLog" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:gravity="center" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:gravity="center"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/textViewLog" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </ScrollView> </LinearLayout> 

Así que el text está apareciendo en mi textView bastante suave, todo está bien. Sin embargo, cuando hay más cantidad de dispositivos disponibles para escanear, no puedo detener la testing haciendo clic en el button. Simplemente se congela y responde demasiado tarde cuando trato de hacer clic.

La function discoverSingleDevice se ejecutará para siempre, porque filter para tener el nombre de búsqueda de mi editText (que codifiqué con "") para que funcione para siempre y solo quiero detener esto haciendo clic en el button de detención en mi layout. Como segundo parámetro, pasé a textViewLog desde mi layout (Kotlin es lo suficientemente inteligente como para encontrarlo por id)

Entonces, ¿cómo evitar el locking de la interfaz de usuario con esta configuration?

ACTUALIZAR:

Como WenChao sugirió, he hecho algo como esto:

  val observable = Scanner() .discoverSingleDevice(this, " ", textViewLog) .doOnError { nexoSetText("General error: ${it.message ?: it::class.java}", textViewLog) nexoSetLogText("General error: ${it.message ?: it::class.java}") } .repeat(1) .doOnComplete { buttonScanTestStop.visibility = View.GONE } .doOnDispose { nexoLog("TEST DISPOSED") } disposable = observable .subscribeOn(Schedulers.newThread()).subscribe() 

Sin embargo, no ayudó, no es tan simple. De nuevo, he intentado varias cosas con otros operadores. Por favor, sea específico a mi caso: proporcioné el código.

Es como después de 2 minutos de escaneo, la aplicación está bastante congelada. En los loggings, por supuesto, que aparecen I / Coreógrafo: ¡se saltaron 54 cuadros! La aplicación puede estar haciendo demasiado trabajo en su hilo principal.

ACTUALIZACIÓN 2:

Como me sugirió PhoenixWang, inicié session para verificar qué hilo ejecuta BluetoothAdapter. Así que lo hice así en mi método de escaneo en la class de escáner:

  private fun scan(context: Context, textView: TextView) = Observable.create<ScannedItem> { emitter -> val bluetoothAdapter = context.getBluetoothAdapter() if (bluetoothAdapter.isEnabled) { val scanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, rssi, _ -> Log.v("BluetoothThread", "Running on: " + Thread.currentThread().name) ///the rest of the code 

Entonces, ¿cómo arreglar esto? Creí que se ejecutaría en el hilo por separado (no en el principal) porque hice la corrección ingenua, que es visible en mi primera actualización. ¿Puede usted ayudar?

ACTUALIZACIÓN 3:

Entonces, el problema es con BluetoothAdapter.leScan: se ejecuta en el hilo principal, incluso si hago algo como esto:

 Observable.fromCallable{ bluetoothAdapter.startLeScan(scanCallback)}.subscribeOn(Schedulers.newThread()).subscribe() 

Como sugiere PhoenixWang, BluetoothAdapter.LeScanCallback se ejecuta en mainthread debido a su implementación. Observable / RxJava no puede cambiarlo. Entonces, ¿cómo resolver mi problema?

RxJava es sincrónico por defecto. En su caso, se ejecuta en el hilo de llamada que es el hilo principal de Android

Debe especificar qué hilo desea que suceda la suscripción.

 Observable.fromCallable(whatever()) .subscribeOn(Schedulers.newThread()).subscribe() 

lea más aquí .

Para ser más claro. Respondo pero no puedo resolver tu problema. Tu codigo:

  Observable.fromCallable{ bluetoothAdapter.startLeScan(scanCallback)}.subscribeOn(Sche‌​dulers.newThread()).‌​subscribe(). 

significa que su bluetoothAdapter.startLeScan(scanCallback) se ejecuta en newThread. Pero no cómo bluetoothAdapter te devuelve la llamada.

Entonces, en la implementación de su startLeScan. Puede iniciar un nuevo hilo o ejecutarlo en otro hilo depende de su implementación.

La misma idea con su Observable.create (// sus cosas)

Están garantizados en ejecución en su planificador especificado. Pero también puede comenzar un nuevo hilo dentro de él. Es por eso que mencioné en un comentario para verificar la implementación de su BluetoothAdapter.

Actualizar

Para ser más claro, una implementación de ejemplo de BluetoothAdapter que puede bloquear su subprocess de interfaz de usuario.

 class BluetoothAdapter { fun startLeScan(callback: LeScanCallback) { val handler = Handler(Looper.getMainLooper()) handler.post { // here you back to your UI Thread. callback.onSuccess(1) } } interface LeScanCallback { fun onSuccess(result: Int) } } 

Entonces, si su implementación de BluetoothAdapter cambiará el hilo de nuevo a la cadena de interfaz de usuario. Eventualmente bloquearás tu subprocess de interfaz de usuario sin importar el hilo de tu Observable. A eso me refiero. Verifique la implementación de su Adaptador Bluetooth

  .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) 

Agregue .subscribeOn (Schedulers.io ()) junto con .observeOn (AndroidSchedulers.mainThread ()) como arriba. Prueba esto. Deberia trabajar.

  • Room - SELECT query, get o default
  • Problema de encadenamiento Completable after flatMapCompletable
  • Cómo manejar el event handling errores en un solo lugar en rxjava usando wrapper
  • Java genérico para Kotlin genérico. Retorno genérico del método
  • Retrofit-Vertx con RxJava2 en Kotlin IllegalStateException message == null
  • JsonArray a la class de datos de Kotlin con Retrofit (se esperaba BEGIN_OBJECT pero era BEGIN_ARRAY)
  • Observable.fromCallable () implementación con exception
  • Cómo hacer un event handling errores en rxjava2 en android
  • Confundido sobre la asignación de la variable RxJava
  • Fusionar observables dependientes
  • RxJava salida diferente entre Flowable y Observable con window y Groupby