Activityの起動とlaunchModeについて
ActivityのlaunchMode
の違いが気になっていたので、公式で推奨されている standard
, singleTop
についてざっくり整理しました。
launchMode=“standard"、あるいは指定がない場合
Androidのシステムは常に、ターゲットタスク*1内で新しいActivityのインスタンスを作成します。
つまり、
<activity android:name=".MainActivity" android:launchMode="standard" > <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない --> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
となっている場合、
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(MainActivity.this, MainActivity.class)); } }); }
と書いたらボタンを押せば何枚でもMainActivityが生成されてスタックに積まれていくわけです。
こんな感じに。
launchMode=“singleTop"の場合
Androidのシステムは、ターゲットタスク内で
- 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない
- 起動しようとしているActiivtyがタスクのトップにない場合は新しいActivityを生成する
というふるまいをします。ややこしいですね。
ちょっと動かしてみましょう。
<activity android:name=".MainActivity" android:label="FirstActivity" android:launchMode="singleTop"> <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない --> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".NextActivity" android:launchMode="singleTop" android:label="SecondActivity" > </activity>
// NextActivityもほぼ同じコード public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(MainActivity.this, MainActivity.class)); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(MainActivity.this, NextActivity.class)); } }); } }
交互に二つのActivityを起動する場合は、新しいActivityが生成されスタックに積まれていきます。
同じActivityを連続して起動しようとすると、singleTopの 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない
という性質により、新しいActivityのインスタンスは生成されず、スタックにあるActivityは増えません。
ところで、launchMode
が standard
のアクティビティを利用している場合、前のActivityから渡されたIntentをonCreateで受け取って表示やAPIへのリクエストパラメータに利用します。
それでは、singleTopのActiivtyがスタックの一番上にあるときに、もう一度Activityを起動しようとしたことを検知、あるいは、その際渡されたIntentを元に表示などを変更したい場合、どうしたらいいでしょうか。
公式ドキュメントにも書いてありますが、Activity.onNewIntent(Intent)
で可能です。
// 先ほどのMainActivityのコードを少し編集 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); startActivity(intent); } }); // 略 } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show(); }
ECやメディア系のアプリで起動時にダイアローグを表示して、押されたボタンによっては、トップ画面の表示をデフォルトではなく特定の単語やカテゴリで検索かけた結果を表示するようにしたい、なんていう場合などは覚えておくと便利そうです。
現場からは以上です。
*1:おおむね起動しようとしているActivityの属するアプリのタスク
Androidの通知チャンネルの振る舞いをプロジェクトのtargetSDKVersionを変えて少し試してみた
Android OのEaster Eggはoctopusでした*1が、OのコードネームはOreoでしたね。さて、Android Oではユーザーが通知をより便利に管理するために通知チャンネルが導入されました。
通知チャンネルがなんたるかや主要な扱い方については公式他に任せるとして、この記事については、通知チャンネルについて自分が試してみた少しマイナーなことについてまとめようと思います。
TL;DR
- Android Oの端末でも、アプリのtargetSDKVersionが25以下なら通知チャンネルを作成せずとも通知は届く
- Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成しているならば、その通知チャンネルが作成&利用可能であると同時に「Uncategorized」 という通知チャンネルが作成される
- Android Oの端末で、アプリのtargetSDKVersionが26なら、通知チャンネルを作成しないと通知が届かないし、その旨のToastが出る
- 通知チャンネルを作成したあと、同じIDを指定して作成し直すことで「チャンネル名」などのチャンネルの設定が更新できるが、バッジの有無の設定は更新できない
以下、検証用のコードなどを置いておきます。
Android Oの端末でも、アプリのtargetSDKVersionが25以下の場合
// アプリのプロジェクトのbuild.gradle android { compileSdkVersion 25 buildToolsVersion '25.0.3' defaultConfig { applicationId 'com.example.woshidan.test' minSdkVersion 16 targetSdkVersion 25 versionCode 1 versionName "1" } ...
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(android.R.drawable.ic_btn_speak_now) .setContentTitle("Title") .setContentText("Text"); final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify("tag", 1234, builder.build());
届いた通知は以下のような形で表示され、通知チャンネルの表示は出ないようです。
Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成している場合
のように、ライブラリプロジェクトを作成して、ライブラリプロジェクト側で targetSdkVersion を26にします。
// libraryプロジェクトのbuild.gradle apply plugin: 'com.android.library' android { compileSdkVersion 26 buildToolsVersion "26.0.1" defaultConfig { minSdkVersion 15 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" }
// アプリ側のプロジェクトのbuild.gradle dependencies { ... compile project(path: ':mylibrary') }
package com.example.woshidan.mylibrary; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.support.annotation.RequiresApi; /** * Created by woshidan on 2017/08/22. */ public class NotificationChannelCreator { @RequiresApi(api = 26) public static void createChannel(final Context context) { NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT); channel.setDescription("テストの説明です"); channel.setShowBadge(true); // create or update the Notification channel final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel); } }
// アプリ側のプロジェクト public class MainActivity extends Activity { @RequiresApi(api = 26) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); NotificationChannelCreator.createChannel(this); // 通知チャンネルを指定しないで通知を送る // ここで通知を送っていない場合でもUncategorizedチャンネルはできる final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(android.R.drawable.ic_btn_speak_now) .setContentTitle("Title") .setContentText("Text"); final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify("tag", 1234, builder.build());
Android Oの端末で、アプリのtargetSDKVersionが26の場合
// アプリのプロジェクトのbuild.gradle android { compileSdkVersion 26 buildToolsVersion '26.0.1' defaultConfig { applicationId 'com.example.woshidan.test' minSdkVersion 16 targetSdkVersion 26 versionCode 1 versionName "1" } ...
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(android.R.drawable.ic_btn_speak_now) .setContentTitle("Title") .setContentText("Text"); final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify("tag", 1234, builder.build());
通知チャンネルの更新について
NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT); channel.setDescription("テストの説明です"); channel.setShowBadge(true); // create or update the Notification channel final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel);
一度上記のコードで通知チャンネルを作成すると、下記のようなチャンネルができます。
その後、下記のコードで更新すると
NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル Updated", NotificationManager.IMPORTANCE_HIGH); channel.setDescription("テストの説明を変えました"); channel.setShowBadge(false); // create or update the Notification channel final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel);
こうなります。
現場からは以上です。
Android SDK Toolsのバージョンを25.2.xから25.3.xにあげたらcocos runコマンドが実行できなくなった
症状
Android SDK Toolsのバージョンをあげて cocos run
コマンドを実行した際、以下のメッセージが出てビルドできなくなりました。
$ cocos run -p android --android-studio Building mode: debug Using Android Studio project : /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio running: '/Users/woshidan/Library/Android/sdk/tools/android update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app' ************************************************************************* The "android" command is deprecated. For manual SDK, AVD, and project management, please use Android Studio. For command-line tools, use tools/bin/sdkmanager and tools/bin/avdmanager ************************************************************************* Invalid or unsupported command "update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app" Supported commands are: android list target android list avd android list device android create avd android move avd android delete avd android list sdk android update sdk Error running command, return code: 2.
原因
cocos
コマンドは Android SDK Tools
のバージョン 25.2.x 以下でサポートされている android
コマンドに依存しており、そのコマンドの一部が Android SDK Tools
の 25.3.0
でサポートされなくなりました。
サポートされなくなったコマンドは Ant
( =Eclipse
) 用のコマンドで、Googleは Eclipse
のサポートをやめると言っているので今後もサポートされることはないでしょう。
対応
本当は、この辺りを読んで、NDKでcocos
コマンドでビルドしていた部分を書き換えて行くのが良いと思いますが、さしあたっては 公式リポジトリの関連issue にある、こちらのコメント からDLできる古めのAndroid Toolsを使ってしのぐかなーという感じです。
API19, API21のAndroid端末で透過GIFを表示すると、透過色の部分が真っ黒になることがある
Android 4.0, 4.1の対応がまだ必要なので、adb shellを使ってAndroidのUIの手動確認自動化について調べてみた
AndroidのUITest(手動)と自称していた手動の確認作業を自動化したかったのですが、UITest用ツールないしフレームワークで現在有力な選択肢と思しきEspressoやAppiumはAPI18以降対応です。そのため、まだ4.0, 4.1系でのテストが外せない自分の仕事の環境では、それだけではそれなりに手動でぽちぽちやる部分が残ってしまいました。
そこで、アサーションというか結果の確認は人間が目視でやることになるかもしれませんが、 adb shell input
を利用して手動でやっている作業の自動化についていくらか調べてみました。
自動化するために必要なこと
まず、自動化するために必要なことは大雑把に下記3点が必要となります。
- 自動化する操作を端末に実行させる
- 自動化する操作にまつわるパラメータを取得する
- 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する
この記事では、上記3点をそれぞれ順に検討していきます。
TL;DR
- 自動化する操作を端末に実行させる
- タップ、スワイプ程度なら
adb shell input command
形式 - マルチタッチなど複雑な操作をする場合は
adb shell sendevent
で可能
- タップ、スワイプ程度なら
- 自動化する操作にまつわるパラメータを取得する
- 回転やクリックなど特定のイベントの前後、といったタイミングでの操作を取得したいならAndroidのソースコード上で
Window.Callback
を使ってタッチイベントを取得して要所を抽出といったアプローチが良いかも- ソースコード上でならタッチイベント以外の情報も一緒に取得しやすいため
- マルチタッチなどややこしい動作の再現をしたかったり、タッチイベント取得用のコードをプロジェクトのソースコードに作業中でも入れるのに抵抗がある場合、
adb shell getevent
で取得した値をすこしいじってそのままadb shell sendevent
に渡してやるのが楽かも- 全てのタッチイベントを
adb shell sendevent
でタッチイベントを実行していくのはやや遅いので、ある程度タイミングがシビアでない操作が許容できたら
- 全てのタッチイベントを
- 回転やクリックなど特定のイベントの前後、といったタイミングでの操作を取得したいならAndroidのソースコード上で
- 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する
- どの方法で実行させるか、どの形式でタッチイベント取得するか決めたら粛々と文字列を加工する
1. 自動化する操作を端末に実行させる
adb shell input でタッチイベントをAndroidで実行する
最初に自分が触ってみるまでよくわかってなかったので確認ですが、以下のように
$ adb shell input tap 100 100
と入力すると、現在ADBと接続されているデバイスの画面上で100x100の座標の位置がタップされます。
Usage: input [<source>] <command> [<arg>...] The sources are: // これは端末によって対応しているものが異なることも mouse keyboard joystick touchnavigation touchpad trackball stylus dpad touchscreen gamepad The commands and default sources are: text <string> (Default: touchscreen) keyevent [--longpress] <key code number or name> ... (Default: keyboard) tap <x> <y> (Default: touchscreen) swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen) press (Default: trackball) roll <dx> <dy> (Default: trackball)
複数デバイスがADBと接続されている場合は -s
オプションでタッチイベントを実行する端末を指定することが可能です。また、その際は adb devices
コマンドで取得できる端末のIDを利用します。
上記の The commands and default sources are:
以下を見ていただくとマルチタッチに相当しそうなコマンドがありませんが、自分が調べた範囲では、基本的に adb shell input command
ではマルチタッチを実行することができません。マルチタッチを必要とするピンチイン・ピンチイン、あるいはマルチタッチそのものを実行させたい場合は、
adb shell sendevent input_event_file type key data
のように、adb shell sendevent
を利用して、やや生々しい形で送る必要があるようです。input_event_file
と表示している部分の引数は adb shell getevent
で表示される /dev/input/eventN
のパスにあるファイルです*1。
// たとえば手元でいつも検証に使っている端末で表示して見た結果は以下 $ adb shell getevent add device 1: /dev/input/event8 name: "msm8974-taiko-mtp-snd-card Headset Jack" add device 2: /dev/input/event7 name: "msm8974-taiko-mtp-snd-card Button Jack" add device 3: /dev/input/event6 name: "system_als" add device 4: /dev/input/event5 name: "mhl-rcp" add device 5: /dev/input/event2 name: "qpnp_pon" add device 6: /dev/input/event1 name: "clearpad" add device 7: /dev/input/event0 name: "bu520x1nvx" could not get driver version for /dev/input/mice, Not a typewriter add device 8: /dev/input/event4 name: "qpnp_chg_unplug_key" add device 9: /dev/input/event3 name: "gpio-keys"
input_event_file
として指定するeventファイルによっては、イヤフォンジャックの抜き差しといったことも取得可能です。今回は画面のタップなどの操作を対象としているので、getevent
や setevent
を実行する際は clearpad
という name
の書かれている /dev/input/event1
を対象として実行することにします。ここの name
やタッチパネルに当たるeventファイルのパスは端末によって違う可能性があるので、それぞれの端末で自動化を行う前に確認します。
// たとえば適当なエミュレータで試した結果 $ adb shell getevent * daemon not running. starting it now at tcp:5037 * * daemon started successfully * add device 1: /dev/input/event0 name: "Power Button" add device 2: /dev/input/event1 name: "qwerty2"
2. 自動化する操作にまつわるパラメータを取得する
前節でコマンドを adb shell
からどう実行すればいいか、といった話をしたので、この節では adb shell input
や adb shell sendevent
に渡すパラメータをどう取得するか、といった話をします。
記事を書く際、自分の中では「どのタッチイベントを対象とするか」「テスト用にプロジェクトのコードを編集するか」という観点で下記の2つのアプローチを検討しました。
Window.Callback
を利用してAndroid
のアプリ側のソースコード上でタッチイベントの内容を取得して一部抽出するadb shell getevent
を利用してeventファイルへの入力をそのまますべて取得する
それでは、以下で各々の手法について紹介します。
自分で書いててこの節は、
- どうやってタッチイベントを取得するか
- 取得したイベントの中からどれを利用するか
- 利用するイベントをどう抽出するか
についてまだ分離して書き直せる気もしますが、今回は一旦こういう感じでお許しください。。
2-1. Window.Callback
を利用して Android
のアプリ側のソースコード上でタッチイベントの内容を取得して一部抽出する
Window という表示領域に関する様々なことを管理するクラスがありまして((と書いたものの、Windowは抽象クラスであり、実際管理しているのはそのサブクラスのPhoneWindowですが https://developer.android.com/reference/android/view/Window.html ))、
Windowからのコールバック呼び出しを受け取るための
Window.Callbackインタフェースを利用すると、タッチイベント含む
Window` に関するイベントが呼び出された際に独自の処理を追加して実装することができます。
この Window.Callback
の dispatchTouchEvent
メソッドをオーバーライドして、タッチイベントの座標などを記録する処理を挟む、というのがこの節で紹介する方法となります。
いろいろ細かいことは置いておいて乱暴に書いてしまうと、下記のようになります。
public class MyWindowCallback implements Window.Callback { final static String TAG = MyWindowCallback.class.getSimpleName(); Window.Callback mOriginalCallback; // public MyWindowCallback(Window.Callback originalCallback) { this.mOriginalCallback = originalCallback; } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i(TAG, "dispatchTouchEvent " + event.toString()); // タッチイベントの内容をログに出す return mOriginalCallback.dispatchTouchEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { return mOriginalCallback.dispatchTrackballEvent(event); // それ以外のタッチイベントは元々のコールバックを呼び出すだけ }
// タッチイベントを取りたいアクティビティ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... final Window win = getWindow(); final Window.Callback localCallback = win.getCallback(); win.setCallback(new MyWindowCallback(localCallback)); }
この方針でも次の方針でも、画面に対するほぼ全てのタッチイベントが収集できますが、出力先がログにせよ、ファイルにせよ出力される内容が非常に多いです。そのため、見出しでも触れましたが、どの操作が最低限必要なタッチイベントなのか区別する必要があります。しかし、この関数をプロジェクト内に足しているように、Androidのソースコード上でタッチイベントを収集するように自動化の際にソースコードを一時的にでも編集することを許容する場合、目印となるログを入れたりすることも比較的容易なのでまぁ大丈夫なのではと思います。
なお、 dispatchTouchEvent
から変換する際、マルチタッチについては、POINTER_UP
, POINTER_DOWN
のイベントとMOVE
の引数を見比べながら変換する必要があり、実行コマンドへの変換処理がなかなかややこしくなります。
しかしまぁ、概ね手動で行っている確認作業はシングルタップが実行できれば十分だし、最小限に抽出されたadb shell input
のコマンドは読めなくもないので、タッチイベントの抜け漏れに注意しながら使えば比較的楽そうです。また、 onContentChanged
メソッドなど他のメソッドも利用することで、回転など操作の再現に必要な他の情報も手に入れられそうです*2。
2-2. adb shell getevent
を利用してeventファイルへの入力をそのまま取得する
2-1の方針において、
- マルチタッチの実行コマンドへの変換処理がややこしい
- 自動化作業のためにソースコードをいじることに抵抗があるケースもある
といった話をしました。それでは、ソースコードに変更を加えることなくタッチイベントを取得する方法として、 adb shell getevent
の話をします。
$ adb shell getevent input_event_file
を実行すると、タッチイベントが発生するたび、標準出力に
0003 0039 00000000 0003 0037 00000000 0003 0035 00000289 0003 0036 0000030b 0003 003a 0000003b 0000 0002 00000000 0003 0039 00000001 0003 0037 00000000 0003 0035 000001ee 0003 0036 000003b1 0003 003a 00000039 0000 0002 00000000 0000 0000 00000000 0003 0039 00000000 0003 0037 00000000 0003 0035 00000289 0003 0036 0000030a ...
といった数字の列が吐き出されます。これは、adb shell sendevent
で利用できるパラメータとなっています。-tl
オプションを利用してもう少し人間の目にわかりやすくした場合のログが下記です。
[ 88406.873926] EV_ABS ABS_MT_TRACKING_ID 00000000 [ 88406.873930] EV_ABS ABS_MT_TOOL_TYPE MT_TOOL_FINGER [ 88406.873934] EV_ABS ABS_MT_POSITION_X 00000368 [ 88406.873937] EV_ABS ABS_MT_POSITION_Y 00000748 [ 88406.873939] EV_ABS ABS_MT_PRESSURE 00000038 [ 88406.873943] EV_SYN SYN_MT_REPORT 00000000 [ 88406.873946] EV_SYN SYN_REPORT 00000000 [ 88406.884306] EV_ABS ABS_MT_TRACKING_ID 00000000 [ 88406.884309] EV_ABS ABS_MT_TOOL_TYPE MT_TOOL_FINGER [ 88406.884312] EV_ABS ABS_MT_POSITION_X 00000367 [ 88406.884355] EV_ABS ABS_MT_POSITION_Y 00000747 [ 88406.884358] EV_ABS ABS_MT_PRESSURE 00000038 [ 88406.884360] EV_SYN SYN_MT_REPORT 00000000 [ 88406.884362] EV_SYN SYN_REPORT 00000000 ...
パラメータに対して、16進数 -> 10進数の変換は必要ですが、このeventファイルに入力されたタッチイベントの内容を文字通りそのままadb shell sendevent
に突っ込み直すので、マルチタッチ含めて各種タッチイベントが実行できます。
しかし、全てのタッチイベントを adb shell sendevent
で実行すると遅くなるので、タイミングが比較的シビアなケースには不向きですし、細々回転や特定のViewの表示タイミングなど adb shell getevent
で取得できない行動がはさまれるものを確認したい場合には面倒です。
3. 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する
これについては、どの方式で値を取得するかが決まったらそれに合わせてちょいちょいと正規表現を用いて値を抽出するスクリプトを書く、といった話になるので、今回は 2-2 の方針でタッチイベントを adb shell getevent
で取得した値を adb shell sendevent
で使えるように加工 & adb shell sendevent
で取得・記録していた操作を実行するスクリプトを例として載せて締めます。
$ adb shell getevent input_event_file(cf. /dev/input/event0) > ./record.txt
// ./record.txt 0003 0039 00000000 0003 0037 00000000 0003 0035 00000289 0003 0036 0000030b 0003 003a 0000003b 0000 0002 00000000 0003 0039 00000001 0003 0037 00000000 0003 0035 000001ee 0003 0036 000003b1 0003 003a 00000039 0000 0002 00000000 0000 0000 00000000 0003 0039 00000000 0003 0037 00000000 0003 0035 00000289 0003 0036 0000030a 0003 003a 0000003c 0000 0002 00000000 0003 0039 00000001 0003 0037 00000000 0003 0035 000001ed 0003 0036 000003b1 0003 003a 0000003b 0000 0002 00000000 0000 0000 00000000 0003 0039 00000000 0003 0037 00000000
begin File.open("./record.txt", "r") do |file| file.each_line do |line| result = line.match(/(\w{4}) (\w{4}) (\w{8})/) type = result[1].to_i(16) key = result[2].to_i(16) data = result[3].to_i(16) # puts "adb shell sendevent /dev/input/event1 #{type} #{key} #{data}" 確認用ログ system("adb shell sendevent /dev/input/event1 #{type} #{key} #{data}") end end rescue Error => e # 中の人がざっくり利用するスクリプトなので雑 puts e.to_s end
まとめ
思ったよりダラダラ長くなったので反省した。繰り返し実行するにあたってはもういくつか考えることはありますが、それに関することは細かいし別記事にします。 Window.Callbackのあたりのコードが雑云々については、ここで書いたコードは production に残すことじゃなくてさっと手作業代替するために値をとることを目的としているのでそれ以外には使わないでください、ということで。
現場からは以上です。
参考
- Android Debug Bridge
- androidの操作を自動化したときの覚書
- adbコマンドを利用したandroid制御まとめ
- ADB でアプリを起動する intent を投げる (am start/broadcast)
- AndroidにUSB ADB経由でキーイベントとかタッチイベントを送り込む
- Rubyでシェルコマンドを実行する方法
- Androidの操作を自動化する
- PHP+ADB でAndroid画面操作の保存と再現をする
- Window.Callback
- android - Detect every touch events without overriding dispatchTouchEvent()?
*1:詳しくは http://gihyo.jp/book/2017/978-4-7741-8759-4
*2:そういったタッチイベントを取得するためのライブラリはすでにあるかもしれませんが自分で調べた範囲であまり調べてよさそうなのなかったので中身把握してるのチャーっと書いちゃうのでいいんじゃないでしょうか
java.util.TimerをAndroidで使ってみた場合
以前気になったので素振りした時のメモ。AndroidのバージョンはAPI 21です。
まとめ
Timer.schedule(TimerTask task, long delay, long interval)
はdelay
秒後から、前回のタスクと今回のタスクの実行開始時間の間隔がなるべくintervalになるように定期的にtaskを実行する- Timerのコンストラクタで新しいスレッドが動き出し、定期的に実行されるタイマーのタスクは全てそのスレッドで実行される
Activity.onStop
が呼び出されてアプリがバックグラウンドへ隠れてもTimerの処理は止まらない
参考:
- http://techbooster.jpn.org/andriod/application/934/
- https://docs.oracle.com/javase/jp/6/api/java/util/Timer.html
簡単にコード書いてログで確認してみた
package com.example.woshidan.timertest; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Timer; import java.util.TimerTask; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Timer timer = new Timer(); final Random random = new Random(); final List<Integer> list = new ArrayList<>(); final long startTime = System.currentTimeMillis(); timer.schedule(new TimerTask() { @Override public void run() { Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " start "); int wait = random.nextInt(1000); try { Thread.sleep(wait + 1000); // アップロード処理の終了の代わりに適当にウェイト置いてる } catch (InterruptedException e) { e.printStackTrace(); } Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " end "); list.add(list.size()); } }, 500, 3000); } @Override protected void onStop() { super.onStop(); Log.d("MainActivity", "onStop"); } @Override protected void onDestroy() { super.onDestroy(); Log.d("MainActivity", "onDestroy"); } }
// 定期的にリクエストされるタイマーのタスクは全て同じスレッドで実行される D/Timer-0: 500 ms. n = 0 start D/Timer-0: 2218 ms. n = 0 end D/Timer-0: 3500 ms. n = 1 start // 前回と今回の間のstartの間隔はなるべく3秒間隔 D/Timer-0: 4896 ms. n = 1 end D/Timer-0: 6500 ms. n = 2 start D/Timer-0: 7733 ms. n = 2 end D/Timer-0: 9500 ms. n = 3 start D/Timer-0: 11096 ms. n = 3 end D/MainActivity: onStop // バックグラウンドになっても止まらない D/Timer-0: 12500 ms. n = 4 start D/Timer-0: 14368 ms. n = 4 end D/Timer-0: 15530 ms. n = 5 start // 遅れることもある(前回分の実行に時間がかかったなど) D/Timer-0: 17289 ms. n = 5 end D/Timer-0: 18568 ms. n = 6 start // そうするとその次で「前回と今回の間のstartの間隔はなるべく3秒間隔」を守るために、次以降も時間がずれたままになる D/Timer-0: 19608 ms. n = 6 end D/Timer-0: 21608 ms. n = 7 start D/Timer-0: 23448 ms. n = 7 end D/Timer-0: 24645 ms. n = 8 start D/Timer-0: 26188 ms. n = 8 end D/Timer-0: 27678 ms. n = 9 start D/Timer-0: 29216 ms. n = 9 end D/Timer-0: 30718 ms. n = 10 start D/Timer-0: 32033 ms. n = 10 end D/Timer-0: 33744 ms. n = 11 start D/Timer-0: 34993 ms. n = 11 end
コードでボタンのStateによって色を変えるようにする
前回の続き的なノリで、コードでボタンのStateによって色を変えるように指定するコードを書いてみます。
API21以上の場合
API21以上の場合は、以下のように記述できます。
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setCornerRadius(roundRadius); int[][] states = new int[][] { new int[] {-android.R.attr.state_pressed}, // not pressed new int[] { android.R.attr.state_pressed} // pressed }; int [] colors = new int[] { Color.RED, Color.GREEN }; // API21以上対応 gradientDrawable.setColor(new ColorStateList(states, colors)); if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(gradientDrawable); } else { button.setBackgroundDrawable(gradientDrawable); }
API20以下も対応する場合
これで先ほどと同じ振る舞いを確保しようとした場合は下記のようになります。
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawableNotPressed = new GradientDrawable(); gradientDrawableNotPressed.setCornerRadius(roundRadius); gradientDrawableNotPressed.setColor(Color.RED); GradientDrawable gradientDrawablePressed = new GradientDrawable(); gradientDrawablePressed.setCornerRadius(roundRadius); gradientDrawablePressed.setColor(Color.GREEN); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[] {-android.R.attr.state_pressed}, gradientDrawableNotPressed); // not pressed stateListDrawable.addState(new int[] {android.R.attr.state_pressed}, gradientDrawablePressed); // pressed if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(stateListDrawable); } else { button.setBackgroundDrawable(stateListDrawable); }
現場からは以上です。