20190301のAndroidに関する記事は8件です。

Android OS 起動とSElinux

はじめに

Androidってkernelは完全にJavaでラップされていて、開発者が触るレイヤーはJavaの領域だけなのかななんて思っていました。
しかし実はLinux部分もガツガツ触って開発したりするんですね。

中々触る機会はなさそうですが、起動周りに触れたので記録に残しておこうと思います。

Androidの基本構造: Architecture

こんな感じに一番下にLinux kernelがあり、その上にHAL(Hardware abstraction layer)、System services、Binder IPC、Application frameworkと階層が続いています。Application frameworkがJava向けのAPI達で、javaから下位層を扱うの階層をちょっとずつ切ってる感じ。Andoridアプリが触るのはjava層より上の世界ですね。

根っこはLinux。組込の人にはなじみがある…けどちょっと違う

Linux kernel上でAndroidの思想通りに開発してもらえるよう、googleさんがAPI公開や開発ルールを定義しながらOSを公開してるって印象を受けました。
Linux kernel自体も例えばUbuntuやCent OSと同じ動きをするわけではないので、例えば起動の仕組みが独特だったり、udevのような仕組みが使えなかったりします。
udev、フォルダはあるからガンガン使ったろうと思ったのにびくともしなくてビビった(笑)

Androidと起動: init.rcとService

Android ApplicationだとActivity, Service, Applicationそれぞれでライフサイクルが違って、Activityなら最初にonCreateが呼ばれて~といった取り決めがあると思いますが、それ以前のboot後はどう動いているかという話です。

initプロセスから起動されるのですが、起動シーケンスの制御をどうするかというと、rcスクリプトというものを使います。
rcスクリプトの記述仕様はこちら

以下の2種類の要素で構成されます。

  1. Actions
  2. Services

1. Actions

何かのイベントトリガーでの動作を書くことが出来ます。ここで使える動作はこちらのCommandsに記載されているものだけ。
chmodやchownのようなfilesystem構成を作るためのものや、2. Servicesを呼び出すコマンドがあったりします。

記述はこんな感じでon XXXの後にコマンドをつらつらと書く感じ。実際のinit.rc等を参考にしてください。

init.rc
#on XXXで定義
on early-init
    #こんな感じでコマンドを羅列
    chown root system /dev/memcg/memory.pressure_level
    chmod 0040 /dev/memcg/memory.pressure_level
    #start サービス名で指定したサービスが起動されます。
    start ueventd

2. Services

initスクリプトからコマンドをSerivceとして起動することが出来ます。こちらもinit.rcより抜粋。
こんな感じでservice サービス名 コマンドと記載することで、on XXXのstartトリガーでサービスの起動が出来ます。
サービスの権限や終了後再起動するかといった設定をserviceの下に記載していきます。

service flash_recovery /system/bin/install-recovery.sh
    class main
    oneshot

serviceを定義する際に重要になるのがSELinux。Android OSはSELinuxを使用していて、正しくSELinuxのルールを定義していなければserviceが起動できません。
正直めんどい

AndroidとSELinux

AndroidのserviceにはSELinuxのルールを適切に定義して上げないと起動できないようになっています。
このSELinuxルールが中々曲者で、ただ自前サービスに合わせて好きに定義すればいいわけではなく、Androidのポリシーに従って正しく定義してあげないといけない
そうでないとneverallow rulesってのに引っかかり、ビルドすら通らないようになっています。つらい
後は、system配下とvendor配下のルールで出来ることが違ったりするので、やりたいことや構成によってどの配下に置くかを意識しないといけない。これは大変すね

やることは以下。

  1. SELinuxのpolicy ruleを定義する。
  2. 実行するコマンドファイルににfile_contextで権限を設定

1についてはこちらのExample policyを見るのが分かりやすいと思います。
typeでpolicyの名前+exec_typeを定義し、allow policy名 許可するルールをつらつらと書いていきます。
許可するルールの取得方法はこちらに記載されていますね。要約すると

  1. Reading denials
    1. dmesg | grep avc:で拒否されたログを取得する。
  2. Switching to permissive
    1. ログ取る時はadb shell setenforce 0等でpermissibeモードにしてね
  3. Using audit2allow
    1. ログの結果にselinux/policycoreutils/audit2allowコマンドをかますとルールが取れるよ!

ってところでしょうか。

詳細は参考のURLを参照ください。

2についてはこんな感じでファイルと権限について書かれたfileがあるので、そちらに定義を加えます。

#############################
# Vendor files
#
/vendor(/.*)?       u:object_r:system_file:s0
/vendor/bin/gpsd    u:object_r:gpsd_exec:s0

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 「App security best practices」 まとめ

あらためて目を通す機会があったので、Androidの公式ページに掲載されているApp security best practicesのセクションについて和訳してみました。

注意事項

以下、2019/03/01現在の公式ページを和訳したものになります。
意訳や端折ってる箇所もありますので、間違い等ありましたらご指摘いただけると幸いです。

