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

【Android】リリース署名の設定をプロジェクト外から上書きする

GitHubでソースを公開していてリリース署名の設定をしたい場合、公開用にはダミーの署名を置き、手元ではそれを上書きしたい。かつ git status でnot stagedのところに上書き内容がいちいち出ないようにプロジェクト外から上書きしたい。

gradle.propertiesプロジェクト直下/gradle.properties が先に読まれ、次に ~/.gradle/gradle.properties が読み込まれるためプロジェクト外から値を上書きできる。

app/build.gradle

変数を囲むのはダブルクオーテーションじゃないと動かない。

android {
    ....
    signingConfigs {
        release {
            keyAlias "$RELEASE_KEY_ALIAS"
            storeFile file("$RELEASE_STORE_FILE_PATH")
            keyPassword "$RELEASE_KEY_PASSWORD"
            storePassword "$RELEASE_STORE_PASSWORD"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
            signingConfig signingConfigs.release
        }
    }
    ...
}

プロジェクト直下のgradle.properties

dummy-release-keyはapp以下に置く

RELEASE_KEY_ALIAS=key
RELEASE_STORE_FILE_PATH=dummy-release-key
RELEASE_KEY_PASSWORD=testtest
RELEASE_STORE_PASSWORD=testtest

そして ~/.gradle/gradle.properties に本当のリリース署名の設定を書く。

【Android】Toolbarの矢印をタップしたら画面を閉じる

ポイントはonOptionsItemSelected()android.R.id.homeをハンドリングしないと反応してくれない点

class MainActivity: AppCompatActivity { 
        ...

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate menu resource file.
        menuInflater.inflate(R.menu.main, menu)
        return true
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            // For arrow button on toolbar
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }
    
    override fun initToolbar() {
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)
        val actionBar = supportActionBar
        if (actionBar != null) {
            // Show back arrow icon
            actionBar.setDisplayHomeAsUpEnabled(true)
            actionBar.setDisplayShowHomeEnabled(true)
            actionBar.setTitle(R.string.app_name)
        }
    }
}

RxJavaのflatMap(mapper, combine)でリストデータをそれぞれ別スレッドで非同期処理する

例えばRSSのリストがあり、それを別々のスレッドで処理して全部の処理が終わったらUIを更新したいとする。 RxJavaのflatMap(mapper, combine)を使えばリストのデータを1つ1つのObservable/Flowableに変換して処理できる。

まず第一引数のmapper部分でそれぞれのデータを受け取ってObservable/Flowableを作る。 このときsubscribeOn(Schedulers.io())などでスレッドを分けておかないと全て同じワーカースレッドで実行されてしまうので注意。

ArrayList<RSS> rssList = ...
Flowable.fromIterable(rssList)
    .subscribeOn(Schedulers.io())
    .flatMap(new Function<RSS, Publisher<? extends RSS>>() {
                 @Override
                 public Publisher<? extends RSS> apply(RSS rss) throws Exception {
                    return Flowable.just(rss).subscribeOn(Schedulers.io());
                 }
             },

次に第二引数のcombine部分で更新処理を行う

Flowable.fromIterable(rssList)
    .subscribeOn(Schedulers.io())
    .flatMap(new Function<RSS, Publisher<? extends RSS>>() {
                 ...
             },
            new BiFunction<RSS, RSS, RSS>() {
                @Override
                public RSS apply(RSS rss, RSS rss2) throws Exception {
                    Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title());
                    // RSSの更新処理
                    ...
                    return rss2;
                }
            })

あとはUIスレッドで結果を受け取って処理をすればよい。

ArrayList<RSS> rssList = ...
Flowable.fromIterable(rssList)
    .subscribeOn(Schedulers.io())
    .flatMap(new Function<RSS, Publisher<? extends RSS>>() {
                 ...
             },
            new BiFunction<RSS, RSS, RSS>() {
                ...
            })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<RSS>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(rssList.size());
        }

        @Override
        public void onNext(RSS rss) {
            // 進捗更新 
        }

        @Override
        public void onError(Throwable t) {

        }

        @Override
        public void onComplete() {
            // 終了処理
        }
    });

全体のコード

Java(ラムダなし)

ArrayList<RSS> rssList = ...
Flowable.fromIterable(rssList)
    .subscribeOn(Schedulers.io())
    .filter(new Predicate<RSS>() {
        @Override
        public boolean test(RSS rss) throws Exception {
            return rss.id() > 0;
        }
    })
    .flatMap(new Function<RSS, Publisher<? extends RSS>>() {
                 @Override
                 public Publisher<? extends RSS> apply(RSS rss) throws Exception {
                    return Flowable.just(rss).subscribeOn(Schedulers.io());
                 }
             },
            new BiFunction<RSS, RSS, RSS>() {
                @Override
                public RSS apply(RSS rss, RSS rss2) throws Exception {
                    Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title());
                    // RSSの更新処理
                    ...
                    return rss2;
                }
            })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<RSS>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(rssList.size());
        }

        @Override
        public void onNext(RSS rss) {
            // 進捗更新 
        }

        @Override
        public void onError(Throwable t) {

        }

        @Override
        public void onComplete() {
              // 終了処理
        }
    });

Java(ラムダあり)

ArrayList<RSS> rssList = ...
Flowable.fromIterable(rssList)
    .subscribeOn(Schedulers.io())
    .flatMap(rss -> Flowable.just(rss).subscribeOn(Schedulers.io()),
    (rss, rss2) -> {
        Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title());
        // RSSの更新処理
             ...
        return rss2;
    })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<RSS>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(rssList.size());
        }
    
        @Override
        public void onNext(RSS rss) {
                // 進捗更新 
        }
    
        @Override
        public void onError(Throwable t) {
    
        }
    
        @Override
        public void onComplete() {
            // 終了処理
        }
    });

