2billion cats, AndroidN EasterEgg, ねこをいっぱい作る ~HyperUltraCatGenerator~

# HyperUltraCatGenerator!
AndroidNのイースターエッグ、Neko Atsumeの猫をいっぱい作るアプリです。

![ezgif-3-747df78d5897.gif](https://qiita-image-store.s3.amazonaws.com/0/146245/86701684-0e9b-9296-7503-96002e80d7dc.gif)

### generate cat
左のNumberPickerを回すか右で指定することで任意のAndroidCatを生成できます。
タップで共有されます。

### save 1000 cats
from ~ toで指定した範囲の名前のAndroidCatを生成できます。
生成したAndroidCatはPictures/Catsに保存されます。

### リポジトリ
https://github.com/nekomimi-daimao/hyper-ultra-cat-generator

## コードの解説
コミットひとつひとつセルフコードレビューしながらこのクソアプリ作成の軌跡を辿ります。
人がどんなこと考えながら開発しているのか、を残しておくと楽しいかなと思って。
付き合ってくださる暇な方はリポジトリ眺めながら読んでください。

### initial commit
自分がアイコンに使っている画像はAndroidNのEasterEgg、Neko Atsumeから生成したものです。
ネットでも自分が集めた画像を晒し合ったり、わりと人気がある印象。
playStoreにもクローンゲームがいくつか上がっています。
[Android Nougat Easter Egg](https://play.google.com/store/apps/details?id=com.covertbagel.neko)
[Neko Easter Egg](https://play.google.com/store/apps/details?id=slaughter.neko)
本家やクローンゲームをやってて思ったんですが、AndroidCatが量産できないんですよね。
餌置いて待ったりタップして逐次生成したり……。
わたしは思いました。**いっぱいほしい。**

### import cats
まずActivity作ります。自動生成だとコンパイル通らないことがまれによくあるので自分はいつもEmptyActivityです。
https://android.googlesource.com/platform/frameworks/base/+/nougat-mr2.3-release/packages/EasterEgg
それっぽいリポジトリから必要そうなファイルを引っこ抜いてコンパイル通らないところは適当に書き換えます。
Cat.javaです。
なにはともあれコードを読むとCatは以下の箇所で生成されているようです。
基本的には`Cat.create()`でランダムに生成し、任意のAndroidCatが欲しい場合はintのseedをCatのコンストラクタに渡す……というところまで読んで終わり。
染色のアルゴリズムはなんかよくわかんないし別にいいかなって……。

```java:Cat.java

// ランダム生成はこれ
public static Cat create(Context context) {
    return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt()));
}

// 生成ロジックはコンストラクタ
public Cat(Context context, long seed) {
    D = new CatParts(context);
    mSeed = seed;
    setName(context.getString(R.string.default_cat_name,
            String.valueOf(mSeed % 1000)));
    final Random nsr = notSoRandom(seed);
    // body color
    mBodyColor = chooseP(nsr, P_BODY_COLORS);
    if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[] {
            nsr.nextFloat()*360f, frandrange(nsr,0.5f,1f), frandrange(nsr,0.5f, 1f)});
    tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail,
            D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap);
    tint(0x20000000, D.leg2Shadow, D.tailShadow);
    if (isDark(mBodyColor)) {
        tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose);
    }
    tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside);
    tint(chooseP(nsr, P_BELLY_COLORS), D.belly);
    tint(chooseP(nsr, P_BELLY_COLORS), D.back);
    final int faceColor = chooseP(nsr, P_BELLY_COLORS);
    tint(faceColor, D.faceSpot);
    if (!isDark(faceColor)) {
        tint(0xFF000000, D.mouth, D.nose);
    }
    mFootType = 0;
    if (nsr.nextFloat() < 0.25f) {
        mFootType = 4;
        tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4);
    } else {
        if (nsr.nextFloat() < 0.25f) {
            mFootType = 2;
            tint(0xFFFFFFFF, D.foot1, D.foot3);
        } else if (nsr.nextFloat() < 0.25f) {
            mFootType = 3; // maybe -2 would be better? meh.
            tint(0xFFFFFFFF, D.foot2, D.foot4);
        } else if (nsr.nextFloat() < 0.1f) {
            mFootType = 1;
            tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4));
        }
    }
    tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap);
    final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS);
    tint(capColor, D.cap);
    //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose);
    final int collarColor = chooseP(nsr, P_COLLAR_COLORS);
    tint(collarColor, D.collar);
    mBowTie = nsr.nextFloat() < 0.1f;
    tint(mBowTie ? collarColor : 0, D.bowtie);
}

```

### cat is a seekbar

[シークバーの使い方 / GETTING STARTED](https://techbooster.org/android/ui/8329/)
とりあえず使ったことないし……と思ってシークバーを使ってみる。シークバーのつまみに動的にAndroidCatを設定したら見た目が面白かったので採用。
[Android　SeekBar のトラックとつまみの位置を合わせる](http://y-anz-m.blogspot.com/2012/10/androidseekbar.html)
なぜかAndroidCatの位置が固定されてしまうのですが、まだ最初だし後で直せばいいかなと思っていったん放置。
シークバーの範囲は0~999。
そう、このときの私はまだ思っていたのです。
「AndroidCatの名前から推測するに、総数は# 0 〜 #999で1000だろう」って……。

### cat is a picker
[NUMBERPICKERで数値を入力する/GETTING STARTED](https://techbooster.org/android/ui/11631/)
もうひとつ、前から使ってみたかったNumberPicker。ぐるぐる回しているとAndroidCatがぐるぐる生成されてそれだけで楽しい。
最大値を`Integer.MAX_VALUE`にセットして、その先に回すと0になるはずがオーバーフローして次の数字が表示されなくなるのを確認。マイナスになってるっぽいです。バグな気はするけど、そんな無茶な使い方も想定されていないからしょうがないかな。

<img src="https://qiita-image-store.s3.amazonaws.com/0/146245/d10d6d7d-3ca3-5332-5347-93f828640b83.gif" width=50%>


欲しい数字までぐるぐる回すのも大変なのでジャンプ機能を追加。
がしかし。`mPicker.setValue(jump);`とやっても`onProgressChanged`が来ない。とりあえず検索してみたところ同じ疑問を抱いた人がいた。

https://stackoverrun.com/ja/q/10095347
*そうじゃねんだよなあ……。*

仕方ないので`NumberPicker.java`を読む。LiearLayoutを継承しててびっくりだけど納得。

```java:NumberPicker.java
    /*
     * @param value The current value.
     * @see #setWrapSelectorWheel(boolean)
     * @see #setMinValue(int)
     * @see #setMaxValue(int)
     */
    public void setValue(int value) {
        setValueInternal(value, false);
    }
```
これが外部から蹴れるやつ。でこれが何を蹴ってるかっていうと以下。

```java:NumberPicker.java
   /**
     * Sets the current value of this NumberPicker.
     *
     * @param current The new value of the NumberPicker.
     * @param notifyChange Whether to notify if the current value changed.
     */
    private void setValueInternal(int current, boolean notifyChange) {
        if (mValue == current) {
            return;
        }
        // Wrap around the values if we go past the start or end
        if (mWrapSelectorWheel) {
            current = getWrappedSelectorIndex(current);
        } else {
            current = Math.max(current, mMinValue);
            current = Math.min(current, mMaxValue);
        }
        int previous = mValue;
        mValue = current;
        // If we're flinging, we'll update the text view at the end when it becomes visible
        if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
            updateInputTextView();
        }
        if (notifyChange) {
            notifyChange(previous, current);
        }
        initializeSelectorWheelIndices();
        invalidate();
    }
```
**君なんでprivateなの？**
外部から値設定してるんだから値変更のコールバックも期待しているに決まってるじゃんというか、変化通知のコールバックに通知するか否かも選ばせてくれていいじゃんみたいな……。
しょうがないので値がセットされたときにコールバックも実行されたっぽくしてあげる。

### cat is cardview, cat is extracter, cat is start
大した変更がないのでまとめます。なんか適当にコミットしちゃったみたいでcat is cardviewはコンパイル通らないです。
なにはともあれ白背景だとAndroidCatが見づらかったのでCardView導入。本来ならConstraintLayoutやCoordinatorLayoutでレイアウトを作るべきなんですが、まだぜんぜん勉強できてないので、ついつい使い慣れたRelativeLayout。レイアウトエディタではLegacyに分類されております。
エラー表示がうっとうしいので文字をstring.xmlに外出し。電球をクリックするか`alt+Enter`叩くと後はAndroidStudioさんがよしなに片付けてくれます。
後はページ初期表示のときに適当に生成した初期値つっこんで、生成したAndroidCatを共有できるようにしておけばgenerate cat部分は終了。

### cat is provider
が、しかし。cat is a pickerでうっかりコミットしていた`shareCat`を実行してみたところ見事にクラッシュ。
なんでクラッシュするんだよ！　**コピペなのに！**

```E/AndroidRuntime: FATAL EXCEPTION: main
    Process: plan.militarize.stray.cat.hyperultracatgenerator, PID: 5979
    android.os.FileUriExposedException: file:///storage/emulated/0/Pictures/Cats/Cat_180.png exposed beyond app through ClipData.Item.getUri()
```

[[Android]android.os.FileUriExposedException](https://torifuku.blogspot.com/2016/07/androidandroidosfileuriexposedexception_4.html)
[Android 7.0 Nougat(ヌガー)でFileUriExposedExceptionが起きる問題](https://backport.net/blog/2017/01/23/nougat_file_uri_exposed_exception/)
[Android N での file:// スキーマによるファイル共有の挙動](https://qiita.com/Shiozawa/items/06ea0dd423c63c20c2f8)

エラーメッセージで検索。`WRITE_EXTERNAL_STORAGE`付与してなかったのもそうですが、AndroidNからはUriの取り扱いが厳しくなったらしい。ファイルを外出しするならもっともっと明示的に、ということか。
ContetProvider系はあまりやったことがなかったので1時間くらいドハマリ。いまいち理解しきれていないですがとりあえず動くところまでは行きました。
コピペ元の`NekoLand.java`でクラッシュしないのは謎ですが、たぶんEasterEggはsystem領域に格納されてる特権アプリだから？

### cat is snack
Toastとかだっせーよな！
ということで最近の流行りと聞くスナックバーを導入。
[Displaying the Snackbar](https://guides.codepath.com/android/Displaying-the-Snackbar)
引数にしたviewの下に出る……と聞いていたが画面下に出る。別にそれでもいいかなって思って放置。AndroidCatが保存されているであろうPicturesへのジャンプボタンをつけてみる。飛んでなにするかとかは**特に考えてない**。
ついでにシークバーのつまみにしていたAndroidCatに専用の表示場所を作ってあげる。
gradleでSupportLibraryのバージョンを定数にする。いつも思うんですがバージョンとか別ファイルに分けるのめんどくさい……めんどくさくない？　gradleがカオスになりやすいのは確かですが。

### cat is permission
ここでRuntimePermission対応。なぜかJava8も導入。Lambdaいいよね！

```java
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    for (int grantResult : grantResults) {
        if (grantResult != PackageManager.PERMISSION_GRANTED) {
            Snackbar.make(mCardSave, R.string.request_permission, Snackbar.LENGTH_SHORT).show();
            return;
        }
    }
    switch (requestCode) {
        case R.id.current_cat:
            shareCat(new Cat(getApplicationContext(), mPicker.getValue()));
            break;
        case R.id.save_cats:
            // TODO save tha cats
            break;
        default:
            return;
    }
}
```
RuntimePermissionのコールバック管理はどの機能がどのPermissionをリクエストしてきたのか覚えてなくちゃいけなくてめんどくさいですね。requestCodeもviewIdもintなのでそのままブチ込んでます。requestCodeを自前で定義すればいいだけの話なんですが、だったらそのままでいいじゃん、みたいな……。
`READ_EXTERNAL_STORAGE`をなぜつけたのか。たぶんいらないと思います。

### cat is foreground
save 1000 cats部分の実装を始めます。
*AndroidCatの名前から推測するに、総数は# 0 〜 #999で1000、つまり1000匹作れればAndroidCatがコンプできる！*
そんなふうに考えていた時期が私にもありました。でも思ったんです。なんで先人のクローンゲームにはAndroidCatコンプリートモードがないんだろうって。

[Neko Easter Egg](https://play.google.com/store/apps/details?id=slaughter.neko)
> This can't really be compared to the easter egg, as this just ruins all of the fun and gives you over 4 billion different cat possibilities...but perhaps you'll find the fun in looking at them one at a time. ;)

**4 billion different cat possibilities...**
よんじゅうおく……？

```java:Cat.java
public static Cat create(Context context) {
    return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt()));
}
```
AndroidCatを決めているのはこの`ThreadLocalRandom.current().nextInt()`です。
[ThreadLocalRandom#nextInt](https://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/ThreadLocalRandom.html#nextInt--)
書いてないですがおそらくすべてのintの値を返しうるのでしょう。intの範囲は-2147483648~2147483647、`Math.abs()`で絶対値にしてるので2147483647匹のAndroidCatがいるはずです。0含めると2147483648かもしれませんがめんどくさいんで計算しません。にじゅうおく……。

呆然としながらIntentServiceのforegroundServiceを実装します。
もう定型文だと思うので適当に。permissionの`FOREGROUND_SERVICE`はたまに忘れてつらい気持ちになります。

### cat is progress
[Android Progress Notification with Examples](https://www.tutlane.com/tutorial/android/android-progress-notification-with-examples)
Notificationのprogressもやったことなかったので作成。

たまにprogressが更新されないな……と思ったら、Notificationの更新頻度には制限があるらしいです。
[Android Nougat and rate limiting of notification updates](https://saket.me/android-7-nougat-rate-limiting-notifications/)
こんなクソアプリならともかく、ここまで頻繁に更新する普通のアプリってあるんでしょうか。進捗をそんな細かく知りたいユーザなんていないでしょうし。

とりあえずAndroidCatの生成ロジックは以下の通りです。

```java:CatServeService.java
Set<Integer> set = new HashSet<>(MAX);
List<Future<?>> list = new ArrayList<Future<?>>(MAX);
executorService = Executors.newWorkStealingPool();

while (set.size() < MAX) {
    int seed = Math.abs(ThreadLocalRandom.current().nextInt());
    int name = seed % 1000;
    if (name < from || to < name) {
        continue;
    }
    if (set.contains(name)) {
        continue;
    }
    set.add(name);
    list.add(executorService.submit(new CatSaver(seed)));
}
executorService.shutdown();
```

我ながら**鉄アレイで殴り続けると死ぬ**並みの**マッチョなロジック**だと自負しています。
乱数生成に偏りがあると永遠にwhileループから抜けられないですが、エミュレータで試す限りではいつもすぐ抜けるので大丈夫でしょう。多分。
`CatSaver`は`shareCat`の中身をコピペしただけのしょうもないRunnableです。ExecutorServiceに渡して待ちます。

```java:CatServeService.java
for (int count = 0; count < list.size(); count++) {
    try {
        list.get(count).get();
        builder.setProgress(MAX, count, false);
        manager.notify(NOTIFICATION_ID_SAVE_CAT_PROGRESS, builder.build());
    } catch (ExecutionException | InterruptedException e) {
    }
}
```

本来ならExecutorServiceに渡された順ではなく各自完了次第progressを更新するとか、失敗したタスクのリトライとか入れるべきなんでしょうがめんどくさいのでやめました。マルチスレッドこわい。

### cat is name
名前出しといた。

### cat is crash
ここまではPのエミュレータで動作確認していました。ここでNの実機に入れてAndroidCatを大量生成したところ**クラッシュ**。Nのエミュレータ作って実行したら**クラッシュ**。特にlogcatが<font color="Red">赤く染まる</font>こともなく……。
やたら重くてあんまり好きじゃないのですが、Profilerを起動したところメモリが大量に増加してクラッシュしています。
**どうせ画像だからメモリリークだろ！**
ということで対策。

+ インナークラスをstaticに
+ `Bitmap.recycle()`
+ 使い終わったBitmap, Drawableにはnullを入れる

それでもクラッシュしました。しょうがないのでマルチスレッドでAndroidCatを生成していたところをシングルスレッドに変更したらクラッシュしなくなりました。

```java:CatServeService.java
// before マルチスレッド
executorService = Executors.newWorkStealingPool();

// after シングルスレッド
executorService = Executors.newSingleThreadExecutor();
```

簡単に切り替えられるExecutorServiceは神。
マルチスレッドでIOするのもやばそうですが、このクラッシュはNのBitmapやDrawableのメモリ解放処理がなにかしら駄目なのかなーという気がします。OやPだとGCが走りまくってクラッシュしないので。

### cat is icon
[Adaptive Iconにとりあえず対応する方法](https://backport.net/blog/2018/02/17/adaptive_icon/)
[Android StudioでAdaptive Iconを作る](https://backport.net/blog/2018/06/16/create_adaptive_icon_in_android_studio/)
せっかくなのでAdaptive Iconも作ってみる。対応しているホームアプリが2018/10/16現在少ないらしく、その威力は確認できませんでした。
「あっこのアプリAdaptive Icon対応してねー！　だっせー！」とか言って煽るくらいしか使いみちがない気がします。フレームワークの新しい仕様にキャッチアップしてないとユーザにクソダサアイコンが見られてしまうのはけっこう残酷な選別。
ちなみにこのクソダサアイコンはgimpで20分もかけて作ったんですが、今思えば横じゃなくて縦にAndroidCatを刻んだほうがかっこよかったかも。

### cat is font
[Kielo Typeface](https://befonts.com/kielo-typeface.html)
フォントも追加。ついでにtoolBarも追加。
普通のフォントは端末標準のフォントにお任せするのがいいと思いますが、こういうアクセントとして装飾フォント使うと楽しいかも。
最初は[これ](https://www.1001freefonts.com/cats-alphabet.font)使おうかと思ったんですが*猫は２種類もいらない*のでオサレな感じに。

### cat is large icon
O以降でNotificationにLargeIconが表示されない。しっかり検証してないですが、Adaptive IconはLarge Iconに設定できないみたいです。Bitmapが生成できてないのかも。Vectorだったら行けたりする……？
めんどくさいので**mipmapフォルダから自動生成されたアイコン画像をコピーしてdrawableフォルダに突っ込む**ことで対応。力こそパワー！

### cat is modifications
いろいろ調整。
progressが更新されるたびに**通知音が鳴り続ける**ことに気づいたので`NotificationManager.IMPORTANCE_DEFAULT`から`NotificationManager.IMPORTANCE_LOW`に変更。
`.setCategory(NotificationCompat.CATEGORY_PROGRESS)`してるじゃん！　鳴らすなや！　と思いますが、まあ、しょうがない……。

foregroudServiceもクラッシュする可能性があったのでfix。`ContextCompat.startForegroundService`で始めたServiceはIntentに不備があってなにもせず終了する場合でも`startForeground()`しとかないとクラッシュします。あまり考えづらいエラーケースなので後から発見して大変なことになったり。

後は`MediaScannerConnection`。生成した画像を即座に他のアプリにも見てほしい場合にこれを行うんですが、1枚ごとに絶え間なく要求しているのがなんとなく気に食わなくて、一括でやってもらうようにしました。これは本当になんとなくでやったので、知見がある方間違ってたら教えてください。

## おわりに
これであなたも自由自在にAndroidCatを大量生成できるようになりました。
この記事を書いている今、この公開する直前、**この地球上で私が最も多くのAndroidCatを生成した人間**だと思います。
みなさんもHyperUltraCatGeneratorでAndroidCatを大量生成してアイコンとかなんかに使ってください！

## おまけ
[めっっっちゃ解析してる方](https://zau2and.blogspot.com/search/label/ねこあつめ)がいました。はっきり言ってこの記事読む時間でこっち読んだほうがAndroidぢからが上がります。
アプリ作ってから見つけました。かなしい。

## おまけのおまけ
AndroidCatの画像サイズは感覚で平均すると14kbです。
14 * 2147483647 = 30064771058

![f2668204-9e05-4e18-b941-82f629d57bd0.png](https://qiita-image-store.s3.amazonaws.com/0/146245/a641f0a4-48d6-9555-6eb1-2d29c6e22aed.png)

すっごーい。