安全なコミュニケーションを強化する

暗黙的インテントとエクスポートしていないコンテンツプロバイダーを利用する

App Chooser を使う

暗黙的インテントで複数起動できるアプリがある場合は、App Chooserが明示的に表示される。
これにより、ユーザーは機密情報を信頼するアプリに転送できる。

val intent = Intent(ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> =
        queryIntentActivities(intent, PackageManager.MATCH_ALL)

// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.
if (possibleActivitiesList.size > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with".

    val chooser = resources.getString(R.string.chooser_title).let { title ->
        Intent.createChooser(intent, title)
    }
    startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

署名付きパーミッションを適用する

自分で管理/所有している2つのアプリ間でデータを共有するときは署名付きパーミッションを使用する。
このパーミッションはユーザー確認を必要としない代わりに、アプリが同じ署名鍵を使用して署名されていることを確認する。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <permission android:name="my_custom_permission_name"
                android:protectionLevel="signature" />

アプリのコンテンツプロバイダへのアクセスを許可しない

自分のアプリから自分が所有していないアプリにデータを送信する予定がない場合は、他のアプリからContentProviderにアクセスすることを明示的に禁止することができる。
<provider>要素のandroid:export属性はAndroid 4.1.1(APIレベル16)以下の場合はデフォルトtrueになっているので、その場合には特に重要な設定である。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application ... >
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            ...
            android:exported="false">
            <!-- Place child elements of <provider> here. -->
        </provider>
        ...
    </application>
</manifest>

ネットワークのセキュリティ対策を適用する

SSLを使う

アプリが信頼できるCAによって発行された証明書を持つWebサーバーと通信する場合はHTTPSリクエストは簡単に実装できる。

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}

ネットワークセキュリティ設定を追加する

アプリが新規またはカスタムのCAを使用している場合は、設定ファイルでネットワークのセキュリティ設定を宣言できる。
このプロセスにより、アプリコードを変更せずに設定を作成できる。

  1. マニュフェストに設定を宣言する

    AndroidManifest.xml
    <manifest ... >
    <application
        android:networkSecurityConfig="@xml/network_security_config"
        ... >
        <!-- Place child elements of <application> element here. -->
    </application>
    </manifest>
    
  2. XMLのリソースファイルを追加する

  • クリアテキストを無効にして、特定のドメインへのすべてのトラフィックがHTTPSを使用する設定
res/xml/network_security_config.xml
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">secure.example.com</domain>
        ...
    </domain-config>
</network-security-config>
  • デバッグ時のみユーザーがインストールした証明書を許可する設定
res/xml/network_security_config.xml
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

TrustManagerを作成する

以下のどれかに当てはまる場合はTrustManagerを設定する必要があるかもしれない

  • 新しいCAまたはカスタムCAによって署名された証明書を持つWebサーバーと通信している
  • そのCAは、使用しているデバイスから信頼されていない
  • ネットワークセキュリティ設定は使用できない

この設定については以下のコンテンツを参照すること

WebViewは慎重に使用する

可能な場合は常に、ホワイトリストに登録されたコンテンツのみをWebViewでロードすること
アプリ内のWebViewではユーザーが自分の管理外のサイトに移動することはできないようにするべき

さらに、アプリケーションのWebView内のコンテンツを完全に制御および信頼している場合を除き、JavaScriptインターフェイスのサポートを有効にしないこと

HTML Message channelsを利用する

Android 6.0(APIレベル23)以降でJavaScriptを使用する必要がある場合はevaluateJavascript()ではなく、HTML Message Channels を利用してWebサイトとアプリ間で通信する。

val myWebView: WebView = findViewById(R.id.webview)

// messagePorts[0] and messagePorts[1] represent the two ports.
// They are already tangled to each other and have been started.
val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel()

// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(object: WebMessagePort.WebMessageCallback() {

    override fun onMessage(port: WebMessagePort, message: WebMessage) {
        Log.d(TAG, "On port $port, received this message: $message")
    }
})

// Send a message from channel[1] to channel[0].
channel[1].postMessage(WebMessage("My secure message"))

適切な権限を提供する

必要な最小限の権限のみを要求すること。また、可能であれば、アプリはこれらの権限の一部を不要になったら放棄すること

Intentを使って許可を延期する

可能であれば、別のアプリで実行できる操作を実行するための権限をアプリに追加しない。
代わりにすでに必要な権限を持っている別のアプリを起動することで許可を延期すること。

