Android Projectの詳細を取得する

buildフォルダを探索して依存ライブラリやクラス一覧を取得するコードを書いていたのですがbuildVariants周りがややこしいためAndroid Projectの構造を取得するためのサンプルコードを書きました。

Android Projectの構造はbuilder-mode-2.1.2.jarの中にあるAndroidProjectが保持しています。
GradleConnectorAndriodProjectクラスを指定するとビルド生成物の出力先や依存ライブラリやsigningConfigなどのbuild.gradleに書かれている情報が取得できます。

詳しくはandroid-project-connect-exampleAndroidGradleConnectorSpec.scalaを参照してください。

Memo: A URL list of Android related repositories

Androidのtool寄りのコードを書くようになったのでメモ。
git cloneしたらめっちゃHDD容量持っていかれる。

ちなみにandroid.googlesource.co以下のrepositoryリストは以下のURLから取得可能です。
https://android.googlesource.com/?format=TEXT

tagと日付を表示するワンライナー

git tag -l | while read -r tag ; do COMMIT_HASH=$(git rev-list -1 $tag) && GIT_COMMITTER_DATE="$(git show $COMMIT_HASH --format=%aD | head -1)" && echo $tag $GIT_COMMITTER_DATE; done

参考

http://stackoverflow.com/a/25939259/2306073

CircleCIを使ってAndroidのテストを実行する

プライベートなリポジトリはdroneioでコンテナ作ってビルドから通知までやっていました。
GitHubのリポジトリは折角なのでCircleCIで回すことにしました。
その時に罠があったので紹介します。

cache_directoriesの罠

以下のように記載してました。

dependencies:
  cache_directories:
    - $ANDROID_HOME/platforms/android-23
    - $ANDROID_HOME/build-tools/23.0.1
    - $ANDROID_HOME/extras/android
    - $ANDROID_HOME/extras/google

$ANDROID_HOME/usr/local/android-sdk-linuxと展開されると期待したら、以下のようなwaringが出ていました。

Warning: circle.yml specified cache directories: /home/ubuntu/android-ci/$ANDROID_HOME/build-tools/23.0.1, /home/ubuntu/android-ci/$ANDROID_HOME/extras/google, /home/ubuntu/android-ci/$ANDROID_HOME/platforms/android-23, /home/ubuntu/android-ci/$ANDROID_HOME/extras/android but they don't exist

/から始まるものでないと絶対パスとして扱われない…?罠過ぎる。
変数展開を期待してダブルクォーテーションで囲ってみたが相対パス扱いのままだったのでベタ書きすることにしました。

またandroid list sdk -u -a -eでupdate sdkに使うパッケージ名を出すことができます。

GitHub android-ci

参考

com.android.ide.common.process.ProcessException

見覚えのないエラーが出たので何かと思ったらretrolambdaの記述が足りてなかった…。

Error:Execution failed for task ':rx-okhttp-sample:dexDebug'.
> com.android.ide.common.process.ProcessException: org.gradle.process.internal.ExecException: Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1

足りていなかったのは以下の記述。

apply plugin: 'me.tatarka.retrolambda'

AndroidのテストをJUnit4に対応する

AndroidをJUnit4を使ったテストに変更します。

今までのテストは以下の通りにAndroidTestCaseを使っていました。

public class HogeTest extends AndroidTestCase {
    public void testHige() {
        Hige hige = createHige(getContext());
        assertThat("check hige", hige, is(notNullValue()))
    }
}

Developers – Building Instrumented Unit Testsの通りにbuild.gradleを設定し、上記のテストケースを書き換えます。

@RunWith(AndroidJUnit4.class)
public class HogeTest {
    @Test
    public void hige() {
        Hige hige = createHige(InstrumentationRegistry.getContext());
        assertThat("check hige", hige, is(notNullValue()))
    }
}

変わった点は、継承がなくなったためContextの取得方法が違う点といくつかのアノテーションの追加になります。
必要であればInstrumentationRegistryからContextをするようにとAndroidJUnitRunnerのドキュメントに記述されていました。

