Androidアプリテスト用のエミュレータ設定メモ

なんかPC買い替えるたびに同じようなこと調べてるのでメモ。

アプリ仕様

  • minSdk=21, targetSdk=29.
  • orientation は Portrait で固定。
  • 横サイズは 320dp-411dp で正常表示されること。

エミュレータ設定

Name           Device                                System Image
------------------------------------------------------------------------------------------------
w320dp-api21 : Nexus 5      4.95" 1080x1920 xxhdpi / Lollipop 21 x86_64 Android  5.0(Google APIs)
w320dp-api29 : Nexus 5      4.95" 1080x1920 xxhdpi / Q        29 x86_64 Android 10.0(Google Play)
w411dp-api21 : Google Pixel 5.0"  1080x1920 420dpi / Lollipop 21 x86_64 Android  5.0(Google APIs)
w411dp-api29 : Google Pixel 5.0"  1080x1920 420dpi / Q        29 x86_64 Android 10.0(Google Play)

RxJava の subscribe の例外ハンドリングが適用されない例外への対応メモ

RxJava の subscribe メソッドの例外ハンドリングを記述していてもアプリが落ちるケースがあるのでとりあえずメモ。

source: git@github.com:beyondseeker/chrono3_rx_grobal_error_handling.git

subscribe() による例外ハンドリングをすり抜ける例

下記のようなコードを Android アプリの中で実行した場合、subscribe メソッドの例外ハンドリングでは対応できず、アプリが落ちる。

/**
 * subscribe() による例外ハンドリングをすり抜ける例
 */
@Test
fun example1() {
    val disposable = CompositeDisposable()
    disposable.add(
        Completable
            .fromCallable {
                // この sleep 中に disposable.dispose() が呼ばれ、このスレッドが interrupt され java.lang.InterruptedException
                // がスローされるが、既に dispose されているので subscribe() にてこの例外をハンドリングできない。
                Thread.sleep(200)
            }
            .subscribeOn(Schedulers.newThread())
            .subscribe({ onSuccess(disposable) }, ::onError)
    )

    Thread.sleep(100)

    disposable.dispose()

    Thread.sleep(300)
}

例外はこんな感じで io.reactivex.exceptions.UndeliverableException がスローされてます。

Exception in thread "RxNewThreadScheduler-1" io.reactivex.exceptions.UndeliverableException: The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with. Further reading: https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling | java.lang.InterruptedException: sleep interrupted
	at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:367)
	at io.reactivex.internal.operators.completable.CompletableFromCallable.subscribeActual(CompletableFromCallable.java:42)
	at io.reactivex.Completable.subscribe(Completable.java:2309)
	at io.reactivex.internal.operators.completable.CompletableSubscribeOn$SubscribeOnObserver.run(CompletableSubscribeOn.java:64)
	at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578)
	at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
	at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.objectfanatics.chrono3.ExampleUnitTest$example1$1.call(ExampleUnitTest.kt:26)
	at com.objectfanatics.chrono3.ExampleUnitTest$example1$1.call(ExampleUnitTest.kt:14)
	at io.reactivex.internal.operators.completable.CompletableFromCallable.subscribeActual(CompletableFromCallable.java:36)
	... 11 more

subscribe() による例外ハンドリングをすり抜けた例外をキャッチする例

下記のコードは、例外を握りつぶしている。
Androidアプリの場合は RxJavaPlugins#setErrorHandler() を Application#onCreate() 辺りで実行しておけばいいんでしょうかね。

/**
 * subscribe() によるハンドリングをすり抜た例外を catch する例
 */
@Test
fun example2() {
    RxJavaPlugins.setErrorHandler { throwable -> println("RxJava Global Error: ${throwable.message}") }
    example1()
}

private fun onSuccess(disposable: Disposable) {
    println("onSuccess: isDisposed = ${disposable.isDisposed}")
}

private fun onError(error: Throwable) {
    println("onError: ${error.message}")
}

dispose された時点以降は onSuccess も onError も呼ばれる必要はないというのであれば、特に問題なさそうな気もする。

SnapHelper のサンプルを作成してみた

先頭もしくは末尾の item をキリの良い位置で IDLE にするための SnapHelper を Horizontal と Vertical 用に作成してみました。

f:id:beyondseeker:20190721004043p:plain:w250
StartSnapHelper を Horizontal/Vertical の RecyclerView で扱うサンプル画面

SnapHelperの本家ドキュメントはこちら
StartSnapHelper の GitHub 上のソースはこちら
サンプルアプリのレポジトリ:git@github.com:beyondseeker/chrono1_StartSnapHelper.git

package com.objectfanatics.infra.androidx.recyclerview.widget

