AndroidでGroovyを使ってみる

はじめに


現在常用のスマホとしてAndroid端末を使っています。
(つい半年前まではiPhoneを使っていました)

端末機としては海外版、XperiaZ3 Compactです。
(昨今の大型化している流れの中軽量かつ音楽プレイヤーとしても優れている機種として選定しました。)

元々iPhoneユーザーかつ環境が全てApple製品で統一されているので、音楽管理もiTunesで行っています。

そういう状況下であったので、iSyncrというアプリでiTunesのプレイリストと端末を同期させて利用していました

どうもXperiaZ3 Compact側のイヤホンジャック(ヘッドホンジャック)が壊れたようで、
音楽プレイヤーとしてはまともには利用できなくなりました

とはいえ、Youtubeを見て修理するのはリスクが高いのと、パーツが個人輸入になるので調達までのハードルが高いので修理方向は諦めました

が、どうも最近のwalkmanでは、microSDを使えプレイリストを少し調整すれば認識できるようなことを聞きつけました
(店頭モックでも、プレイリストを認識し再生可能であることを確認しました)
なので、microSDとの同期はiSyncrに任せ、同期したプレイリストの変更を独自のアプリでやればいいのではないかと思い今回やってみました

やりたいこと


まずはiSyncrで作ったプレイリストがウォークマンから認識できるかどうかを確認しました。
結論的に言うとそのままでは不可能で、一旦編集が必要のようです。

どうやらiSyncrを使ってiTunesの曲を転送したときは、iSyncrの作ったm3uプレイリストを正しく認識出来ないようです
これは、ウォークマン側のプレイリスト内のパスの解釈の問題のようです。
対処方法としては、パスの先頭の「/」を削除することで認識させることできます
(ウォークマン内部では、パスが補完されているのかもしれませんがその辺りは不明です)

こんな感じになるようにします

1
2
/hoge/fuga.mp3     -> hoge/fuga.mp3
/hoge/fugafuga.mp3 -> hoge/fugafuga.mp3

ということで実装としてやりたいことは至ってシンプルです

  • 編集後のプレイリストファイルを作成する
  • iSyncrで作成したプレイリストの内容を、ほぼそのまま編集後のプレイリストに書き込む
  • この時、行の最初の文字が「/」の場合、上記のように先頭の「/」を空文字に置換する

何故Groovy


Javaで書くと、以下の内容が面倒だったりするわけなのです

  • StreamだとかWriterやらReaderやらでコード自体が肥大化、冗長化すること
  • ファイルの中身の操作や置換とか結構大変で面倒

なので、今回その辺もサクッと実装できるGroovyで書いてみました

他にあるとすれば・・・

  • GroovyがAndroidでも動くということ
  • 置換検証用のコードをサクッと移植したかったこと
  • 個人的にGroovyでコード書くというリハビリ(書かなくなって久しいので)

当該Groovyコード


やりたいことを実施するファイルの置換用コードは以下のようになります

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def files = new File('Your Path').listFiles()
playList = files.findAll { it.getName() =~ /.*\.m3u$/ }

