2019年3月9日土曜日

Android on Raspberry Piを安全にシャットダウン

左手の親指を怪我して2針縫ったのでしばらく不自由していました。

現在、Raspberry Pi 3 B+でAndroid7.1.1を動かしてScratch Jrを動かしていますが、Raspberry Piには電源ボタンが無いので、Androidをシャットダウンするには他のPCからadbコマンドでシャットダウンしてから電源を切るか、直接電源を切る必要があります。

できればシャットダウンしてから電源を切りたいので、シャットダウンアプリを導入しようとしましたが、rootを取っておく必要があるアプリばかりのようです。

そこで「android adb shutdown」でネットを検索すると、いくつかのサイトがヒットしますが、概ねrootを取ってあることが前提のようです。

幸い、現在使用しているAndroid7.1.1のadb shellコマンドはroot権限で動作するようなので(adb shellのプロンプトが#になります)、以下のコマンドでシャットダウンが可能でした。
adb shell am start -a com.android.internal.intent.action.REQUEST_SHUTDOWN
adb shell am start -n android/com.android.internal.app.ShutdownActivity
さらに、Android4.4以降は
adb shell setprop sys.powerctl shutdown
でもシャットダウン可能です。

その1:ターミナルアプリのインストール


とは言え、シャットダウンする為だけにPCを立ち上げ、コマンドを叩くのも面倒です。

なんとかRaspberry Piだけでシャットダウンできないかと考え、adb shellを実行しているならターミナルアプリをインストールして、上記コマンドを実行すればいいんでは無いかと考えテストしてみると。

amを利用するものはAndroidの権限で弾かれてしまいましたが、setpropを利用するケースは問題なくシャットダウンしてくれました。

これで、わざわざPCを立ち上げなくてもRaspberry Piだけでシャットダウンしてから電源を切れるようになりました。

その2:ShutdownPiの作成


とは言っても、毎回ターミナルアプリを立ち上げてコマンドを入力するのは面倒です。ワンクリックでシャットダウンしたいものです。

ということで、Kotlinの勉強も含めてシャットダウンアプリを作成することにしました。

以下はメインとなるダイアログ部分のソースです。ダイアログを表示して、シャットダウン/リーブート/キャンセルを選択できるようにしてあります。

package com.example.keiichimaru.shutdownpi.dialog

import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.util.Log
import com.example.keiichimaru.shutdownpi.R

class DlgShutdownConfirm(context: Context) {
    private val mContext: Activity? = if (context is Activity) context else null

    companion object {
        const val TYPE_SHUTDOWN = "shutdown"
        const val TYPE_REBOOT = "reboot"
    }

    fun show( manager: FragmentManager, tag: String?) {
        val dialog = DlgShutdownConfirmDialog()
        dialog.onRebootButtonClickListener = DialogInterface.OnClickListener { _, _ ->
            finishActivity()
            doShutdown(TYPE_REBOOT)
        }
        dialog.onShutdownButtonClickListener = DialogInterface.OnClickListener { _, _ ->
            finishActivity()
            doShutdown(TYPE_SHUTDOWN)
        }
        dialog.onCancelButtonClickListener = DialogInterface.OnClickListener { _, _ ->
            finishActivity()
        }
        dialog.show(manager, tag)
    }
    private fun finishActivity() {
        requireNotNull(mContext) {"DlgShutdownConfirm initialize error."}
        mContext.finishAndRemoveTask()
    }
    private fun doShutdown(type: String) {
        require((type == TYPE_SHUTDOWN) or (type == TYPE_REBOOT)) { "type required $TYPE_SHUTDOWN or $TYPE_REBOOT." }
        val command = arrayOf(
            "sh",
            "-c", "setprop sys.powerctl $type"
        )
        val runtime = Runtime.getRuntime()
        try {
            Log.d(DlgShutdownConfirm::class.qualifiedName, command.reduce { s1, s2 -> "$s1 $s2" })
            val process = runtime.exec(command)
            val result = process.inputStream.bufferedReader().use { it.readText() }
            Log.d(DlgShutdownConfirm::class.qualifiedName, result)
            process.waitFor()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
    class DlgShutdownConfirmDialog : DialogFragment() {
        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
            val builder = AlertDialog.Builder(activity!!)
            builder
                .setMessage(R.string.dlg_shutdown_msg)
                .setNeutralButton(R.string.dlg_reboot, onRebootButtonClickListener)
                .setNegativeButton(R.string.dlg_shutdown, onShutdownButtonClickListener)
                .setPositiveButton(R.string.dlg_cancel, onCancelButtonClickListener)
            return builder.create()
        }
        internal var onRebootButtonClickListener: DialogInterface.OnClickListener? = null
        internal var onShutdownButtonClickListener: DialogInterface.OnClickListener? = null
        internal var onCancelButtonClickListener: DialogInterface.OnClickListener? = null
    }
}

MainActivityは以下の通りです。
アプリが起動されると、画面は表示せずに確認ダイアログを表示してシャットダウンします。

package com.example.keiichimaru.shutdownpi.dialog

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.example.keiichimaru.shutdownpi.dialog.DlgShutdownConfirm

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        DlgShutdownConfirm(this).show(supportFragmentManager, MainActivity::class.qualifiedName)
    }

}

Edisonが故障した一件

 最近またEdisonで遊んでいます。 教育用のデバイスとしてよく出来ているなと思っている Edison ですが、現在3台を所有しています。 最近このうちの1台が調子が悪くなってしましました。具体的にはBeep用のデバイスが壊れたようです。購入時にチェックをしたつもりですが今回久...