AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

Chrome Custom TabでデフォルトブラウザがChrome以外の場合の対応

デフォルトブラウザがChrome以外のとき、FirefoxChrome Custom Tabで開いてくれるがドルフィンブラウザなどは非対応のブラウザではそちらが開いてしまう。

f:id:phicdy:20190714185147g:plain

Chrome Custom TabのIntentに com.android.chrome を指定してあげればChrome Custom Tabで開けるようになる。万が一Chromeがないまたは無効化されている場合に備えて ActivityNotFoundException をハンドリングしておく。 今回はSnackbarを指定しているが、もっと丁寧にやるならWebViewで開くようにしてあげるともっといいかもしれない。

try {
    val customTabsIntent = CustomTabsIntent.Builder()
            .setShowTitle(true)
            .setToolbarColor(ContextCompat.getColor(activity, R.color.background_toolbar))
            .build()
    customTabsIntent.intent.setPackage("com.android.chrome") // Chromeを指定
    customTabsIntent.launchUrl(context, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
    // Chromeがない、または無効にされている
    Snackbar.make(findViewById(R.id.fab), R.string.open_internal_browser_error, Snackbar.LENGTH_SHORT).show()
}

Androidのマルチモジュールでのjacoco設定

ハマったのでまとめておく。結論から言うとjacocoにアップロードしてGitHubのPR連携をする場合はレポートをマージする必要はない。マージしてしまうとパスを判別できなくなるせいかエラーになってしまう。

f:id:phicdy:20190624224848p:plain

CircleCIで bash <(curl -s https://codecov.io/bash) としていれば勝手に複数のレポートをアップロードしてくれる。

f:id:phicdy:20190624225114p:plain

一方で自分でJenkinsなどを立てている場合はマージしてファイルを1つにしないとグラフ表示などの設定がうまくいかない。

マージする場合

build.gradle

buildscript {
...
    ext.jacoco_version = "0.8.2"
...
}

apply plugin: 'jacoco'

jacoco {
    toolVersion = jacoco_version
}

task jacocoMerge(type: JacocoMerge) {
    gradle.afterProject { p, state ->
        if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) {
            executionData file("${p.buildDir}/jacoco").listFiles().findAll {
                it.name.endsWith(".exec")
            }
        }
    }
}

def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*',
                            '**/BuildConfig.class', '**/*Activity*.class',
                            '**/*Fragment*.class', '**/*Receiver.class',
                            '**/*Manifest*.class', '**/*Application*.class',
                            ....]
task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) {
    executionData jacocoMerge.destinationFile
    def sources = []
    subprojects.forEach {
        sources += "${it.projectDir.path}/src/main/java"
    }
    sourceDirectories = files(sources)
    classDirectories = fileTree(
            dir: ".",
            includes: ["**/build/intermediates/classes/debug/**",
                       "**/build/tmp/kotlin-classes/debug/**"],
            excludes: coverageExcludeFiles
    )

    reports {
        xml.enabled = true
        html.enabled true
        csv.enabled false
        xml.destination file("${buildDir}/reports/jacoco/report.xml")
        html.destination file("${buildDir}/reports/jacoco/html")
    }
}

task testDebugUnitTest(dependsOn: [tasks.jacocoMergedReport])

マージしない場合

phicdy.hatenablog.com phicdy.hatenablog.com

辺りの設定を各モジュールに設定すればOK。

具体的には↓みたいな設定を jacoco.gradle としてプロジェクトのルートに置く。

apply plugin: 'jacoco'

jacoco {
    toolVersion = jacoco_version
}

// A list of directories which should be included in coverage report
def coverageSourceDirs = ['src/main/java', 'src/main/kotlin']
// A list of files which should be excluded from coverage report since they are generated and/or framework code
def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*',
                            '**/BuildConfig.class', '**/*Activity*.class',
                            '**/*Fragment*.class', '**/*Receiver.class',
                            '**/*Manifest*.class', '**/*Application*.class',
                            ...]