import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.NO_POSITION

/**
 * This is a [LinearSnapHelper] tries to show the first or last visible item completely.
 */
class StartSnapHelper : LinearSnapHelper() {
    override fun calculateDistanceToFinalSnap(layoutManager: LayoutManager, targetView: View): IntArray =
        when {
            layoutManager.canScrollHorizontally() -> intArrayOf(distanceToLeft(layoutManager, targetView), 0)
            layoutManager.canScrollVertically() -> intArrayOf(0, distanceToTop(layoutManager, targetView))
            else -> throw newException(layoutManager)
        }

    override fun findSnapView(layoutManager: LayoutManager): View? {
        with(layoutManager as LinearLayoutManager) {
            val firstVisibleItemPos = findFirstVisibleItemPosition()

            if (isEmpty(firstVisibleItemPos) || isLastItemCompletelyVisible()) {
                return null
            }

            val firstVisibleItem = findViewByPosition(firstVisibleItemPos)

            getOrientationHelper(layoutManager).run {
                // Visible size of firstVisibleItem.
                val visibleSizeOfFirstVisibleItem = getDecoratedEnd(firstVisibleItem)

                // Size of firstVisibleItem.
                val wholeSizeOfFirstVisibleItem = getDecoratedMeasurement(firstVisibleItem)

                return when {
                    // Visible size of firstVisibleItem is greater than zero and greater than or equals to half of the whole size.
                    visibleSizeOfFirstVisibleItem > 0 && visibleSizeOfFirstVisibleItem >= wholeSizeOfFirstVisibleItem / 2 -> firstVisibleItem

                    // Visible size of firstVisibleItem is zero or less than half of the whole size.
                    else -> findViewByPosition(firstVisibleItemPos + 1)
                }
            }
        }
    }

    companion object {
        private fun horizontalHelper(layoutManager: LayoutManager): OrientationHelper =
            OrientationHelper.createHorizontalHelper(layoutManager)

        private fun verticalHelper(layoutManager: LayoutManager): OrientationHelper =
            OrientationHelper.createVerticalHelper(layoutManager)

        // The left of the view including its decoration, margin and padding.
        private fun distanceToLeft(layoutManager: LayoutManager, targetView: View): Int =
            horizontalHelper(layoutManager).run { getDecoratedStart(targetView) - startAfterPadding }

        // The top of the view including its decoration, margin and padding.
        private fun distanceToTop(layoutManager: LayoutManager, targetView: View): Int =
            verticalHelper(layoutManager).run { getDecoratedStart(targetView) - startAfterPadding }

        private fun isEmpty(firstItemPos: Int) = firstItemPos == NO_POSITION

        private fun LinearLayoutManager.isLastItemCompletelyVisible() =
            findLastCompletelyVisibleItemPosition() == itemCount - 1

        private fun getOrientationHelper(layoutManager: LinearLayoutManager): OrientationHelper = when {
            layoutManager.canScrollHorizontally() -> horizontalHelper(layoutManager)
            layoutManager.canScrollVertically() -> verticalHelper(layoutManager)
            else -> throw newException(layoutManager)
        }

        private fun newException(layoutManager: LayoutManager) =
            UnsupportedOperationException("Unexpected layoutManager: ${layoutManager.javaClass.name}")
    }
}

module を追加して Program type already present が出た場合

複数の AndroidManifest.xml にて同一の package name を設定したことにより Program type already present エラーになった時のメモ。

  • 重複クラスを確認する。
    • gradle の build 表示 から Program type already present <FQCN> 文字列を確認。
    • IntelliJ Idea(or Android Studio) で CTRL+N にて当該 FQCN を検索。
  • AndroidManifest.xml の name 要素を変更する。

Android Studio 上の IDEA Vim でエスケープキーが効かなくなった際のメモ

Android Studio(IntelliJ IDEA) で IDEA Vim が突然エスケープ(escape) キーが効かなくなってしまった場合のメモ。

Insert mode から抜けられなくなったと思いIDEの再起動やPCの再起動をしてみたが状況は変わらず、、、。

結論としては単なる勘違いで、CTRL+ALT+v で IDEA Vim を disabled にしてしまっていただけでした。

戻すには CTRL+ALT+v で IDEA Vim を enabled にしてあげればOK。

Microsoft Office 2010 が動かなくなった(KB4461627)

2019-01-05にPCを再起動したところ、Excel を起動すると、起動後スプレッドシート自体が表示されるがしばらく Excel が操作不能という状態が続き、その後 Excel が突然落ちるという、再現可能な現象が発生した。

windows update が問題だったらしく、Excel 2010 (KB4461627)、2019 年 1 月 2日の更新プログラム をアンインストールすることで解決した。