以下の例は、READ_CONTACTSWRITE_CONTACTSの権限を要求する代わりに連絡先アプリにユーザーを誘導する方法である。

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent(Intent.ACTION_INSERT).apply {
    type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
    // Make sure that the user has a contacts app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

また、ストレージへのアクセスやファイルの選択など、アプリがファイルベースのI/Oを実行する必要がある場合は、システムがアプリに代わって操作を完了できるため、特別な権限は必要なくなる。
さらに、ユーザーが特定のURIでコンテンツを選択した後、呼び出し側のアプリは選択されたリソースへの許可を与えられる。

アプリ間でデータを安全に共有する

より安全な方法でアプリコンテンツを他のアプリに共有するには以下に従うこと

  • 必要に応じて、読み取り/書き込み専用の権限を強制する
  • FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSIONのフラグを利用して、データへのワンタイムアクセスを提供する
  • データを共有するときのURIはFileProviderを利用してfile://ではなくcontent://を使用すること。

次に例として、URIパーミッション付与フラグとコンテンツプロバイダパーミッションを使用して、アプリケーションのPDFファイルを別のPDFビューアアプリケーションに表示する方法を示す

// Create an Intent to launch a PDF viewer for a file owned by this app.
Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse("content://com.example/personal-info.pdf")

    // This flag gives the started app read access to the file.
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.also { intent ->
    // Make sure that the user has a PDF viewer app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

安全にデータを保存する

プライベートデータを内部ストレージに保存する

アプリ毎にサンドボックス化されているデバイスの内部ストレージに保存する。
この領域は閲覧許可などは必要なく、他のアプリはファイルにアクセスすることはできないようになっている。
アンインストール時にはアプリと一緒に内部ストレージに保存した全てのファイルを削除する。

次に例として、内部ストレージにデータを書き込む方法および読み取る方法を示す

// Creates a file with this name, or replaces an existing file
// that has the same name. Note that the file name cannot contain
// path separators.
val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}
// The file name cannot contain path separators.
val FILE_NAME = "sensitive_info.txt"
val fis = openFileInput(FILE_NAME)

// available() determines the approximate number of bytes that can be
// read without blocking.
val bytesAvailable = fis.available()
val fileBuffer = ByteArray(bytesAvailable)
val topSecretFileContents = StringBuilder(bytesAvailable).apply {
    // Make sure that read() returns a number of bytes that is equal to the
    // file's size.
    while (fis.read(fileBuffer) != -1) {
        append(fileBuffer)
    }
}

外部ストレージは慎重に使用する

スコープディレクトリアクセスを使用する

アプリごとにSandbox化されているプライベート領域を利用すること
この領域はファイル閲覧の許可は必要なく、他のアプリからは参照することはできない領域となっている

書き込みの例

val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}

読み込みの例

val FILE_NAME = "sensitive_info.txt"
val fis = openFileInput(FILE_NAME)

val bytesAvailable = fis.available()
val fileBuffer = ByteArray(bytesAvailable)
val topSecretFileContents = StringBuilder(bytesAvailable).apply {
    while (fis.read(fileBuffer) != -1) {
        append(fileBuffer)
    }
}

データの有効性を確認する

外部ストレージのデータを使用している場合は、データの内容が破損または変更されていないことを確認すること
安定した形式ではなくなったファイルを処理するためのロジックも含める必要がある
例として、ファイルの有効性をチェックする権限とロジックを示す

AndroidManifest.xml
<manifest>
    <!-- API 19以降はサンドボックス化されているプライベート領域を利用すること。
         その場合は、権限の付与は必要ありません。 -->
    <uses-permission
          android:name="android.permission.READ_EXTERNAL_STORAGE"
          android:maxSdkVersion="18" />
</manifest>
private val UNAVAILABLE_STORAGE_STATES: Set<String> =
        setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
// ...
val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
when {
    isExternalStorageEmulated(ringtone) -> {
        Log.e(TAG, "External storage is not present")
    }
    UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
        Log.e(TAG, "External storage is not available")
    }
    else -> {
        val fis = FileInputStream(ringtone)

        // available() determines the approximate number of bytes that
        // can be read without blocking.
        val bytesAvailable: Int = fis.available()
        val fileBuffer = ByteArray(bytesAvailable)
        StringBuilder(bytesAvailable).apply {
            while (fis.read(fileBuffer) != -1) {
                append(fileBuffer)
            }
            // Implement appropriate logic for checking a file's validity.
            checkFileValidity(this)
        }
    }
}

機密性の低いデータのみをキャッシュファイルに保存する

機密性の低いデータに素早くアクセスするためには、キャッシュ領域に保存すること。
1MBを超える場合はgetExternalCacheDir()それ以外はgetCacheDir()を使用してFileオブジェクトを取得すること。

以下例として、直近ダウンロードしたファイルをキャッシュする方法を示す

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}
  • 注意点

    • getExternalCacheDir()を利用して共有ストレージに配置すると、アプリ実行中でもユーザーがこのファイルを触ることができるため、ユーザー操作によるキャッシュミスを適切に処理するためのロジックを含める必要がある
    • これらのファイルにはセキュリティが適用されていないため、WRITE_EXTERNAL_STORAGE権限を持つアプリはすべてアクセスすることができる。
  • 関連コンテンツ

プライベートモードでSharedPreferencesを使用する

SharedPreferencesを利用する際にはMODE_PRIVATEを使用すること。
MODE_PRIVATEでは自分のアプリだけがアクセスできるようになるため。