Kotlin

Flowable.fromIterable<RSS>(rsss)
    .subscribeOn(Schedulers.io())
    .filter { rss -> rss.id > 0 }
    .flatMap({ data -> Flowable.just(data).subscribeOn(Schedulers.io()) })
        { _, rss2 ->
        Log.d("test", "BiFunction, Thread:" + Thread.currentThread().name + ", rss:" + rss2.title)
        // RSSの更新処理
             ...
        rss2
    }
    .observeOn(AndroidSchedulers.mainThread())
        .subscribe(object: Subscriber<RSS> {
            override fun onSubscribe(s: Subscription?) {
                s!!.request(rssList.size.toLong())
            }

            override fun onNext(t: RSS?) {
                // 進捗更新 
            }

            override fun onError(t: Throwable?) {
            }

            override fun onComplete() {
                // 終了処理
            }
    })

Macに外付けキーボードをつなげるときの設定

自分用。

環境

  • Mac OS 10.12.6
  • Karabiner-Elements 11.6.0
  • 日本語外付けキーボード

手順

  1. Karabiner-Elementsをインストールする。
  2. Complex Modifications -> Add rule -> Import more rules from Internet(open a web browser) -> For Japanese (日本語環境向けの設定) (rev3)をImport
  3. Complex Modifications -> Add rule -> コマンドキーを単体で押したときに、英数・かなキーを送信する。(左コマンドは英数、右コマンドはかな)をEnable
  4. escキーを押したときに、英数キーも送信する(vim用)をEnable
  5. Simple Modifications -> Target Device: から接続したキーボードを選択 -> Add item -> 好きなキーをright_commandに設定(かなキーとなる)

kotlinのファイルをjacocoのコードカバレッジ対象に入れる

久し振りにjacocoの結果みたらあれ・・・なんか結果がおかしいなとなった。 よく見てみるとkotlinに変換したファイル達がコードカバレッジ対象から外れていた・・・

phicdy.hatenablog.com

以下で対応できた

  • sourceDirectoriessrc/main/kotlin を入れる(全部 src/main/java にあるから必要ないかも)
  • classDirectories$buildDir/tmp/kotlin-classes/<product flavor>Debug を追加

build.gradle

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 = [...]
task jacocoTestReport(type: JacocoReport, dependsOn: ['testUiTestDebugUnitTest']) {
    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/uiTest/debug",
                        exclude: coverageExcludeFiles) +
                fileTree(dir: "$buildDir/tmp/kotlin-classes/uiTestDebug",
                        excludes: coverageExcludeFiles)
    }
    sourceDirectories = files(coverageSourceDirs)
    executionData = files "${buildDir}/jacoco/testUiTestDebugUnitTest.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"
    }
}

jacoco: Add kotlin classes to jacoco coverage by phicdy · Pull Request #69 · phicdy/MyCuration · GitHub

参考

vgaidarji.me

Nature Remoを買った

去年の11/5に注文して12/23に届いた。 13000円。 今在庫を見てみたら2~3日後配達になっていてかなり在庫が充実している・・・

nature.global

f:id:phicdy:20180302193055j:plain

(置き方がこれでいいかは疑問が残る・・・)

いわゆるスマートリモコンで赤外線リモコンで操作できる家電(エアコンとかテレビとか)をスマホGoogle Homeなどから操作できるものである。

良かったところ

エアコンの登録が楽

他のスマートリモコンを使ったことはないのでNature Remoが特化している機能ではないかもしれない。Nature Remoのアプリを開いて1つのキーを登録するだけでエアコンの種類が判別され、全操作が自動的にできるようになった。

エアコンのオン・オフを細かく設定できる

これはNature Remoに特化している話ではない。 通常のエアコンのリモコンだと何時間後に切る or 何時間後につけるのどちらか1つしか設定できない。 Nature Remoの場合IFTTTと連携すればを下記のように細かく設定できる。

  • 平日朝7時半に暖房19℃でつける
  • 平日朝8時半に切る
  • 平日夜6時に暖房21℃でつける
  • 休日朝10時に暖房19℃でつける
  • 毎日夜12時に切る

それに加えてNature RemoアプリにGeo Fencingの設定があるので、家を離れたらエアコンを切るようにしている。

悪かったところ

購入後に無線ルーターを買い替えたためSSIDが変わった。どうやらNature Remo側にSSIDを変更する機能がなく、初期化しか方法がなかったので設定が全部やり直しになってしまった。

おわりに

買ってからしばらく経つが、かなり満足している。もっと早く買っておけばよかった・・・

java.lang.RuntimeException: com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex

UIテストを実行しようとしたらエラー・・・

f:id:phicdy:20180206222343p:plain

ひとまずRun with --stacktraceを押してみる。

...
Caused by: com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
    at com.android.dx.merge.DexMerger$8.updateIndex(DexMerger.java:565)
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:574)
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:166)
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:198)
    at com.android.builder.dexing.DexArchiveMergerCallable.call(DexArchiveMergerCallable.java:61)
    ... 1 more

これはいわゆる65536メソッド問題のようです。

UIテスト周りのbuild.gradleを見直したところ以下は消せそうだったので消し、hamcrest-libraryのメソッドはorg.hamcrest.CoreMatchersのメソッドに置き換えました。

testImplementation 'org.hamcrest:hamcrest-library:1.3'
uiTestImplementation 'com.android.support:support-annotations:25.3.1'
uiTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'

これでメソッド数が減ってUIテストが実行できるようになりました(なんでExpressoのテストないのに入れてたんだろ)