6日間でAndroidアプリを作って公開した記録

# この文章について

[カメラのシャッター音を消すアプリ](https://play.google.com/store/apps/details?id=cameramute.app.cameramute&hl=ja)を6日間で作成した時のログです。

作成中にどんなことを考えたか、どう問題を解決したか、どんなサイトやツールを使って開発したかを
綴っています。

日付は2018年です。時刻はブラウザの履歴から調べました。

# 一日目 (6/13 水) 技術調査

## 23:07 AudioTrackの調査

前々から、AudioTrackを開きまくれば、カメラアプリはシャッター音を鳴らせなくなるのでないか？
と考えていた。

ふとやる気になったので、調査用のプログラムを作ることにする。
この時点では、アプリを公開することは考えてはいなかった。

[AudioTrackのドキュメント](https://developer.android.com/reference/android/media/AudioTrack)を見ながら、以下のソースコードを作成。

```kotlin
findViewById<Button>(R.id.openButton).setOnClickListener {
            val tracks = ArrayList<AudioTrack>()
            val minBufSize = AudioTrack.getMinBufferSize(44100, CHANNEL_OUT_STEREO, ENCODING_PCM_FLOAT)
            for (i in 0..5000) {
                try {
                    val player = AudioTrack.Builder()
                            .setAudioAttributes(AudioAttributes.Builder()
                            // 省略
                            .build()
                    player.play()
                    Log.i("player", "state=" + player.state + " state2=" + player.playState)
                    tracks.add(player)
                } catch (e: Exception) {
                    Log.i("player", "cannot create", e)

                    Thread.sleep(1000)
                }
            }
        }
```

このアプリを実行すると、すぐにオープン数の限界に達するので、カメラを起動して撮影してみる。
結果は以下の通り
- Nexus5(Android6): 成功(カメラの音が鳴らなかった)
- Pixel(AndroidP): 失敗(シャッター音が鳴った)

どうやら、普通にオープンしまくるだけではダメなようだ。残念。

## 23:40 Androidのソースの調査

アプリごと(プロセスごと)にオープンできるAudioTrackの数に制限があるのだろうか。この制限がどこから来るのかがわかれば、制限を回避する方法もわかるかもしれない。

logcatに、

```
no more tracks available
```
というエラーメッセージが出ていたので、この文字列を元に[Androidソースコード検索サービス](https://sites.google.com/site/devcollaboration/codesearch)
で検索。が、残念ながらヒットせず。AOSPには含まれないデバイスドライバが出力しているようだ。

このサイトは、Android2.2から最新版まで、多数のバージョンのコードを全文検索でき、非常に便利なので愛用している。
[android.googlesource.com](https://android.googlesource.com/)は、なぜ検索ができないのか。検索の会社なのに。

話を戻すと、全文検索でヒットしなかったので、適当に以下のキーワードでググってみた。
- "android audio process open limit"
- "android audio mixir process"

すると、Android7からは/system/etc/audio_policy_configuration.xmlなるXMLファイルが端末内に存在するらしいことがわかった。
https://source.android.com/devices/audio/implement-policy

その中に、オープンできるAudioTrack数が記載されているのでは？と思ったのだが、実機内には/system/etc/audio_policy_configuration.xmlを見つけることができず。

Androidのソースの中に[audio_policy_configuration.xml](https://android.googlesource.com/device/google/dragon/+/7df7ec13b1d222ac3a66797fbe432605ea8f973f/audio_policy_configuration.xml)を発見したが、特にヒントに鳴るようなことは書いていなかった。


## 23:52 他のアプリを調べる

ちょっと調べるのに飽きてきた。

そもそも、ひょっとすると音を消すアプリは既にあるのかもしれないと思い、アプリを検索することにした。
キーワードは以下
- "android shutter japan" (英語のアプリを探すため)
- "android カメラ シャッター音 消す"

すると、[Mute Mode](https://play.google.com/store/apps/details?id=com.meiling.makemutecamera)なるアプリを発見。
早速インストールして試す。確かにシャッター音を消せている。でも一体どうやって？？

## 24:16 音を消す方法がわかった

logcatとにらめっこしていると、以下のログが出てくることに気づいた。

```
Accessing hidden method Landroid/media/AudioManager;->setMasterMute(ZI)V (light greylist, reflection)
```
ん？AudioManagerのsetMasterMuteというmethodを呼んでいる。すごくそれっぽいメソッド名。これで行けそうだ。

なお、Android Pからは、[SDKのhiddenなmethodにはアクセスできない](https://developer.android.com/preview/restrictions-non-sdk-interfaces?hl=ja)ので、reflectionでhiddenなメソッドを呼ぼうとすると、上記のようなlogが出力される。

hiddenなメソッドの説明を読むには、ソースコードを読むしか無い。早速、[AudioManagerのドキュメント](https://developer.android.com/reference/android/media/AudioManager)を開く。[Android SDK Search](https://github.com/romannurik/AndroidSDKSearchExtension)というchromeの拡張を入れていると、以下のようにクラス名の下に"view source"というリンクが表示されるので、手軽にソースコードを参照できて便利。

![android_source_search.png](https://qiita-image-store.s3.amazonaws.com/0/91308/322687f1-6088-79d9-8bce-d60968ad00c8.png)



AudioManager.javaを読むと、確かに[setMasterMute()](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioManager.java#851)というmethodが```@hide```で存在している。よし、これを試してみよう。

ところで、logcatには、light greylistと書いてあった。

```
Accessing hidden method Landroid/media/AudioManager;->setMasterMute(ZI)V (light greylist, reflection)
```

light greylistとはどういう意味だ？将来的にはreflectionでも呼べなくなるということなのか？

https://developer.android.com/preview/restrictions-non-sdk-interfaces
に、以下の説明があった。

```
- whitelist: the SDK
- light-greylist: non SDK methods / fields that are still accessible.
- dark-greylist:
  - For apps whose target SDK is below P: each use of a dark greylist interface is permitted.
  - For apps whose target SDK is P or above: same behavior as blacklist
- blacklist: restricted regardless of target SDK.
```

light greylistのメソッドは、少なくともすぐに使えなくなるということはなさそうだ。一安心。
これでアプリをつくる目処は立った。寝よう。

## 24:54 寝た

# 二日目 (6/14 木) 何もしなかった

特に何もしなかった。

# 三日目 (6/15 金) アプリのコアの機能を作成

## 21:08 ビルドエラーに悩む

Android Studioで新規プロジェクト作成。
EmptyActivityのテンプレートでプロジェクトを作成し

```
    dataBinding {
        enabled = true
    }
```
とdatabindingを使う指定をいれたのだが、どういうわけか

```
Unresolved reference: databinding
```
とでて、ビルドが通らない。
しばらく悩む。

## 21:15 ビルドエラー解決

ふとbuild.gradleを見ると、

```
dependencies {
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
```
と、alpha版が指定されていることに気づく。

テンプレートで作ったアプリのビルドが通らないことはAndroid Studioでは良くあることなので、ここで腹を立ててはいけない。

```
dependencies {
    implementation 'com.android.support:appcompat-v7:27.1.1'
```
と、27.1.1にしたらコンパイルが通るようになった。

## 21:22 カメラアプリが起動していることを検知する方法を

次は、カメラアプリを起動している時だけmuteしたいので、その方法を探す。

ActivityManagerに今動いているアプリ(task)を取れるAPIがあったはず。一番上のtaskがカメラアプリなら、muteすればよいだろう。
と思い、[ActivityManagerのドキュメント](https://developer.android.com/reference/android/app/ActivityManager.html#getRunningTasks(int)
)を読んだ。

```
This method was deprecated in API level 21.
As of Build.VERSION_CODES.LOLLIPOP, this method is no longer available to third party applications
```

え…使えなくなっている…。

仕方ない。別の方法を考えよう。

Cameraの状態をフックできないだろうかと、[Service一覧](https://developer.android.com/reference/android/content/Context)から"camera"というキーワードで検索。
CAMERA_SERVICE
というのがあった。CameraManagerを取得できるらしい。

今度は[CameraManagerのドキュメント](https://developer.android.com/reference/android/hardware/camera2/CameraManager)を見てみると、[registerAvailabilityCallback](https://developer.android.com/reference/android/hardware/camera2/CameraManager#registerAvailabilityCallback(android.hardware.camera2.CameraManager.AvailabilityCallback,%20android.os.Handler))というメソッドを発見。
- onCameraAvailable
- onCameraUnavailable

という２つのcallbackが呼ばれるらしい。

カメラアプリが起動すると、おそらくそのアプリがカメラをオープンするので、onCameraUnavailableが呼ばれてくるのだろう。ということは、カメラアプリが起動したことを検知できることになる。これは使えそうだ。

ところで、registerAvailabilityCallbackはBroadcast Intentではなく、ただのcallbackである。つまり、カメラの起動を検知するためには、自アプリはbackgroundで動作し続けている必要がある。ということはkillされないようにforegroundサービスにする必要がある。

foregroundサービスにするのって、どうやってやるんだったっけ？

## 21:34 serviceの復習

[serviceのドキュメント](https://developer.android.com/reference/android/app/Service)を呼んで、復習。
サービス起動後にstartForeground()を呼べばforegroundサービスになるのか。

そういえば、Oreoでバックグラウンドで動くserviceに対しての制限が厳しくなったんだったっけ。
というわけで、[Oreoでの制限のドキュメント](https://developer.android.com/about/versions/oreo/background)も読んでおく。


## 21:59 アプリを書き始める

これで、だいたい調べ物は終わった。実際にシャッター音を消すアプリの作成に取り掛かることにする。
このあたりで、アプリをPlay Storeに公開しようかと思い始める。

AudioManager.setMasterMuteはreflectionでアクセスする必要があるので、Javaで困ったときの定番、ひしだま's 技術メモページの[reflectionの解説](http://www.ne.jp/asahi/hishidama/home/tech/java/reflection.html)を読む。

コーディングを行い、カメラアプリが動作している間だけ自動でmute/unmuteする機能は実装できた。
風呂に入ろう。

## 23:19 Notificationが出ない

うーむ。startForeground()でforeground serviceにするとNotificationが出るはずなのだが、出ない。
OreoからNotificationChannelの設定をしないとNotificationが出なくなったが、設定をミスっているっぽいのだが、何が悪いのかさっぱりわからない。

"android foreground service icon not shown"
というキーワードでググり、出てきたサイトを片っ端から読む。

何をやったら出るようになったのか忘れたが、いろいろやっていたら出るようになった。

## 23:27 Notificationアイコンの検討

基本機能は動いてきたので、アイコンをどうしようか考え始める。
[Material Designのサイト](https://material.io/tools/icons/?style=baseline)で、良さそうなアイコンが無いか探す。

カメラとスピーカーと無音のアイコンを適当に組み合わせれば良いだろうか。

![baseline_camera_black_48.png](https://qiita-image-store.s3.amazonaws.com/0/91308/a91fe1b9-7d7a-26a0-584b-996f1e3084e8.png)![baseline_voice_over_off_black_48.png](https://qiita-image-store.s3.amazonaws.com/0/91308/c5c26149-c25b-28a6-4912-c0acb0801e84.png)![baseline_volume_off_black_48.png](https://qiita-image-store.s3.amazonaws.com/0/91308/c166ea25-41e5-dcd5-1101-a00054192067.png)


## 23:41 ミッフィー風アイコンを思いつく

他のアプリのアイコンも参考にしようと思い、似たようなアプリのアイコンを見て考える。
意外と、プログラムを書くよりもアイコンなどを考えるほうが時間がかかる。

ふと、ミッフィーっぽい顔アイコンにしようかと思いたつ。ミッフィーの口は☓の形をしていてmuteっぽいので。
試しにGIMPで作ってみてアプリに組み込んでみた。なかなか良さそうだ。
![notificationxcf.png](https://qiita-image-store.s3.amazonaws.com/0/91308/7731cc3d-a136-d065-6acd-71924d48ea45.png)


## 24:53 寝た

# 四日目 (6/16 土) 細かい動作の調整

## 22:51 動作で気になる点を修正

ミッフィー風アイコンはいろいろな方面から怒られそうなので、やめることにした。やはり夜中に考えついたアイディアは冷静になって考えなおしたほうが良い。

また、一日アプリを使ってみていろいろと気になる点が出てきた。

- ロックスクリーンにNotificationが出てしまう。
→ロックスクリーンに出ている必要はないので、ロックスクリーンには出ないようにした。
- mute/unmuteのToast表示が長い。
→調べたが、Toastの表示の長さはSHORTとLONG以外にはないっぽい。

また、アウトカメラとインカメラとの切り替え時に、availability callbackはどう呼ばれるのかが気になったので、実験を行い、アルゴリズムを修正した。

## 25:06 寝た

# 五日目 (6/17 日) アイコンとUIの作成

## 22:18 再びアイコン検討

そろそろソースをgitで管理しようと、git init。
githubにもprivate repositoryを作成して、現状のソースをpush。

その後、アイコンの検討を行う。
アイコンを作成するときには、主に以下の２つのサイトを使っている
- [material.io](https://material.io/tools/icons/?style=baseline)
- [Noun Project](https://thenounproject.com/)

## 23:59 アプリのUI検討

アイコン検討に飽きてきたので、アプリのUIを考える事にする。

アプリには、自動mute機能のON/OFFしかないので、開始ボタンと停止ボタンがあれば良い。
(ボタンひとつでトグルでも良いが、今回は２つの別のボタンにした)
開始ボタンは目立つデカイボタンにして、アプリのベースカラーにしよう。

アプリのベースカラーは、[Material designのパレット](https://material.io/design/color/the-color-system.html#color-theme-creation)からオレンジを選択。
最初青系にしようと思ったが、ツールアプリには青はありふれた色なので、あえてオレンジにしてみた。

テーマカラーは、
- colorPrimary
- colorPrimaryDark
- colorAccent

の3色を選ぶ必要がある。

colorPrimaryは前述の通りオレンジ(#F57F17)にした。
colorPrimaryDarkは、GIMPで適当にオレンジを暗くして作成。
colorAccentは、"オレンジ 補色"でググッて、適当に青っぽい色を選択。

最終的に、以下のようなUIになった(colorAccentの色は使われていないが)。

![home_small.png](https://qiita-image-store.s3.amazonaws.com/0/91308/83f5339b-0d29-811e-e1a3-0b78f9c71c27.png)

なお、アプリ名は「シャッター音消し」にした。

## 24:35 アプリのアイコン作成

アプリのアイコンは、カメラのシャッター画像に斜めの線を入れたアイコンにすることにした。
最初、慣れているGIMPで作ろうと思ったが、svgでの作成に初チャレンジ。新しいことをやらないと飽きるので。

svgの編集は、オンラインで使える[Gravit Designer](https://designer.io/)を使ってみることにした。これも初挑戦。

画像のベースは[カメラのアイコン](https://material.io/tools/icons/?icon=camera&style=baseline)のsvgをダウンロードして、Gravit Designerで編集。
アイコンの色は、適当にグラデーションのパレットをググって、それに合わせて色を付けた。

![launcher.png](https://qiita-image-store.s3.amazonaws.com/0/91308/ec68542c-7873-50b9-1ca8-9a940d289573.png)


## 25:29 寝た

# 六日目 (6/18 月) Play Storeへリリース

## 22:05 Play Storeに出す準備

アプリは完成したので、Play Storeに出す準備を行う。

Play Storeに出すときに一番悩むのが、1024x500の宣伝用画像。
プロの写真を使ってその上に文字を入れるのが手っ取り早いので、[fotolia](https://jp.fotolia.com/)で良さそうな画像を探す。

音を消している感を出すために、寝顔の写真を探し、以下の二点を購入。6クレジットx2で計2200円。

![fotolia1_300.jpg](https://qiita-image-store.s3.amazonaws.com/0/91308/c2ffd596-79c9-03a4-8097-e4bb63703581.jpeg)
![fotolia2_300.jpg](https://qiita-image-store.s3.amazonaws.com/0/91308/4e89c189-151f-c7e4-76f5-46dc03a67c16.jpeg)

一枚目は、日本での配布用。「日本人が開発している感」を出すために、意図的に日本風の写真にした。
二枚目は、日本以外の国での配布用。

これらの写真に文字を重ねて、完成。

![fotolia1_with_word_300.png](https://qiita-image-store.s3.amazonaws.com/0/91308/87a08cc1-b7c4-bea9-2bd1-22f1b50db47a.png)
![fotolia2_with_word_300.png](https://qiita-image-store.s3.amazonaws.com/0/91308/5fe46be2-4b45-b7b3-984d-8284f8bf30e8.png)


## 25:05 リリース

Play Storeにリリース

## 25:10 寝た

# 最終的に完成したアプリ

[シャッター音消し](https://play.google.com/store/apps/details?id=cameramute.app.cameramute&hl=ja)という名前でリリース。