アプリ間でデータ共有を行う場合はSharedPreferencesを使用しないこと
アプリ間でのデータ共有については以下を参照

サービスと依存関係を最新に保つ

ほとんどのアプリは特別なタスクを実行するのに外部のライブラリやデバイスシステム情報を使用する。
アプリの依存関係を最新に保つことが重要。

Google Playサービスのセキュリティプロバイダを確認する

GooglePlayサービスが更新されているか確認すること(ただし非同期で)
最新でない場合は、アプリは認証エラーを引き起こすはず。

確認方法については以下を参考にする

すべてのアプリの依存関係を更新する

アプリをデプロイする前に、すべてのライブラリ、SDK、およびその他の依存関係が最新であることを確認すること

Android SDKなどのファーストパーティの依存関係には、SDK ManagerなどのAndroid Studioにある更新ツールを使用すること。
サードパーティの依存関係については、アプリが使用しているライブラリのWebサイトを確認し、利用可能なアップデートとセキュリティパッチをインストールすること。

まとめ

こうして改めて確認すると、やりきれてないものもあるなぁと思ってしまいました。
実はこれは1ページまとめただけで、セキュリティのセクションは他にも色々あるんですよね。
とは言えセキュリティに関することなので、一度はしっかりと目を通すことをお勧めします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android SDK をCLI で管理する(Unity向け)

はじめに

Android SDKをアップデートしなきゃと思ったらCLIを見つけたので使ってみたメモ。UnityでしかAndoroidアプリを作らないのでAndroid Studioは不要、IDEなしで軽くコマンドライン管理できたら嬉しい。

追記:Unity公式ドキュメント:Android environment setup
(shienaさんより情報を頂きました。ありがとうございます)

セットアップ

  1. Android Studioのダウンロードページの下の方「Command line tools only」から環境に合わせてダウンロード
  2. zipを解凍してできたtoolsを適当な場所に配置
  3. tools/bin/sdkmanager.bat(Windowsの場合)がSDK ManagerのCLI。

使い方

sdkmanagerのユーザーガイドより。

リストアップ

インストール済みパッケージ(Installed packages)と利用可能パッケージ(Available packages)をリストアップする。

$ sdkmanager --list

実行するとまず Installed packages、続けて Available packages が表示される。 Available packagesのリストは長いので、だいぶスクロールして戻らないと Installed packagesを確認できない。

インストール

$ sdkmanager パッケージ名

パッケージ名は--list で表示されたPath を書けばOK。スペース区切りで複数指定できる。

アンインストール

$ sdkmanager --uninstall パッケージ名

インストール済みパッケージのアップデート

$ sdkmanager --update

UnityのAndroid SDK設定

パッケージのインストール

UnityでAndroidアプリをビルドするために必要な最低限のパッケージをインストールする。バージョンはビルドターゲットに合わせて必要なものを。2019/03/01 現在の最新バージョンだと最終的こんな感じ。実験用なので、自前のPixel 3だけで動けばいいや、な環境。

$ sdkmanager build-tools;28.0.3 platform-tools platforms;android-28

結果。

$ sdkmanager --list
Installed packages:=====================] 100% Computing updates...
  Path                 | Version | Description                    | Location
  -------              | ------- | -------                        | -------
  build-tools;28.0.3   | 28.0.3  | Android SDK Build-Tools 28.0.3 | build-tools\28.0.3\
  platform-tools       | 28.0.1  | Android SDK Platform-Tools     | platform-tools\
  platforms;android-28 | 6       | Android SDK Platform 28        | platforms\android-28\
  tools                | 26.1.1  | Android SDK Tools 26.1.1       | tools\
(略)

Unity側の設定

Preferences

いつも忘れるのでメモ。Preferences... > External Tools のAndroid SDK 設定には、先程CLI を配置したパスを指定すればOK。つまり指定したパスの直下に以下のディレクトリがあるようにすると、その下をUnityが探してくれる。

build-tools
platform-tools
platforms
tools

Player Settings

Project Settings > Other Settings。Minimum API Level とTarget API Level には先程インストールしたパッケージのバージョンを指定する(実際は先にこっちが決まるはずなので順序が逆かも……)。

まとめ

ひとまずでかいAndroid Studio不要でUnity用にAndroid SDKをコマンドライン管理できるようになった。たぶんすぐ忘れるのでメモとして残しておく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GooglePlayにAppBundleをコマンドでアップロード

前回の「deploygateにapkをコマンドでアップロード」に続き、GooglePlayへの本番アプリのリリースも自動化にも取り組みました。
今まではAndroidStudio上の操作でAppBundleファイルを作成し、GooglePlayへのアップロードは手動でやっていましたが、作業の効率化やミス防止やCI導入を見据えて一連の作業をコマンド化しています。

AppBundleの導入

AppBundleの詳細についてはこちら。すでに導入している場合はスキップしてください。

1. アプリ署名への登録

GooglePlayConsoleを開き、「登録するアプリ > リリース管理 > アプリの署名」を選択します。利用規約が表示されるので、内容を確認して問題なければアプリを登録しましょう。