参考

AndroidのToolbarにスタイルを適用する

ActionBarを無効化

NoActionBarを継承したスタイルを作り、applicationのthemeに設定する。

Androidmanifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="epy0n0ff.com.toolbar"
    >

  <application
      android:allowBackup="true"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:theme="@style/AppTheme"
      >
    <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        >
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>

styles.xml

  <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">

  </style>

Toolbarの色を変更する

NoActionBarを継承したスタイルを作る。スタイルを適用しただけではだめなので、利用するActivityで必ず先にsetSupportActionBar(toolbar)しておく。

  • android:backgroundはToolbarの背景色
  • android:textColorPrimaryはToolbarのタイトルテキストの色
  • actionMenuTextColorはToolbarに設定したメニューのテキストの色

styles.xml

  <style name="ToolbarTheme" parent="Theme.AppCompat.NoActionBar">
    <!-- setSupportActionbar()で設定しないと適用されない -->
    <item name="android:background">#ffffff</item>
    <item name="android:textColorPrimary">#0000ff</item>
    <item name="actionMenuTextColor">#00ff00</item>
  </style>

プレビュー

こんな感じになります。
Toolbarにスタイル適用

[メモ]動画ファイルのContentResolverの戻り値

動画をギャラリーから開くIntent
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
onActivityResultで受けたIntentからContentResolverへ問い合わせ
Cursor cursor = getContentResolver().query(data.getData(), null, null, null, null);
cursor.moveToFirst();
for(int i=0; i< cursor.getColumnCount(); i++) {
  Log.d("movie", cursor.getColumnName(i) );
}
結果
_id
_data
_display_name
_size
mime_type
date_added
date_modified
title
duration
artist
album
resolution
description
isprivate
tags
category
language
mini_thumb_data
latitude
longitude
datetaken
mini_thumb_magic
bucket_id
bucket_display_name
bookmark
width
height

AndroidのmanifestPlaceholdersの使いどころ

build.gradleのbuildTypesとproductFlavors内でmanifestPlaceholdersと変数展開について軽くまとめてみる。

今のところ使った場面は以下の4つです。

  • GCMのPermission
  • GCMのIntentFilter
  • AndroidManifestでSearchRecentSuggestionsProviderの定義
  • android:schemeのホスト名

build.gradleを以下のように設定している場合についてそれぞれ説明します。

build.gradle

