体内時計を狂わせることで冬休みを1日増やす

今年ももうあと僅か。冬休みが待ち遠しいですね。
私の会社の今年の冬休みは、12/28（木）〜1/8（月）の12日間です。
贅沢な悩みなのは承知の上ですが、正直もっと休みたいなーと思いました。
そこで考えました。`24時間 * 12日 = 288時間` のところ、体内時計を狂わせて1日を22時間として生活すれば、`288時間 / 22時間 = 約13日` となり、体感的には1日多く休めることになります。
よくわからないと思うので詳しく解説します。

## 前提条件
+ 一日の睡眠時間は8時間とする
+ 冬休み中は一歩も家の外に出ない
+ 現実とは異なる仮想時間を生きる

## 1日目
**0時**に就寝、8時に起床
![1日目.png](https://qiita-image-store.s3.amazonaws.com/0/152304/cd80035f-c032-482e-4947-1359f696b671.png)


## 2日目
**22時**に就寝、翌6時に起床
![2日目.png](https://qiita-image-store.s3.amazonaws.com/0/152304/df5687e9-1a41-a4c6-4eab-a0d812b95cdd.png)


## 3日目
**20時**に就寝、翌4時に起床
![3日目.png](https://qiita-image-store.s3.amazonaws.com/0/152304/8619a520-ccb5-1d90-234b-e02e7c47845e.png)


このように、就寝時刻を2時間ずつずらしていくことで、体感的に1日多くの休みを得ることができます。
どうせ24時間のうち2時間ぐらい減ったところで気付きゃしません。

今回はこの生活を支援するためのスマホアプリを作ってみました。

## 事前準備
1. 冬を越せるだけの食料を備蓄します。
2. カーテン（遮光1級を推奨）を締め切って、太陽光が部屋に差し込まないようにします。
3. 部屋中の時計を撤去します。
4. スマホの時刻表示の箇所にはマスキングテープなどを貼って、物理的に見えないようにします。
<img src="https://qiita-image-store.s3.amazonaws.com/0/152304/d2d457f3-97e1-3a73-1718-09049428421c.jpeg">


とにかく本当の時間を意識してしまったら負けなので気をつけましょう。

## アプリの概要
初日の寝る時間をセットしたら、以降は自動で計算した時刻にアラームが鳴ります。
冬休み中は誰とも会わずチャットなどでコミュニケーションも取らないので（LINEなどの送信時刻が表示されるチャットアプリは使えません）、少しでも人の温もりが感じられるようにアラーム音は音楽ではなく音声読み上げにしました。

### 初期画面
<img src="https://qiita-image-store.s3.amazonaws.com/0/152304/352a3951-9155-e733-9018-66132ede6ce8.png" width="300">

### 初日の就寝時刻設定画面
<img src="https://qiita-image-store.s3.amazonaws.com/0/152304/11273ba7-ec5c-e300-dc86-49432146a8f3.png" width="300">

### アラーム設定完了画面
<img src="https://qiita-image-store.s3.amazonaws.com/0/152304/c4b70b2b-3d75-b4e9-7fb4-e8bb2b5f3ff5.png" width="300">

## 使用技術
Monacaを使って開発しています。
また、以下のCordovaプラグインを使用しました。

+ [uniclau/AlarmPlugin](https://github.com/uniclau/AlarmPlugin)
  + スケジューリングされた時刻にアプリを起動するプラグイン（Androidのみ）
+ [vilic/cordova-plugin-tts](https://github.com/vilic/cordova-plugin-tts)
  + 音声読み上げを行うプラグイン（Androidのみ？）
+ [Datepicker プラグイン](http://docs.monaca.io/ja/reference/third_party_phonegap/datepicker/)
  + 日付/時刻選択UIを提供するMonaca組み込みプラグイン

`AlarmPlugin`は、Androidのみサポートしています。おそらく指定時刻にアプリを強制的に起動するという挙動が、Appleサイドで許可されていないか推奨されていないのだと思われます。
`cordova-plugin-tts`は、なぜかiOSでは動きませんでした。

よって、今回のアプリはAndroid端末のみを対象としています。

## ソースコード
```html:index.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
  <script src="components/loader.js"></script>
  <script src="lib/onsenui/js/onsenui.min.js"></script>

  <link rel="stylesheet" href="components/loader.css">
  <link rel="stylesheet" href="lib/onsenui/css/onsenui.css">
  <link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css">
  <link rel="stylesheet" href="css/style.css">

  <script>
    const sleepTime = 8;  // 8時間睡眠します
    const oneDayTime = 22;  // 1日を22時間とします
    
    // 初日の就寝時刻をセットする
    function setBedtime() {
      // datePickerで時刻を表示
      window.plugins.datePicker.show({
        mode : 'time',
        date : new Date()
      }, (returnDate) => {
        // 選択された時刻が取得される
        let alarmTime = new Date(returnDate);
        // 現在時刻を取得
        let now = new Date();
        // 過去時刻を指定された場合は、1日加算して翌日に設定
        if(now.getTime() > alarmTime.getTime()) {
          alarmTime.setDate(alarmTime.getDate() + 1);
        }        
        // アラームをセット
        setAlarm(alarmTime);
        // ローカルストレージに就寝時刻として登録
        localStorage.setItem('bedTime', alarmTime.toString());
      }, onError);
    }
    
    // アラームを設定する
    function setAlarm(alarmTime) {
      navigator.plugins.alarm.set(alarmTime, 
      () => {
        alert('次のアラームを設定しました');
      }, 
      onError);
    }

    // アプリ起動時の処理
    document.addEventListener('deviceready', onLaunchApp, onError);
    // アプリがバックグラウンドから復帰したときの処理
    document.addEventListener('resume', onLaunchApp, onError);
    
    // アプリが立ち上がったら、アラーム機能によって立ち上がったかどうかをチェックする
    function onLaunchApp() {
      // 現在時刻を取得
      let now = new Date();
      // ローカルストレージから就寝時刻と起床時刻を取得
      let bedTime = localStorage.getItem('bedTime');
      let wakeupTime = localStorage.getItem('wakeupTime');
      
      if(bedTime !== null) {
        // Date型に変換
        bedTime = new Date(bedTime);
        // 就寝時刻だったら
        let diff = now.getTime() - bedTime.getTime();
        if(0 <= diff && diff <= 60000) {
          document.querySelector('#message').textContent = '寝る時間です';
          // テキスト読み上げ
          textToSpeech('寝る時間ですよ');
          // 起床時刻を求めてアラームを再設定
          let nextAlarmDate = new Date(
            bedTime.getFullYear(),
            bedTime.getMonth(),
            bedTime.getDate(),
            bedTime.getHours() + sleepTime, // 睡眠時間を加算
            bedTime.getMinutes(),
            bedTime.getSeconds()
          );
          setAlarm(nextAlarmDate);
          // ローカルストレージの起床時刻を更新
          localStorage.setItem('wakeupTime', nextAlarmDate.toString());
        }
      }
      
      if(wakeupTime !== null) {
        // Date型に変換
        wakeupTime = new Date(wakeupTime);
        // 起床時刻だったら
        let diff = now.getTime() - wakeupTime.getTime();
        if(0 <= diff && diff <= 60000) {
          document.querySelector('#message').textContent = '起きる時間です';
          // テキスト読み上げ
          textToSpeech('起きてください！起きてください！起きてください！');
          // 今夜の就寝時刻を求めてアラームを再設定
          let nextAlarmDate = new Date(
            wakeupTime.getFullYear(),
            wakeupTime.getMonth(),
            wakeupTime.getDate(),
            wakeupTime.getHours() - sleepTime + oneDayTime,  // 1日あたりの時間を加算
            wakeupTime.getMinutes(),
            wakeupTime.getSeconds()
          );
          setAlarm(nextAlarmDate);
          // ローカルストレージの就寝時刻を更新
          localStorage.setItem('bedTime', nextAlarmDate.toString());
        }
      } 
    }
    
    // テキスト読み上げを実行
    function textToSpeech(message) {
      TTS.speak({
        text: message,
        locale: 'ja-JP'
      }, function () {
        console.log('success');
      }, function (reason) {
        console.log(reason);
      });
    }
    
    // エラー時の処理
    function onError(error) {
      console.log(error);
    }
  </script>
</head>
<body>
  <ons-page>
    <ons-toolbar>
      <div class="center">いっぱい休むあぷり</div>
    </ons-toolbar>
    <div class="content">
      <p>初日は何時に寝ますか？</p>
      <ons-button modifier="large" onclick="setBedtime()">設定</ons-button>
      <p id="message"></p>
    </div>
  </ons-page>
</body>
</html>
```

### 設定ボタン押下時の処理
設定ボタンが押されると、Datepickerプラグインによって初日の就寝時刻を設定する画面が表示されます。

```text:Datepickerプラグインの利用方法
window.plugins.datePicker.show({
  mode : 'time',  // 時刻を表示
  date : 初期表示する時刻
})
```
このメソッドの戻り値はPromiseです。ユーザーによって選択された日時が成功時コールバック関数に渡されます。
ここでは時刻のみを選択させるようにしているので、現在時刻よりも前の時刻、たとえば現在20時で就寝時刻を1時に設定された場合は、翌日を指しているとみなして日またぎの処理（日付に1日加算）をしています。
就寝時刻が設定できたら、アラーム設定を行い、ローカルストレージに保存しておきます。

### アラームの設定
アラーム設定は`AlarmPlugin`プラグインを使って行います。

```text:AlarmPluginプラグインの利用方法
navigator.plugins.alarm.set(アラーム時刻, アラーム設定完了時処理, エラー処理)
```

`AlarmPlugin`プラグインの使い方は非常にシンプルです。第一引数に指定された時刻になったらアプリが強制的に起動します。アプリがバックグラウンドにいる場合はフォアグラウンドに戻し、アプリが落ちている場合は新たに立ち上げます。
なお、同時に複数のアラームを設定することはできないようです。最後にセットしたアラームで前のアラームが上書きされます。ですのでこのアプリではアラームが発生したタイミングで、次回のアラームを設定するようにしています。

### アラーム発生時の処理
アプリが新しく起動した場合は`deviceready`、アプリがフォアグラウンドに復帰した場合には`resume`イベントが発生します。
いずれかのイベントが発生したタイミングで、以下の処理を行っています。

```javascript:アプリ起動時の処理
// 現在時刻を取得
let now = new Date();
// ローカルストレージから就寝時刻と起床時刻を取得
let bedTime = localStorage.getItem('bedTime');
let wakeupTime = localStorage.getItem('wakeupTime');

if(bedTime !== null) {
  // Date型に変換
  bedTime = new Date(bedTime);
  // 就寝時刻だったら
  let diff = now.getTime() - bedTime.getTime();
  if(0 <= diff && diff <= 60000) {
    document.querySelector('#message').textContent = '寝る時間です';
    // テキスト読み上げ
    textToSpeech('寝る時間ですよ');
    // 起床時刻を求めてアラームを再設定
    let nextAlarmDate = new Date(
      bedTime.getFullYear(),
      bedTime.getMonth(),
      bedTime.getDate(),
      bedTime.getHours() + sleepTime, // 睡眠時間を加算
      bedTime.getMinutes(),
      bedTime.getSeconds()
    );
    setAlarm(nextAlarmDate);
    // ローカルストレージの起床時刻を更新
    localStorage.setItem('wakeupTime', nextAlarmDate.toString());
  }
}

if(wakeupTime !== null) {
  // Date型に変換
  wakeupTime = new Date(wakeupTime);
  // 起床時刻だったら
  let diff = now.getTime() - wakeupTime.getTime();
  if(0 <= diff && diff <= 60000) {
    document.querySelector('#message').textContent = '起きる時間です';
    // テキスト読み上げ
    textToSpeech('起きてください！起きてください！起きてください！');
    // 今夜の就寝時刻を求めてアラームを再設定
    let nextAlarmDate = new Date(
      wakeupTime.getFullYear(),
      wakeupTime.getMonth(),
      wakeupTime.getDate(),
      wakeupTime.getHours() - sleepTime + oneDayTime,  // 1日あたりの時間を加算
      wakeupTime.getMinutes(),
      wakeupTime.getSeconds()
    );
    setAlarm(nextAlarmDate);
    // ローカルストレージの就寝時刻を更新
    localStorage.setItem('bedTime', nextAlarmDate.toString());
  }
} 
```

まず、ローカルストレージに保存されているアラーム時刻（就寝時刻および起床時刻）を取得します。ローカルストレージへの保存処理は前回アラーム設定時に行っています。

そして`AlarmPlugin`プラグインの機能によってアプリが起動したのかどうかを調べるために、現在時刻とアラーム時刻との比較を行います。比較を行っているif文の条件式を見ると、`(現在時刻 - アラーム時刻)` が、`0 以上 60000 以下` であるかどうかを調べています。これはアラームが発生してから`deviceready` または `resume` イベントが発生するまでの間には少しタイムラグがあるため、アラームが発生してから現在までの時刻が60000ミリ秒（1分）以内であれば誤差として許容する、という意味です。

現在時刻が就寝時刻だった場合は、8時間後を起床時刻として、次回アラーム設定とローカルストレージへの保存処理を行います。
現在時刻が起床時刻だった場合は、前日就寝時刻の22時間後を今日の就寝時刻として、次回アラーム設定とローカルストレージへの保存処理を行います。
いずれの場合も、就寝時刻または起床時刻であることをユーザーに伝えるために音声読み上げ処理を行っています。

### 音声読み上げ処理
音声読み上げは、`cordova-plugin-tts`プラグインを使って行います。

```text:cordova-plugin-ttsプラグインの利用方法
TTS.speak({
  text: 読み上げる文字列,
  locale: 'ja-JP'  // 日本語
})
```

メソッドの引数に読み上げる文字列を渡すだけで、読み上げが実行されます。戻り値はPromiseとなっています。

ソースコード全体を見るとごちゃごちゃと書いてありますが、一つ一つの要素を切り出して見てみると、簡単な処理の組み合わせで実装されていることがわかるかと思います。

なお、冬休みが終わることを考えたくなかったので、アラームを停止する処理は入れませんでした。一度アラームを設定すると永遠に繰り返され続けます。
もし停止処理を入れる場合は、停止ボタンが押されたらローカルストレージをクリアするようにすれば良いでしょう。

## 注意事項
体内時計を狂わせる生活、また太陽光を浴びない生活には、重篤な健康被害を引き起こす危険性があります。
今回紹介したアプリによって病気などの被害が発生しましても、一切の責任を負いかねますのでご了承ください。
私は特別な訓練を受けているので大丈夫ですが、皆さんは安易に真似をしないようにしてください。