2. 秘密鍵のエクスポートと暗号化

まだ秘密鍵をエクスポートしていないと思うので、まずはエクスポートと暗号化を行います。合意後に表示されている画面の指示に従い、PEPKツールをダウンロードしましょう。PEPKツールで既存のkeystoreから秘密鍵のエクスポートと暗号化をおこないます。

$ java -jar pepk.jar --keystore=foo.keystore --alias=foo --output=encrypted_private_key_path --encryptionkey=xxxxxxxxx
3. 暗号化された秘密鍵をアップロード

出力されたencrypted_private_key_pathを表示されている画面上のボタンからGooglePlayへアップロードします。新たにアップロード鍵を生成して使用する場合はこちらを参考に。

gradle-play-publisherの導入

GooglePlayへのアップロードには、gradle-play-publisherを利用します。

1. GooglePlayConsoleとGoogleAPIConsole上のプロジェクトをリンク

GooglePlayConsoleの「設定 > APIアクセス」を開き、プロジェクトをGooglePlayにリンクさせます。プロジェクトが表示されない場合はGoogleAPIConsole上で新規作成し、GooglePlayAndroidDeveloperAPIを有効にしましょう。

スクリーンショット 2019-02-28 14.51.22.png

2. サービスアカウントキー(JSON)のダウンロード

続いて表示されている画面の「サービスアカウントを作成」ボタンをクリックし、GoogleAPIConsoleでのサービスアカウント作成に進みます。画面の指示に従いサービスアカウントを作成したら、「認証情報 > 認証情報の作成 > サービスアカウントキー」 にてサービスアカウントキー(JSON)をダウンロードします。

スクリーンショット 2019-03-01 13.54.47.png

3. 作成したサービスアカウントからのアクセスを許可

作成が完了するとGooglePlayConsoleの画面上の「サービスアカウント」欄に作成したサービスアカウントが表示されます。「アクセスを許可」ボタンから適切な権限を設定しましょう。

4. build.gradleの設定

app下のbuild.gradleに記述を追加します。

build.gradle
plugins {
    id 'com.android.application'
    id 'com.github.triplet.play' version '2.1.0'
    ...
}

ダウロードしたサービスアカウントキー(JSON)をAndroidプロジェクトの適当な場所に配置し、serviceAccountCredentialsで指定します。以下ではbeta版向きにしていますが、製品版をアップロードする場合はtrackをproductionにします。

build.gradle
play {
    serviceAccountCredentials = file("your-key.json")
    track = "beta"
}

GooglePlayへのアップロード

コンソールでプロジェクト直下に移動し、まずは以下のコマンドでGooglePlayに登録している画像等のリソースをダウンロードしましょう。

./gradlew bootstrapReleasePlayResources

GooglePlayに登録してある画像等を変更したい場合は、リソースを更新して以下のコマンドでアップロードできます。

./gradlew publishListingRelease

いよいよAppBundleをGooglePlayへアップロード!

./gradlew publishReleaseBundle

※productFlavorsを利用している場合はタスクもそのBuildVariantに合わせましょう。./gradlew tasksで確認できます。

以上、簡単アップロードでした (^o^)/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これからAndroidでダークモードを実装するエンジニア/デザイナ向け資料

はじめに

去年末にQiitaに投稿した「スマホアプリ開発者のための2018年動向まとめ」でダークモードについて少し書いたのですが、2019年になってからダークモードのニュースが増えているような気がします。
今風の表現で言えば2019年は"ダークモード元年"になりそうな予感(?)です..!

↓Androidでダークモード?

Android Qの初期ビルドが出回る。噂のダークモードに加え、DeX的なデスクトップモードも搭載か
https://japanese.engadget.com/2019/01/17/android-q-dex/

↓iOSもダークモード??

iOS 13でついにiPhoneにダークモードが登場か、バッテリー消費を劇的に抑えられるかも
http://gigazine.net/news/20190203-ios-13-dark-mode/

↓Chromeでもダークモード???

Chrome Canary now supports dark mode in Windows 10 and respects light/dark mode Setting
https://techdows.com/2019/02/chrome-canary-now-supports-dark-mode-in-windows-10-and-respects-light-dark-mode-setting.html

個人的に出しているニュースリーダーアプリにも1月にダークモードを実装したので
そのときに得たデザインのノウハウや実装方法についてまとめます。

↓こんなの
  

デザインについて

ダークモードの色について

Android Deveopers スタイルとテーマ
https://developer.android.com/guide/topics/ui/themes?hl=ja

Androidのテーマには大きく黒背景(Dark)と白背景(Light)があります。
コンポーネントごとに個別のカラー設定も可能ですが、
通常はThemeとしてカラーなどの表示をまとめて設定します。

style.xml
<resources>
    <!-- styleでテーマを書いていきます -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" />
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
</resources>