android {
...

  buildTypes {
    release {
      manifestPlaceholders = [hostName: "epy0n0ff.com"]
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    develop {
      debuggable true
      signingConfig signingConfigs.debug
      manifestPlaceholders = [hostName: "dev.epy0n0ff.com"]
      applicationIdSuffix '.develop'
    }
    local {
      debuggable true
      signingConfig signingConfigs.debug
      manifestPlaceholders = [hostName: "local.epy0n0ff.com"]
      applicationIdSuffix '.local'
    }
    debug {
      debuggable true
      signingConfig signingConfigs.debug
      manifestPlaceholders = [hostName: "dev.epy0n0ff.com"]
      applicationIdSuffix '.debug'
    }
  }

...
}

GCMのPermission

GCMのPermission要素にはapplicationIdを含みます。
buildTypes毎にAndroidManiefst.xmlを用意してもいいのですが、事故を減らすために
main/AndroidManifest.xmlを以下の用に変更します。

AndroidManifest.xml(適用前)

  <permission android:name="com.epy0n0ff.develop.permission.C2D_MESSAGE"
      android:protectionLevel="signature"/>
  <uses-permission android:name="com.epy0n0ff.develop.permission.C2D_MESSAGE"/>

AndroidManifest.xml(適用後)

  <permission android:name="${applicationId}.permission.C2D_MESSAGE"
      android:protectionLevel="signature"/>
  <uses-permission android:name="com.epy0n0ff.develop.permission.C2D_MESSAGE"/>

GCMのIntentFilter

GCMの設定には前項以外にもIntentFilterにapplicationIdが使用されています。
ここもapplicationIdの変数を使うように変更します。

AndroidManifest.xml(適用前)

    <receiver
        android:name="com.epy0n0ff.GCMReceiver"
        android:permission="com.google.android.c2dm.permission.SEND"
        >
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
        <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>

        <category android:name="com.epy0n0ff.develop"/>
      </intent-filter>
    </receiver>

AndroidManifest.xml(適用後)

    <receiver
        android:name="com.epy0n0ff.GCMReceiver"
        android:permission="com.google.android.c2dm.permission.SEND"
        >
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
        <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>

        <category android:name="${applicationId}"/>
      </intent-filter>
    </receiver>

AndroidManifestでSearchRecentSuggestionsProviderの定義

見落としがちというか気づきにくいのがSearchRecentSuggestionsProviderの定義です。
Logcatで見ていると以下のようなエラーが出るので修正します。

/com.epy0n0ff.develop E/ActivityThread﹕ Failed to find provider info for com.epy0n0ff.content.provider.search

AndroidManifest.xml(適用前)

    <provider
        android:name=".content.provider.RecentSearchSuggestionProvider"
        android:authorities="com.epy0n0ff.content.provider.search"
        android:exported="false"
        />

AndroidManifest.xml(適用後)

    <provider
        android:name=".content.provider.RecentSearchSuggestionProvider"
        android:authorities="${applicationId}.content.provider.search"
        android:exported="false"
        />

android:schemeのホスト名

ここでやっとmanifestPlaceholdersの登場です。
manifestPlaceholdersはkey-valueのmapなので${key}と書くとそこにvalueが展開されます。

AndroidManifest.xml(適用前)

    <activity
        android:name=".view.activity.UrlInterpreterActivity"
        android:launchMode="singleTask"
        android:noHistory="true">
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>

        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>

        <data
            android:host="develop.epy0n0ff.com"
            android:pathPattern="/sp/.*"
            android:scheme="http"/>
    </activity>

AndroidManifest.xml(適用後)

    <activity
        android:name=".view.activity.UrlInterpreterActivity"
        android:launchMode="singleTask"
        android:noHistory="true">
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>

        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>

        <data
            android:host="${hostName}"
            android:pathPattern="/sp/.*"
            android:scheme="http"/>
    </activity>

参考

apktoolでエラー

apktoolでデコンパイルしようと以下の様なエラーが出た。

apktool d hoge.apk
...
W: Could not decode attr value, using undecoded value instead: ns=android, name=touchscreenBlocksFocus, value=0xffffffff
...

どうやらframework.apkが古いようなのでnexus5から引っ張ってきて入れ替える。

adb pull /system/framework/framework-res.apk
apktool if framework-res.apk

これで無事デコンパイルできるようになった。

参考

[電話] apktoolでapkを弄り回す日々。そのいち

RxAndroidで勘違いしていたこと

Observable.subscribe()したら必ずsubscribe()の戻り値であるSubscriptionのunsubscribe()を呼ばなければいけない

そう思っていた時期が僕にもありました。
Subscriptionのleakが起きるのはonCompletedが呼ばれないObservableをsubscribe()したままにしてしまう場合です。以下のコード例はonCompletedが呼ばれてunsubscribeされるので明示的に呼び出す必要はありません。
ただし、Activityのライフサイクルより処理が長くなる場合は不要な処理となるのでCompositeSubscription等を使ってonPause()で明示的にunsubscribe()してあげましょう。

Observable.just(...).subscribe(...);
Observable.from(...).subscribe(...);
Observable.create(new Observable.OnSubscribe<Object>() {
    @Override
    public void call(Subscriber<? super Object> subscriber) {
        subscriber.onNext(new Object());
        subscriber.onCompleted();
    }
}).subscribe(...);
Observable.create(new Observable.OnSubscribe<Object>() {
    @Override
    public void call(Subscriber<? super Object> subscriber) {
        subscriber.onError(new IllegalStateException());
    }
}).subscribe(...);

参考

Top 7 Tips for RxJava on Android