playList.each { file ->
  def playListName = file.canonicalPath

  // 変更版プレイリスト
  def newPlayListName = "${file.canonicalPath}.new"
  def newPlayListFile = new File(newPlayListName)

  // 変更版プレイリストに現行プレイリストの修正内容を出力
  newPlayListFile.withWriter('UTF-8') { writer ->
    file.newReader().transformLine(writer) { line ->
      if (line.startsWith("/")){
        // 行の最初の文字の場合、「/」を置換する
        line.replaceFirst(/\//,"")
      } else {
        // それ以外の場合はそのまま出力
        line
      }
    }
  }

  // ファイル名を変更する
  file.renameTo("${file.canonicalPath}.old")
  newPlayListFile.renameTo("${playListName}")
}

本当であれば上書き保存という形にしたいところですが、
元のファイルが無くなってしまうと今度は同期が取れなくなる可能性があるので別ファイルとしました

GroovyをAndroidに導入


まずはGroovyのインストールをします。
以下のような形でsdkmanを使いましょう

1
2
3
4
$ curl -s http://get.sdkman.io | bash
$ sdk ls groovy
$ sdk install groovy 2.4.5
$ sdk use groovy 2.4.5

次にAndroidStudioでAndoridプロジェクトを用意します
Androidプロジェクトに対して、groovy-android-gradle-pluginを導入します

プロジェクトROOTにあるbuild.gradleを以下のようにします
(プラグインを導入するのみです)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        classpath 'org.codehaus.groovy:gradle-groovy-android-plugin:0.3.6'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

次にアプリ側のbuild.gradleを変更します
(プラグインの適用と、コンパイルライブラリの追加、プラグインの設定追加)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apply plugin: 'com.android.application'
apply plugin: 'groovyx.grooid.groovy-android'

android {
    中略
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'org.codehaus.groovy:groovy:2.4.3:grooid'
}

project.androidGroovy {
    options {
        configure(groovyOptions) {
            encoding = 'UTF-8'
            forkOptions.jvmArgs = ['-noverify'] // maybe necessary if you use Google Play Services
        }
        sourceCompatibility = '1.7'
        targetCompatibility = '1.7'
    }
}

ソースの配置を「src/main/java/MainActivity.java」から「src/main/groovy/MainActivity.groovy」変更します

最後にソースを以下のようにしてGroovyライクに実装します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@CompileStatic
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        def hello = { lang ->
            def toast = Toast.makeText(this, "Hello ${lang}!", Toast.LENGTH_LONG)
            toast.setGravity(Gravity.CENTER, 0, 0)
            toast.show()
        }
        hello("Hello Groovy on Android!")
    }
}

一応いっておきますが、@CompileStaticアノテーションで静的コンパイルしなくても動作はします
が、動的コードはパフォーマンス的にきびしいので@CompileStatic推奨されているようです

ビルド時の注意点


Android Studioでビルドを実行したとき、エラーがあった場合以下のような形でEventLogに出力されてビルドが停止します。
メッセージを見ても、何が悪いか分かりません。正直、自分は困惑しました

1
2
3
4
5
6
7
0:52:06 Platform and Plugin Updates: The following components are ready to update: Google Play services, Google Repository, Android SDK Platform-tools
1:14:33 Gradle sync started
1:14:57 Gradle sync completed
1:14:57 Executing tasks: [:app:generateDebugSources, :app:generateDebugAndroidTestSources]
1:15:01 Gradle build finished in 4s 168ms
1:15:41 Executing tasks: [:app:generateDebugSources, :app:generateDebugAndroidTestSources, :app:compileDebugSources, :app:compileDebugAndroidTestSources]
1:15:47 ExternalSystemException: String index out of range: -105

そのときはGradle Consoleを確認しましょう
以下のような形でエラーメッセージが表示されています
(この場合、Syntaxエラーですね)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
:app:compileDebugJavaWithJavac UP-TO-DATE
:app:compileDebugGroovyWithGroovyc
startup failed:
MainActivity.groovy: 53: expecting ')', found '}' @ line 53, column 13.
               }
               ^

1 error


 FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugGroovyWithGroovyc'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 5.687 secs

因みに上記のような形になった場合、何故かそのままではビルドの再開が出来ませんでした。
対応としては一度「Clean Project」を実行する必要があります

結果的に・・・


ここまで読めば、分かる方はわかりますが実現はサクッとは出来ませんでした。

何故かと言うとAndroid4.4あたりから導入されたStorage Access Frameworkのことをスッカリ忘れていたのでやりたいことは出来ませんでしたorz

当初の運用自体は、iSyncrの同期先をSDカードにしてSDカードに対して同期
その後同期されたSDカードをPC側にマウントして、上記コードを通してやればよいので無理にAndroid経由させなくていいかなぁと思った次第です

とはいえGroovyでAndroidアプリを書いてみることはまぁできたのでよしとします。
GroovyでGroovy GDKの便利なAPIやクロージャの恩恵を最大限に受けてて、簡潔に書くことが出来て素晴らしいなと思いました