次にonTouch()を見てみます。
@Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() != MotionEvent.ACTION_OUTSIDE) { // 【1】 return false; } String packageName = getTopActivityPackageName(); // 【2】 boolean matched = packageName.equals("com.android.chrome") || packageName.equals("com.android.browser"); // 【3】 if (!matched) { return false; } // 【4】 mTimestamp[0] = event.getEventTime(); Arrays.sort(mTimestamp); long diff = 0; for (int i = 0; i < mTimestamp.length - 1; i++) { diff += mTimestamp[i + 1] - mTimestamp[i]; } if (diff < 500) { // 【5】 Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); Toast.makeText(getApplicationContext(), "Launch from " + packageName, Toast.LENGTH_SHORT).show(); } return false; }
【1】でタッチイベントがACTION_OUTSIZEかどうかをチェックし、そうでなければ何もせずにメソッドを抜けます。今回のアプリの作りでは、ACTION_OUTSIDE以外が通知されてくることはないのですが、デバッグで一時的にViewを表示させることも考慮して、このチェックを入れています。
【2】で独自メソッドgetTopActivityPackageName()を呼び出して、タッチイベント発生時に表示されている画面のパッケージ名を取得します。
そのパッケージ名がブラウザに当たるかどうかを【3】で判定して、ブラウザではなければメソッドを抜けます。サンプルアプリでは「Android標準ブラウザ」「Chrome」の両方と比較しています。ここを変更することで、任意のアプリのタッチイベントにフックさせて処理を行えるようになります。
【4】から【5】の間で、所定のアクションが行われたかを判定しています。サンプルアプリでは500ms未満で3回タップされたことをトリガーとすることにしています。ここを任意のアクションに変更することで、任意のアクションに任意の処理を関連付けることが可能になります。
ただし、ACTION_OUTSIDEは通常のタッチイベントと異なり、以下のような制限があります。
イベントを識別可能なプロパティはeventTimeとdownTimeしかありません(ACTION_OUTSIDEでは両者は同じ値です)。つまり、トリガーとなるアクションはシングルタップ、ダブルタップ、トリプルタップ、あるいは三三七拍子などのイベント発生間隔でしか定義できません。
【5】以降では、トリガーとなるアクションから実行される処理を記述します。このサンプルでは自身のActivityを起動していますが、ここで今度は「操作可能なViewを同様の仕組みで表示し、具体的な処理をユーザーに選択させる」というのが実用的かもしれません。
最後にgetTopActivityPackageName()を見てみます。
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) private String getTopActivityPackageName() { String packageName = ""; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // 【1】 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> list = am.getRunningAppProcesses(); packageName = list.get(0).processName; } else { // 【2】 UsageStatsManager usm = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); // 【3】 long endTime = System.currentTimeMillis(); long beginTime = endTime - 7 * 24 * 60 * 60 * 1000; List<UsageStats> list = usm.queryUsageStats(UsageStatsManager.INTERVAL_BEST, beginTime, endTime); // 【4】 if (list != null && list.size() > 0) { SortedMap<Long, UsageStats> map = new TreeMap<>(); for (UsageStats usageStats : list) { map.put(usageStats.getLastTimeUsed(), usageStats); } if (!map.isEmpty()) { packageName = map.get(map.lastKey()).getPackageName(); // 【5】 } } } return packageName; }
【1】でAndroid 5未満であるかどうかを判定し、そうであればActivityManagerで現在表示されているプロセスのパッケージ名を取得します。そうでなければ【2】で使用履歴から現在表示されているプロセスのパッケージ名取得を試みます。
【3】でUsageStatsManagerを取得し、【4】のようにして使用履歴のリストを取得します。使用履歴の期間は1週間前から現在としています。結構長めに設定しているのには理由があり、実際に動作させて試してみたところ、使用履歴はタスクを切り替えるなどを行わなければ取得できなくなるようです。
例えば1週間前からずっとブラウザだけを使用していて、一度もタスクを切り替えていない場合、「UsageStatsManager#queryUsageStats(int, long, long)」は、なぜか使用履歴を返してくれなくなります。タスクを切り替えた場合は再び1週間前からの使用履歴を取得してくれるようです。
「UsageStatsManager#queryUsageStats(int, long, long)」のJavadocにある以下の例の通り、「queryUsageStats(INTERVAL_YEARLY, 2013, 2016)」で使用履歴を取得しようとしたのですが、できませんでした。
intervalType = INTERVAL_YEARLY beginTime = 2013 endTime = 2015 (exclusive) Results: 2013 - com.example.alpha 2013 - com.example.beta 2014 - com.example.alpha 2014 - com.example.beta 2014 - com.example.charlie
今のところ、INTERVAL_BESTと、インターバルを長めに設定したミリ秒を渡すのが良さそうです。
【5】で一番新しい使用履歴を現在表示されている画面のアプリとしています。
今回はACTION_OUTSIDEとSYSTEM_ALERT_WINDOWを組み合わせて、他のアプリのタッチイベントを監視して動作する常駐アプリを作りながら、必要な機能について説明していきました。タッチイベントから取得可能なプロパティがほとんどないのがつらいところですが、アイデア次第で利用シーンは広がると思います。筆者はユーザー名とパスワードなどのスニペットをクリップボードにコピーするツールを作ってみようと思います。
最後に、本稿を執筆するに当たり以下の記事を参考にさせていただいたことをお礼申し上げます。
次回は「Activity、Fragment、Viewおよびアプリのライフサイクルとコールバック」を予定しています。
株式会社ゆめみ所属のエンジニア。Applet、デスクトップJava、サーバサイドJavaの業務開発を経て、ケータイJava、組み込みJavaから現在はAndroidを中心にJavaに関わる。他の執筆記事は「Androidで動く携帯Javaアプリ開発入門」「携帯アプリを作って学ぶJava文法の基礎」など。iOS開発もたしなみ、Java以外ではHaskellやC/C++、Luaを好む。
Copyright © ITmedia, Inc. All Rights Reserved.