AndroidManifest.xml
:
        <!-- android:themeで使用するテーマを指定します -->
        <activity
                android:name=".MainActivity"
                android:label="@string/app_name"
                android:theme="@style/AppTheme.NoActionBar">
:

Android Studioでプロジェクトを作成するとデフォルトとしてTheme.AppCompat.Light.DarkActionBarが設定されています。
一切設定をしていない素のTheme.AppCompat.Light の画面キャプチャは以下となります。

style.xml
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light" />
</resources>

style.xml
<resources>
    <style name="AppTheme" parent="Theme.AppCompat" />
</resources>

上記の色はサポートライブラリの以下xmlが使用されています。

colors_material.xml
https://chromium.googlesource.com/android_tools/+/HEAD/sdk/extras/android/support/v7/appcompat/res/values/colors_material.xml

カラーコードを表にしてまとめると以下のようになります。
(xml上はARGBカラーコード表記ですが、QiitaにRGBカラーコード表示機能があるためA100%のものはRGBに省略しました)

material_dark material_light
foreground #ffffff (white) #000000 (black)
background #303030 (grey_850) #fafafa (grey_50)
background_floating #424242 (grey_800) #ffffff (white)
primary #212121 (grey_900) #f5f5f5 (grey_100)
primary_dark #000000 (black) #757575 (grey_600)
ripple #33ffffff #1f000000
accent #80cbc4 (deep_teal_200) #009688 (deep_teal_500)
button #5a595b #d6d7d7
primary_text_default #ffffffff #de000000
secondary_text_default #b3ffffff #8a000000

Material Design / The color system
https://material.io/design/color/the-color-system.html

上記Material Designサイトでは表記されていないgrey_850なども使用されています。
Floating Action Buttonにも使用されているaccentカラーは
lightでは500ベース, darkでは200ベースがデフォルトのようです。

ダークモードのアイコンについて

YouTubeやTwitterでアイコンが表示されていますが共通しているのは欠けた月をモチーフとしています。
YouTubeはMaterial Designの brightness_4 を使用しています。
Twitterの右上が欠けた月はMaterial Designにないオリジナルのアイコンです。

Material Design / Icons
https://material.io/tools/icons/

YouTubeのアイコン
スクリーンショット 2019-01-20 23.57.35.png

Twitterのアイコン
スクリーンショット 2019-01-20 23.57.13.png

私は個人のアプリでは brightness_2 を使用しました。
Material Designサイトの以下のあたりがおすすめです。

↓このへん
スクリーンショット 2019-03-01 0.24.25.png

開発について

夜間モードリソースを使う実装方法がオススメ

Theme切り替えはいくつかの実装方法がありますが、「夜間モード」を用いると簡潔に行うことができます。
Androidでは2010年に公開されたAndroid2.2(v8)からカーナビアプリ・デスクトップ向けアプリ向けに、
夜のみDarkテーマを使用する「夜間モード」があります。

リソースの提供 - 夜間モード
https://developer.android.com/guide/topics/resources/providing-resources#NightQualifier

Android Developers Documentation - UiModeManager
https://developer.android.com/reference/android/app/UiModeManager#setNightMode(int)

リソース末尾に-nightを付与することで昼間向けテーマと夜間向けテーマを設定することができます。

  • res/values/themes.xml
  • res/values-night/themes.xml

上記の「夜間モード」はAndroid6.0(v23)からモバイルアプリでも使用できるようになりました。
切替方法は後述します。

DayNightテーマ

Medium - Android Developers "AppCompat — DayNight"
https://medium.com/androiddevelopers/appcompat-v23-2-daynight-d10f90c83e94

夜間モード切り替えは全体であればAppCompatDelegate.setDefaultNightMode()特定のAppCompat内であればAppCompatDelegate.setLocalNightMode()で行います。
Android9からは「システム設定に従う」が追加され、デフォルト設定になっています。

AppCompatDelegate.setDefaultNightMode()

  • MODE_NIGHT_NO. Always use the day (light) theme.
  • MODE_NIGHT_YES. Always use the night (dark) theme.
  • MODE_AUTO - !!Soon to be deprecated!!
  • MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which on Android Pie and above is a system setting (more on this below).

また、昼間であればTheme.AppCompat.Light、夜であればTheme.AppCompatを使用するDayNightテーマがサポートライブラリのv23から追加されています。

theme.xml
<style name="MyTheme" parent="Theme.AppCompat.DayNight">
</style>

ユーザが自発的に夜間モードテーマを切り替える場合、
切り替え後は Activity#recreate()を呼び出すとテーマが適応されます。

【補足】MaterialComponent.DayNightテーマ

Medium - Android Developers "DayNight — Adding a dark mode to your app"
https://medium.com/androiddevelopers/appcompat-v23-2-daynight-d10f90c83e94

ちょうどこの記事を書いていた2019年2月に上記のMediumが更新されました..!
タイトルもダークモード関連に変更..!

ざっくりと変更点は
Theme.MaterialComponents.DayNight1.1.0 から使える。
AppCompatDelegate.setDefaultNightMode()MODE_AUTOは非推奨にする。
とのことです。