task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
        xml.destination new File("${buildDir}/reports/jacoco/jacocoTestReport.xml")
        html.destination new File("${buildDir}/reports/jacoco/html")
        classDirectories =
                fileTree(dir: "${buildDir}/intermediates/classes/debug",
                        exclude: coverageExcludeFiles) +
                        fileTree(dir: "$buildDir/tmp/kotlin-classes/debug",
                                excludes: coverageExcludeFiles)
    }
    sourceDirectories = files(coverageSourceDirs)
    executionData = files "${buildDir}/jacoco/testDebugUnitTest.exec"

    doLast {
        println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/report.xml"
        println "jacoco html report has been generated to file://${reports.html.destination}/index.html"
    }
}

そしてjacocoを使いたいモジュールで apply from: "$rootDir/jacoco.gradle" するかもしくはルートのbuild.gradleで適用させてしまうと楽。

subprojects {
    if (name == 'app') {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'
    apply from: "$rootDir/jacoco.gradle"
    ...
}

参考

vividcode.hatenablog.com

clash-m45.hatenablog.com

github.com

Koin 2.0へのマイグレーション

先日Koin 2.0が出たのでアップデートしてみました。

medium.com

APIが変わっているのでマイグレーションが必要です。

エントリポイントの変更

1.0まではApplicationクラスでstartKoin()に自身とモジュールのリストを渡していましたが、DSLになり、個別に指定するようになりました。

-        startKoin(this, listOf(appModule))
+        startKoin {
+            androidLogger()
+            androidContext(this@MyApplication)
+            modules(listOf(appModule))
+        }

スコープの変更

1.0までは自分で名前をつけており、非常に不便でした。2.0からはActivity/Fragmentを型として指定できるので便利です。

-    scope("main") { (view: MainActivityView) ->
-        MainActivityPresenter(
-                view = view,
-                hogeRepository = get(),
-                fugaUseCase = get()
-        )
+    scope(named<MainActivity>()) {
+        scoped { (view: MainActivityView) ->
+            MainActivityPresenter(
+                    view = view,
+                    hogeRepository = get(),
+                    fugaUseCase = get()
+            )
+        }
     }

Activity/Fragment側では1.0までは bindScope(getOrCreateScope("main")) と書いていましたが不要になりました。そしてinject()の代わりにcurrentScope.inject()を使います。これで自動的にスコープが紐づくようです。

-    private val presenter: MainActivityPresenter by inject { parametersOf(this) }
+    private val presenter: MainActivityPresenter by currentScope.inject { parametersOf(this) }

私の環境ではこれだけでマイグレーションできました。もっといろんな機能を使っている方は公式サイトも2.0のドキュメントになっているのでそちらを参照すれば対応可能かと思います。

おわりに

Annotation Processorを使っているDaggerほどではないもののKoin 2.0ではかなり起動時のパフォーマンスが改善されました。今後にも期待です。

参考

insert-koin.io

Macbook Pro(15-inch, 2016)のキーボードが2回押されるので修理に出した

これです。

www.apple.com

実は買ってから結構序盤の段階でbが2回押されたり1回も押されなかったりする問題が起きてました。上のプログラムが発表されてからいつか修理に出したいなーと思いつつも1週間くらいPCないのはなーと思いとどまってました。このゴールデンウィークで1週間海外旅行に行くことになったのでその間に出すことにしました。

流れ

まずバックアップを取ります。Apple Storeでバックアップ取ったか聞かれます。結局データは消えずに戻ってきました。

バックアップを取ったらApple Storeに行きます(ちゃんと調べてないけどWebから直接修理に出せる気もする)。職場が近いので表参道店に行きました。平日11:30から昼休みを取って行ったんですが、12:40からしか空いてないと言われました。平日でも混んでいる・・・。1時間後にもう1度行きました。

まず症状を伝えたところキーボード内部の掃除をしてくれました。この段階でbが1回も押されない現象は直りました。ただ2回押される問題は直らなかったので結局修理に出すことに。キーボード下の基盤を全交換なので本来ならば88000円のようでしたが上記プログラムがあるので無料でした。

配送と店舗受け取りか選べます。私は店舗受け取りにしました。

4/24に修理に出したのですが4/26にはもう受取可能のメールが来ました。これだったら旅行とかなくても出せばよかった・・・。むしろ到着が早すぎて5/3受け取り以降は要連絡(日程調整が要る?)となってしまったので帰国後すぐ受け取らなければならなかったです。

f:id:phicdy:20190505171610p:plain

あとは受け取って終わりです。受け渡しのときにMacbook Proだけ渡されたので何か袋とか大きいバッグを持っていったほうがよいです。

終わりに

2~3日で修理が終わって受け取れるのでいつ出してもよかったなと思いました。キーボードが直っただけでなくディスプレイなども綺麗にしてくれたのでありがたかったです。

Ultimate Hacking Keyboardを買いました

f:id:phicdy:20190322151836j:plain

会社ではErgoDoxを使っていますが、リモートワークするときに毎回持って帰るのがめんどくさいなーと思って会社の自作キーボードチャンネルを見ていたところ良さげな分離キーボードだったので買ってみました。去年の11/3に注文して3/21にようやく到着した。

ultimatehackingkeyboard.com

phicdy.hatenablog.com

かっちょいい。

f:id:phicdy:20190322151858j:plain

箱を開けるとこんな感じ

f:id:phicdy:20190322151847j:plain

パームレストも買いました。

f:id:phicdy:20190322151914j:plain

値段

計40533円($324.93+2380円)

組み立て

半田とかのスキルはないのでキーボードが完成済みなのは助かります。↓に従って組み立てます。

ultimatehackingkeyboard.com

f:id:phicdy:20190322154224j:plain

3種類組み立て方があって、今回はTentingを選択。

f:id:phicdy:20190322153422p:plain

まずパームレストをつけます。パームレストにネジが入っています。

f:id:phicdy:20190322154335j:plain

左右2箇所ずつネジ止めします。

f:id:phicdy:20190322154401j:plain

f:id:phicdy:20190322154518j:plain

足をつけます。上下に3個ずつのネジをつけます。

f:id:phicdy:20190322155404j:plain

足をはめます。

f:id:phicdy:20190322160018j:plain f:id:phicdy:20190322160023j:plain

完成。

f:id:phicdy:20190322160650j:plain

キーマップ

繋いでみると現在のキーマップが表示されます。MacQWERTYなのでFn+4で切り替える。

Keymap Name Keymap Abbreviation Keymap Switch Shortcut
QWERTY for PC QWR Fn + 1
Dvorak for PC DVO Fn + 2
Colemak for PC COL Fn + 3
QWERTY for Mac QWM Fn + 4
Dvorak for Mac DVM Fn + 5
Colemak for Mac COM Fn + 6

Mouseキー

Macの左Ctrlの位置にMouseキーがあります。これでマウスを操作したりクリックしたり画面をスクロールしたりできる模様。

f:id:phicdy:20190322163140p:plain

Modキー

矢印キーを打つときはModキーを押しながらjkliで打つ。他にもDeleteキーやHomeキーもModキーを押しながらで打てるようになる。

f:id:phicdy:20190322164933p:plain f:id:phicdy:20190322165250p:plain

Agentがあってこれでキーをいじれるようです。 github.com

終わりに

まだまだ操作に慣れないですが、慣れると使いやすそうです。ErgoDoxほど自由ではないかなと思ってましたが以外にカスタマイズできそう。キーボード自体の品質も良くて満足です。ただ来るまで半年近くかかったので今から買う人はそのくらいかかること前提で注文したほうがいいと思います。

追記(2019/3/23)

公式アカウントからリプライが来ました。どうやら4月中旬からは改善されて1週間以内に発送できるようになるようです。

Android Gradle plugin 3.2.0からjacocoでJavaファイルのカバレッジが取れない問題の修正

通常はKotlinだけで書くと思うので関係ないです。Java -> Kotlin移行中のプロジェクトだと起きる。

↓以前の記事

phicdy.hatenablog.com

phicdy.hatenablog.com

原因

Product Falvorを free としたとき、Javaのクラスファイルの出力先がAndroid Gradle plugin 3.2.0で ${buildDir}/intermediates/classes/free/debug/ から ${buildDir}/intermediates/javac/freeDebug/compileFreeDebugJavaWithJavac/classes/ に変わった。

修正

あとGradleのアップデートでパスの指定がdeprecatedになったのでそれも修正

task jacocoTestReport(type: JacocoReport, dependsOn: ['testFreeDebugUnitTest']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
        xml.destination new File("${buildDir}/reports/jacoco/jacocoTestReport.xml")
        html.destination new File("${buildDir}/reports/jacoco/html")
        classDirectories.setFrom(
                files(
                    fileTree(
                            dir: "${buildDir}/intermediates/javac/freeDebug/compileFreeDebugJavaWithJavac/classes/",
                            exclude: coverageExcludeFiles
                    ) +
                    fileTree(
                            dir: "$buildDir/tmp/kotlin-classes/freeDebug",
                            excludes: coverageExcludeFiles
                    )
                )
        )
    }
    sourceDirectories.setFrom(files(coverageSourceDirs))
    executionData.setFrom(new File("${buildDir}/jacoco/testFreeDebugUnitTest.exec"))

    doLast {
        println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/jacocoTestReport.xml"
        println "jacoco html report has been generated to file://${reports.html.destination}/index.html"
    }
}

?attr/selectableItemBackgroundは何色か

答え:

アプリ・Activityに適用しているテーマ、起動するAndroid OS VersionとcompileSdkVersionとサポートライブラリのバージョンによる。 Theme.AppCompat.Light を使っていてAndroid 5.0以上でcompileSdkVersion 28でAndroidXなら #21000000

以下解説

?attrとは何か

?attrは適用しているテーマに定義されている値を参照する。なのでアプリに適用しているテーマを確認すればよい。

developer.android.com

テーマを辿る

例えばAndroidXで Theme.AppCompat.Light を使っているとする。

AndroidXのres/values を見ると values-v16~v28まである。それぞれの theme_base.xml を見て階層構造を確認してみると以下の図のようになる(もはやAndroid 4系を見る必要はないのでここではv21以上を対象とする)。

f:id:phicdy:20190213233851p:plain

では実際にどの selectableItemBackground が使われるかを見る。

selectableItemBackgroundandroid:Theme.Material.Light.NoActionBar 以外には宣言されていないので android:Theme.Material.Light.NoActionBarselectableItemBackground が使われることになる。 これはアプリのAndroid SDK、つまり compileSdkVersion で指定しているSDKにあるスタイルが使われる。 ここからはAndroid Studio上でコードを確認できる。

マテリアルデザイン系のテーマは themes_material.xml に定義されている。 Theme.Material.Light.NoActionBar には selectableItemBackground がないのでその一つ前の Theme.Material.Light を見る。

    <style name="Theme.Material.Light" parent="Theme.Light">
        ...
        <item name="selectableItemBackground">@drawable/item_background_material</item>

ようやく宣言が見つかった。 item_background_material.xml を見てみる。

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight"> 
    <item android:id="@id/mask"> 
        <color android:color="@color/white" /> 
    </item> 
</ripple>

?attrなのでもう一度上のテーマ階層を順に見ていくが colorControlHighlight は宣言されていないので Theme.Material.Light に戻る。

<style name="Theme.Material.Light" parent="Theme.Light”>
    ...
    <item name="colorControlHighlight">@color/ripple_material_light</item>

Android SDK内の ripple_material_light.xml を見る。

<selector xmlns:android="http://schemas.android.com/apk/res/android"> 
  <item android:alpha="@dimen/highlight_alpha_material_light"
        android:color="@color/foreground_material_light" />
</selector>

色は foreground_material_light であることがわかった。更に辿っていく。

colors_material_xml

<color name="foreground_material_light">@color/black</color>

colors.xml

<color name="black">#ff000000</color>

ついに到達。 #ff000000 である。 同様にアルファも見ると0.12であることがわかった。

<item name="highlight_alpha_material_light" format="float" type="dimen">0.12</item>

アルファを16進数にに置き換えると21。つまり #21000000 となる。

qiita.com

おわりに

全部は確認していないが、Android 5.0以上であればどのcompileSdkVersionでどのサポートライブラリのバージョンでもそうそうこの色は変わらないのではないかと思う。必要であればそれぞれの環境の値をこの記事と同じ方法で辿っていけば実際の値が確認できる。