2020/05/02

Java - 赤ちゃんに動画を見せているときに PC/Mac を操作されるのを防止する

昔 (8年ほど前)、娘に PC で DVD を見せているときに、キーボードをいじってしまっていろんなことが起きるのを防ぐために作成した Babyproof という Java アプリケーションです。
当時は Java 6 の時代で、Windows での使用しか想定していませんでしたが、今回は Java 14 ベースの Mac 版にリライトしてみました。Windows でも動くんじゃないかと思いますが、未確認です。

ダウンロードとインストール

Mac 用には jpackage で JRE ごとパッケージ化してみました。開くとマウントされますので、Applications にドラッグ&ドロップです。
ダウンロード (for Mac)

その他のプラットフォームでは JRE 14 をインストールの上、下記 jar を使ってください。
ダウンロード (Java 14 用実行可能 jar)

使用方法

babyproof.jar をダブルクリックすると起動します。起動すると、キーボードとマウスがほぼ操作不可状態になります。

Windows の Alt+TAB や Mac の Command+TAB は効きますので、アプリケーションをBabyproof から他に切り替えれば、操作可能になります。もう一度 Babyproof を選択すれば、再度操作不可状態になります。

操作不能となっている状態でキーボードから「babyproof」と入力すれば、Babyproof は終了します。

制限事項

アプリケーションまで届かないイベントは防げないので、Windows キーなどには反応してしまいます。Mac だと、画面上部のメニューバーがガードされておらず、この領域にはマウスが反応します。

実現方法

Swing で透明なフレームを全画面表示にすることで、画面表示を妨げることなく、キーボード、マウスのイベントを全部 Babyproof に拾わせるという作戦になっています。

ソースコード

import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;

public class Babyproof extends JFrame {
  private static final long serialVersionUID = 1L;

  public static void main(final String args[]) {
    new Babyproof().setVisible(true);
  }

  private static final String PASSWORD = "babyproof";
  private static final char PASSWORD_HEAD = PASSWORD.charAt(0);
  private final StringBuilder input = new StringBuilder(PASSWORD.length());

  public Babyproof() {
    super("Babyproof");
    setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);

    setExtendedState(JFrame.MAXIMIZED_BOTH);
    setUndecorated(true);
    setBackground(new Color(0x00000000, true));

    final MessageLabel label = new MessageLabel();
    add(label);
    final Timer timer = new Timer(20, label);
    timer.start();

    addKeyListener(new KeyListener() {
      public void keyPressed(final KeyEvent e) {
      }
      public void keyReleased(final KeyEvent e) {
      }
      public void keyTyped(final KeyEvent e) {
        final char c = e.getKeyChar();
        input.append(c);
        if (input.length() >= PASSWORD.length() && input.toString().startsWith(PASSWORD)) {
          System.exit(0);
        }
        if (!PASSWORD.startsWith(input.toString())) {
          input.setLength(0);
          input.append(c);
          if (c != PASSWORD_HEAD) {
            label.restart();
          }
        }
      }
    });
  }

  private static final class MessageLabel extends JLabel implements ActionListener {
    private static final long serialVersionUID = 1L;

    private MessageLabel() {
      super("Type `" + PASSWORD + "' to exit.");
      setFont(getFont().deriveFont(Font.BOLD, 32.0f));
      setForeground(new Color(0x00000000, true));
      setBackground(new Color(0x00000000, true));
      setHorizontalAlignment(JLabel.LEFT);
      setVerticalAlignment(JLabel.TOP);
    }

    private int mode = 0;
    private int alpha = 0;

    public void restart() {
      if (mode == 0) {
        this.mode = 1;
      }
    }

    public void actionPerformed(final ActionEvent e) {
      switch (mode) {
      case 1:
        alpha += 4;
        if (alpha >= 0x100) {
          mode = -1;
        } else {
          setForeground(new Color(0xff, 0xff, 0xff, alpha));
        }
        break;
      case -1:
        alpha -= 4;
        if (alpha > 0) {
          setForeground(new Color(0xff, 0xff, 0xff, alpha));
        } else {
          setForeground(new Color(0xff, 0xff, 0xff, 0));
          mode = 0;
        }
        break;
      default:
      }
    }
  }
}

見所1 babyproof と入力すると終了するロジック

使用方法で述べた通り、「babyproof」と入力すると終了するようになっていますが、それが42〜53行目のロジックです。Babyproof が有効なとき、キー入力のたびにこの keyTyped() が呼ばれます。ここで、今まで入力された文字列が「babyproof」に合致するかどうかを調べ、合致したら終了させています。

  • 入力された文字を一旦 StringBuilder に積む
  • 積まれた文字列が「babyproof」の長さ以上になったら、前方から「babyproof」に合致するかどうか調べ、合致したら終了
  • 入力された文字が「babyproof」に前方一致しなかったら、積んである文字列を一旦破棄し、今入力された1文字だけ積み直す
最後に1文字積み直しているのは、例えば「babyproobabyproof」と入力したときにちゃんと終了するためです。「babyproob」のところで合致しないと判定されて破棄しますが、この「b」を積み直さないと残りの「abyproof」だけでは合致しないからです。

見所2 パスワードが合致しないときに「Type `babyproof' to exit.」と浮かびあがらせるロジック

実はコードの半分はこの処理です……。
「babyproof」以外のキー入力を行うと、画面左上にメッセージが浮かびあがるようになっています。赤ちゃんはきっと読めないだろうとタカをくくってパスワードを画面に表示しています。
まず、20ms ごとに駆動するタイマーをセットしています (33〜34行目)。
メッセージを表示させる要求をタイマーに伝えると (51行目→73〜77行目)、メッセージ文字の透過度を徐々に下げて (不透明にして) から、また上げる (透明にする) という処理が動くようになっています (80〜99行目)。一巡したらまた要求があるまで待ちます。透明度を変えている途中で新しい要求があった場合は無視します。

ちょっとしたツールですが、うちの子が小さいときはけっこう重宝しました。
でも最近は iPad でアクセスガイド使いますから、出番はないです。