個人的な感想

実際に開発して画面を見比べてみて、ダークモード実装が映えるのは
値段の高い端末よりも安価なディスプレイを搭載した端末のように感じました。
Android Go端末やノングレア液晶のChromebookだと白背景よりも黒背景のほうが格段に見やすくなったように感じます。

個人的に、去年にダークモードがない理由でアプリに低レビューをいただいた事がありました。。
今年以降、ダークテーマがアプリの必須機能かのようになる可能性もあります。。

ダークモードの実装は容易ですが、Colorの整理に時間が取られました。
リソースの整理は日頃やっておくと良いかもしれません。

おわり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ProgressTimeLatchとは何なのか

DroidKaigiのカンファレンスアプリのソースコードを見ていたら、 ProgressTimeLatch なるものが使われており、何だろうと思い調べてみると、色々と便利なやつだったので投稿します。

端的にいうと、プログレスバーなどを使っている時に起こるチラつきを、いい感じに制御してくれるやつでした。

ProgressTimeLatchとは

調べてみると、下記のツイートで紹介されたのが最初のようです。(おそらく)
Chris Banes氏と言えば、色々ライブラリを公開されており、お世話になった方も多いのではないかと思います。
https://twitter.com/chrisbanes/status/927928428539580416

ここで ContentLoadingProgressBar なるもの出てきますが、そもそも ProgressTimeLatchContentLoadingProgressBar の機能をどんなViewでも手軽に使えるように作られたもののようです。

ちなみに ContentLoadingProgressBar はSupport Libraryに入っているらしいです。(知らなかった…)
https://developer.android.com/reference/android/support/v4/widget/ContentLoadingProgressBar

どう便利なのか

アプリを作っていると、下記のようなことがあるかと思います。

  1. API通信などの非同期処理をする。
  2. 処理をしている間は、プログレスバーなどを表示して、処理中を表す。
  3. 処理が終了したら、プログレスバーなどを非表示にする。

あるあるの処理だと思います。

しかしながら、この処理の小さな問題として、APIの通信が一瞬で終わった場合は、プログレスバーの表示・非表示が一瞬で行われ、ユーザーとしては何かがチラついたように感じることがあるかと思います。

これをいい感じに制御してくれるのが ProgressTimeLatch というわけです。

使い方

