たくさんの自由帳

要 Shizuku アプリを切り替えたら自動でタスクを終了するアプリ

投稿日 : | 0 日前

文字数(だいたい) : 4194

どうもこんばんわ。最近買ったもの紹介ドラゴン。いつまで使えてるか記録します。

水筒。象印の720mlのワンタッチじゃない方です。水道水飲んでます。名古屋のお水が美味しいらしいので飲んでみたいです(買ったものと関係ない)
なんか知らんけどよく頭が痛くなるので、水不足による頭痛を回避するために持ち歩いてる。まあ寝すぎの頭痛とかあるので原因の1つを潰しただけなんですけどね、
ひねって開けるタイプの方を買ったので、容量の割に小さめなのかなとちょっと思った。

あとはリュックサックを無印のでかいやつにしました。いっぱい入るのでいい!!
今まで使ってたやつがもう風前の灯。汚れてきちゃった。

おわり。

本題

Xperia 1 VIIのカメラ、なんとアプリを切り替えても状態が保持されてる。
Android アプリ開発したことがあれば、状態を引き継ぐonSaveInstanceState()を律儀に実装していて偉いと口を揃えるかと思います。
全人類の敵であるアクティビティを保持しないを有効にしてもちゃんと動くというのは素晴らしいです。

静止画撮影モード・動画撮影モード・ズーム倍率・自撮りカメラ・・・をアプリを終了せずに切り替えた場合に(タスク一覧画面に残した状態)アプリの状態を復元する。

が、、、どうもこの挙動が慣れなくて・・・Pixelの場合だと動画撮影で倍率変えてても、アプリを一旦離れた状態でリセットされてるので、履歴画面から開いてもまた静止画モード、等倍ズームに戻ってくれてます。
こっちがいい!!

んだけど、設定を見てもそれらしき項目は見つからなかった。。私は!こっちの!挙動が良い!!

Android アプリ開発者向けに話すと、この処理を任意のアプリで使いたい。
アプリを離れたらonStop()にライフサイクルが進むので、そこでタスクから削除するfinishAndRemoveTask()を呼び出したい。

class MainActivity : ComponentActivity() {
    override fun onStop() {
        super.onStop()
        finishAndRemoveTask()
    }
}

つくった

アプリを離れたら自動でタスクを終了するアプリ!!!
以下の録画はカメラアプリではないですが、好きなアプリを離れたと同時にタスク削除することが出来ます。

使い道としては、今回のように離れたときにアプリを勝手に閉じておいてほしい時に使えます。
今回のカメラアプリの件や、放ったらかしにすると使えなくなるアプリとか?に

ブラウザアプリは絶対やめたほうが良いです。シークレットタブが消えるので。

いい感じに動いてると思います。これでカメラの倍率を毎回戻す手間がなくなりました。。。

だうんろーど

APKあります。

ソースコードもあります。後述しますがShizuku+隠しAPIのコンボなのでandroid.jarを差し替える必要があります。

仕組み

  • 今表示されているActivityがタスク削除対象になるまで待つ
  • 削除対象が他の画面になるまで待つ
  • 消す
  • 最初に戻る

隠しAPIを使っています

AndroidのソースコードであるAOSPを斜め読みして、今表示されているActivityが切り替わったときに呼ばれるコールバックと、タスク一覧画面からタスク(アプリ)を終了する関数を見つけました。
多分IActivityManagerにあるregisterTaskStackListener()removeTask()ですね。

ただ、もちろんのことこれらのAPIAndroid内部で使われる前提なので、そもそもサードパーティーのアプリが利用できない。
いくつか問題があり、権限の件、隠しAPIの件、リフレクション対策の件。

権限の話ですが、Shizukuを使います。これは内部で使われているAPIShizukuが代わりに叩いてくれます。詳しい話はまだ今度(前もまた今度って言った気がする・・・)
ActivityManagerとかTelephonyManager等のなんとかManagerにある関数呼び出しをShizuku経由にすることができます。

