DreamServiceでエオルゼア時計スクリーンセーバーを作る

# はじめに
この話は『 [ThinkSmart View for Zoom](https://psref.lenovo.com/product/thinksmart/thinksmart_view_for_zoom) 』という製品の新古品を[結構安いお店](https://iosys.co.jp/)で買ったところから始まります。
とりあえずROMイメージをごにょごにょして(保障対象外です)APKのインストールができるようになりましたが、何に使いましょうか…。
そうだね、エオルゼア時計だね！(お約束)
せっかくなのでスクリーンセーバー化して、いつでも見れるようにしましょう。

# コード

アドベントカレンダー進行というわけで、ガシガシとコーディングしていきましょう。

## レイアウトXML
<details><summary>簡易的な割に長いので折りたたんでいます</summary>

```xml:daydream.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal|center_vertical"
    android:background="#000000"
    android:gravity="center_vertical"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title_lt"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="Local Time"
        android:textAlignment="center"
        android:textColor="#FFFFFF"
        android:textSize="50dp" />

    <TextView
        android:id="@+id/local_clock"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:text="00:00:00"
        android:textAlignment="center"
        android:textColor="#FFFFFF"
        android:textSize="100sp" />

    <Space
        android:layout_width="match_parent"
        android:layout_height="50dp" />

    <TextView
        android:id="@+id/title_et"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="Eorzea Time"
        android:textAlignment="center"
        android:textColor="#FFFFFF"
        android:textSize="50dp" />

    <TextView
        android:id="@+id/eorzea_clock"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:text="00:00:00"
        android:textAlignment="center"
        android:textColor="#FFFFFF"
        android:textSize="100sp" />

</LinearLayout>
```
</details>

## DreamService用 Javaコード

```java:Daydream.java
package jp.rainyvillage.android.daydreamtest2;
import android.service.dreams.DreamService;
import android.util.Log;

import java.util.Timer;

public class Daydream extends DreamService {
    private Timer dispUpdateTimer;
    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        setInteractive(true);
        setFullscreen(true);
        setContentView(R.layout.daydream);

    }

    @Override
    public void onDreamingStarted() {
        super.onDreamingStarted();
        this.dispUpdateTimer = new Timer();
        this.dispUpdateTimer.schedule(new DispUpdateTimerTask(this), 0, 500);
    }

    @Override
    public void onDreamingStopped() {
        super.onDreamingStopped();
        if (this.dispUpdateTimer != null) {
            this.dispUpdateTimer.cancel();
        }
    }
}

```

## 表示更新用TimerTask

```java:DispUpdateTimerTask.java
package jp.rainyvillage.android.daydreamtest2;

import android.util.Log;
import android.view.View;
import android.widget.TextView;

import java.util.Calendar;
import java.util.TimeZone;
import java.util.TimerTask;

public class DispUpdateTimerTask extends TimerTask {
    private static final long SEC_OF_MIN = 60;
    private static final long SEC_OF_HOUR = SEC_OF_MIN * 60;
    private static final long SEC_OF_DAY = SEC_OF_HOUR * 24;
    Daydream daydream;
    Calendar calendarDefault;
    Calendar calendarTokyo;
    public DispUpdateTimerTask (Daydream daydream) {
        super();
        this.daydream = daydream;
        this.calendarDefault = Calendar.getInstance();
        this.calendarDefault.setTimeZone(TimeZone.getDefault());
        this.calendarTokyo = Calendar.getInstance();
        this.calendarTokyo.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
    }
    public void run () {
        TextView localClock = (TextView)daydream.findViewById(R.id.local_clock);
        TextView eorzeaClock = (TextView)daydream.findViewById(R.id.eorzea_clock);

        long now = System.currentTimeMillis();
        calendarDefault.setTimeInMillis(now);
        int localSec = calendarDefault.get(Calendar.SECOND);
        int localMin = calendarDefault.get(Calendar.MINUTE);
        int localHour = calendarDefault.get(Calendar.HOUR) + (12 * calendarDefault.get(Calendar.AM_PM));
        String strLocalClock = String.format("%02d:%02d:%02d", localHour, localMin, localSec);

        calendarTokyo.setTimeInMillis(now);
        long dayOfWeek = calendarTokyo.get(Calendar.DAY_OF_WEEK) - 1;
        long hour = calendarTokyo.get(Calendar.HOUR) + (12 * calendarTokyo.get(Calendar.AM_PM));
        long min = calendarTokyo.get(Calendar.MINUTE);
        long sec = calendarTokyo.get(Calendar.SECOND);
        long weekSec = (SEC_OF_DAY * dayOfWeek) + (SEC_OF_HOUR * hour) + (SEC_OF_MIN * min) + sec;
        long eorzeaWeekSec = (long)(weekSec * (1440f / 70f));
        int eorzeaMin = (int)((eorzeaWeekSec / SEC_OF_MIN) % 60);
        int eorzeaHour = (int)((eorzeaWeekSec / SEC_OF_HOUR) % 24);
        String strEorzeaHourMin = String.format("%02d:%02d:--", eorzeaHour, eorzeaMin);

        localClock.setText(strLocalClock);
        eorzeaClock.setText(strEorzeaHourMin);
    }
}
```

## AndroidManifest

```xml:AndroidManifest.xml
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.rainyvillage.android.daydreamtest2">
    <uses-sdk android:targetSdkVersion="17" android:minSdkVersion="17"/>
    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:allowBackup="false">

        <service
            android:name=".Daydream"
            android:exported="true"
            android:label="@string/app_name"
            android:icon="@mipmap/ic_launcher"
            android:permission="android.permission.BIND_DREAM_SERVICE">
            <intent-filter>
                <action android:name="android.service.dreams.DreamService" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>
    </application>
</manifest>
```

# 解決が難航したところ

アプリを実機に転送すると、スクリーンセーバーの一覧に表示はされるのですが、選択することができませんでした。
[StackOverflowのこの記事](https://stackoverflow.com/questions/36443752/unable-to-start-daydream-service-in-my-custom-android-app)を参照したところ、API level 21以上の場合はDreamServiceのパーミッションを与える記述をマニフェストに入れなければならないそうです。
※上記のマニフェストファイルには記述済みです。

# 動作の様子
|スクリーンショット|実機での写真|
|:-:|:-:|
|![Screenshot_20231209-213354.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/119f4a86-0deb-ac64-019a-418515cac61b.png)|![DSC00692.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/4ba31ba5-a83c-6c3d-ffe2-c78d60af3068.jpeg)|

# さいごに

ほかにもいろいろなエオルゼア時計を作っているので、ぜひご覧ください☆

* [M5StickC Plus](https://qiita.com/CIB-MC/items/7d60ed998fdf0ce01e68)
* [Sensor Watch](https://qiita.com/CIB-MC/items/78691e73d7df04b2e5b9)
* [専用アナログ時計キット](https://qiita.com/CIB-MC/items/2cc3d29076288d76e0ba)