かなりシンプルなクラスなので、ソースコードを読めばなんとなくわかるかと思いますが、軽く解説。
( ソースコードはこちら→ https://github.com/chrisbanes/tivi/commit/96e7cae7560ffd358b8c58c47267ed1024df53f6

class ProgressTimeLatch(
        private val delayMs: Long = 750,
        private val minShowTime: Long = 500,
        private val viewRefreshingToggle: ((Boolean) -> Unit)
)

コンストラクタは上記のような感じになっています。

delayMs
Viewを表示するまで遅延させる時間をセットします。

minShowTime
Viewの最低限の表示時間をセットします。

viewRefreshingToggle
Viewの表示・非表示のLambdaをセットします。

既存のコードに適用する場合は、こちらのdiffが参考になりそうです。対象のViewをラップするようにして、今まで表示・非表示を切り替えていたところで、 refreshing を切り替える形。

いい感じに制御してくれる例

下記のような場合を想定します。

ProgressTimeLatch(delayMs = 500, minShowTime = 500) {
 // ProgressBarの表示・非表示
}

・非同期処理が早く(100ms)終わった場合
 → delayMs より小さいのでProgressBarは表示されない(チラつかない)

・非同期処理がやや早く(600ms)終わった場合
 → delayMs より大きいのでProgressBarは表示される
 → minShowTime が残っているので、残り時間分表示して非表示になる(チラつかない)

・非同期処理が遅く(2000ms)終わった場合
 → delayMs より大きいのでProgressBarは表示される
 → delayMsminShowTime の合計以上なので、元の処理が終わったタイミングで非表示になる(チラつかない)

という感じです。便利!

まとめ

  • ProgressTimeLatch はチラつきをいい感じに制御してくれるやつだった
  • 使い方もシンプルなので、使っていきたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android で Watson Assistance(旧Conversation)

gradleの設定
gradel(App.module)へ下記を追加

implementation 'com.ibm.watson.developer_cloud:android-sdk:0.5.0'
implementation 'com.ibm.watson.developer_cloud:java-sdk:6.14.0'

[IAM Key]
認証が少しややこしいです。
以前はusername, passwordで認証していたようですが、現在は仕様が変わりIAMキーにて認証を行います。
watsonの設定はIBM Cloudのダッシュボードから行え、対象サービス(Assistance)の設定ページでIAMキーを確認できます。
このキーを使って認証しますので、他人に使われると、あなたのアカウントに課金される可能性があります。

[EndPoint]
IAMキーとセットで使われるのが、EndPointです。
アクセスするURIなのですがAssitanceの場合は複数 候補があります。
分からなくなったら、設定ページで確認できます。
サーバーがワシントンDCの場合
https://gateway-wdc.watsonplatform.net/assistant/api
となります。

[WorkSpace]
これがどこをどう探しても見つからず苦労しました。
IBMのハンズオンでは設定ページに記載があることになっていますが、現在のページにはその欄は無くなっており、確認できません。
Create a skill -> 対象のインスタンス と進むと、Assistanceの会話を構成するツールが起動します。
その時のブラウザ上のURIの中にWorkSpaceIDが潜んでいます。
~workspaces/ ここ /build~ にあります。

IBMのwatsonで提供される各機能は各種言語向けのサンプルコードを伴った解説(英語)が用意されています。
APIレファレンス
https://console.bluemix.net/apidocs/assistant
https://www.ibm.com/cloud/watson-assistant/
IBMのチュートリアル(日本語)
https://cloud.ibm.com/docs/services/assistant?topic=assistant-getting-started#getting-started

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android で Watson Assistant(旧Conversation)

gradleの設定
android studioの場合
gradel(App.module)へ下記を追加

implementation 'com.ibm.watson.developer_cloud:android-sdk:0.5.0'
implementation 'com.ibm.watson.developer_cloud:java-sdk:6.14.0'

通信がありますので、AndroidManifest.xml へ下記を追記してください。
<uses-permission android:name="android.permission.INTERNET" />

Watson Assistant にはv1とv2があります。
それぞれ使用するAPIが違います。まずどちらのバージョンを使用するか設定画面で確認してください。
以下v1で運用する場合の説明となります。

[API Key]
認証が少しややこしいです。
以前はusername, passwordで認証していたようですが、現在はIAMに仕様が変わりAPIキーにて認証を行います。
watsonの設定はIBM Cloudのダッシュボードから行え、対象サービス(Assistant)の設定ページでIAMキーを確認できます。
このキーを使って認証しますので、他人に使われると、あなたのアカウントに課金される可能性があります。

[EndPoint]
IAMキーとセットで使われるのが、EndPointです。
アクセスするURIなのですがAssitantの場合は複数 候補があります。
分からなくなったら、設定ページで確認できます。
サーバーがワシントンDCの場合
https://gateway-wdc.watsonplatform.net/assistant/api
となります。

[WorkSpace]
これがどこをどう探しても見つからず苦労しました。
IBMのハンズオンでは設定ページに記載があることになっていますが、現在のページにはその欄は無くなっており、確認できません。
Create a skill -> 対象のインスタンス と進むと、Assistantの会話を構成するツールが起動します。
その時のブラウザ上のURIの中にWorkSpaceIDが潜んでいます。
~workspaces/ ここ /build~ にあります。

追記: 作ったskillを選ぶ画面の各スキルの枠の右上 3点ドット -> View API Detail で確認できました。

もっとも簡単なサンプル

SampleAssistant.java
private static final String WORKSPACE_ID = "";
private static final String IAM_Key = "";
private static final String URI = "";

~中略~

String sendMessage = "こんにちわ";  //ワトソンへ送信する文字列
IamOptions iamOptions = new IamOptions.Builder().apiKey(IAM_Key).build();
Assistant service = new Assistant("2018-09-20", iamOptions);
service.setEndPoint(URI);

InputData input = new InputData.Builder(sendMessage).build();

MessageOptions options = new MessageOptions.Builder(WORKSPACE_ID)
  .input(input)
  .build();

MessageResponse response = service.message(options).execute();

System.out.println(response);

このままでは、常にダイアログ ルートノードでの反応となります。
セッションを継続するためには、いくつか方法があるようですが、watson からの返信で受け取った context を次のリクエストに渡すやり方が、一番簡単だと思われます。

Context watsonContext = response.getContext();  //と受け取っておき
SampleAssistant2.java
MessageOptions options;
if(watsonContext != null) {
    options = new MessageOptions.Builder(WORKSPACE_ID)
      .input(input)
      .context(watsonContext)  //次のリクエストに渡す
      .build();
}else{
    options = new MessageOptions.Builder(WORKSPACE_ID)
    .input(input)
    .build();
}
MessageResponse response = watsonAssistant.message(options).execute();
watsonContext = response.getContext();

IBMのwatsonで提供される各機能は各種言語向けのサンプルコードを伴った解説(英語)が用意されています。
APIリファレンスV1
https://console.bluemix.net/apidocs/assistant
APIリファレンスV2
https://cloud.ibm.com/apidocs/assistant-v2
https://www.ibm.com/cloud/watson-assistant/

IBMのチュートリアル(日本語)
https://cloud.ibm.com/docs/services/assistant?topic=assistant-getting-started#getting-started

watsonはクレジットカードの登録無しでも、一定範囲内の利用は無料です。
AIサービス利用の感じをつかむため、ちょっと触ってみてはどうでしょう。
特に、Assistantは概念的にもわかりやすく、様々なサイトへ応用可能ですのでお勧めです。
Wordpress用のモジュールもあるようですよ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む