まだ問題があります。隠しAPIの話です。Android StudioSDK Managerからダウンロードできるandroid.jar隠しAPIが削除された状態になってます。
まあ実行時には隠しAPIも存在するのでリフレクションする技もありますが・・・Android Studio隠しAPIが入ったandroid.jarに差し替えるのが良いかと。

最後にリフレクション対策の話。これはAndroidHiddenApiBypassライブラリを入れることで回避できます。
というのも、いくら隠しAPIとは言えAOSPには確かに存在するのに、リフレクションで関数を呼び出そうとしても存在しない例外を投げる仕組みが何故か存在します。
Shizukuは関数呼び出しに成功した後の、権限を回避するために使うものであり、そもそも関数が見つからない場合はこの回避策が必要になる。

表示されてる Activity 取得

Shizukuで呼び出して、Flowでコールバックをいい感じにしてこんな感じ。ActivityManagerにある関数呼び出しをShizuku経由にするためのIActivityManager...ですね。
あとはcollect { }するたびにcallbackFlow { }の中身が起動する(コールバックがその都度登録される)のをやめるためにstateIn()HotFlow (StateFlow)に変換しています。

val taskStackHotFlow = callbackFlow {
    val listener = object : ITaskStackListener.Stub() {
        override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo?) {
            trySend(taskInfo)
        }
        // TODO 他にもコールバックがいっぱい存在します
    }

    val activityManager = IActivityManager.Stub.asInterface(
        ShizukuBinderWrapper(ServiceManager.getService("activity"))
    )

    activityManager.registerTaskStackListener(listener)
    awaitClose { activityManager.unregisterTaskStackListener(listener) }
}.stateIn(
    scope = scope,
    started = SharingStarted.Eagerly,
    initialValue = null
)

削除している部分

first { }で削除対象が来るまで一時停止します。その後、他のアプリに切り替わるのも待って確認します。
これでもう他のアプリに切り替わったので削除の準備が整った。removeTask()を呼び出します。

純粋なwhileループで作れたのがお気に入り。suspend funなのでめっちゃ同期的なコードに原点回帰が出来る。

scope.launch {

    val idList = listOf("") // ここに離れたときに削除したい applicationId
    val activityManager = IActivityManager.Stub.asInterface(
        ShizukuBinderWrapper(ServiceManager.getService("activity"))

    )

    while (isActive) {

        // 削除対象が来るまで待つ
        val removeTask = taskStackHotFlow.first { info -> info?.topActivity?.packageName in idList }
        // 別のアプリが開かれるのを待つ
        taskStackHotFlow.first { info -> info?.topActivity?.packageName !in idList }

        if (removeTask != null) {
            // 削除する
            activityManager.removeTask(removeTask.taskId)
        }
    }
}

これだけです。このアプリではこれをフォアグラウンドサービスで実行しています。

おまけ

アクセシビリティサービスでなんとかなったかも

多分いま表示されているActivityを取得することはShizuku使わずにともAccessibilityServiceで作れた可能性がある。
ただタスクから消す方法がイマイチ存在しなさそうで。

Shizuku で Toast を出す

Toastを出したかったが、何故かカメラアプリだから出せなかった。logcatには以下のように表示された。

Suppressing toast from package io.github.takusan23.onstop2finishandremovetask by user request.

Shizuku経由でToastを出せば流石に表示されるやろってことで、これで表示できると思います。

val notification = INotificationManager.Stub.asInterface(
    ShizukuBinderWrapper(ServiceManager.getService("notification"))
)
notification.enqueueTextToast(
    "com.android.shell",
    Binder(),
    "Shizuku で Toast を出す",
    Toast.LENGTH_SHORT,
    isUiContext,
    displayId,
    object : ITransientNotificationCallback.Stub() {
        override fun onToastShown() {
            // do nothing
        }

        override fun onToastHidden() {
            // do nothing
        }
    }
)

おわりに

まじで関係ない話ですが、B2CとかE2EとかP2Pとかの、to2に置き換えるやつ、同じ音(?)だから、ただ短くなるってだけの意味だけなんですかね?英語わからん・・・