- 投稿日:2019-11-26T21:51:55+09:00
僕がAndroidアプリ開発をする上で気を付けていること
こんにちは!データベースからデータを取り出す際によくCursorIndexOutOfBoundsExceptionと怒られるヨースケです。だいたいがmoveToFirst()の書き忘れで、初めてデータベースを使ったときもこのエラーでした。最初は訳も分からずめちゃくちゃ原因を特定するのにだいぶ時間がかかりました(笑)。
今回はアプリ開発をするときに気を付けていること、心掛けていることを書いていこうと思います。
- 普段使っているアプリを注意深く見る
- コピーはするがペーストはしない
- ユーザー目線
- 設計書的なものを書く
普段使っているアプリを注意深く見る
これは開発する時のレイアウトの参考になります。特に意識はしていませんが、初めて使うアプリやいろんなところで見かける端末で表示されているシステムチック?なものを見るとどういうプログラムなのかなぁとか、これはダイアログを使っているのかな?とか考えちゃいます。
例えば
Twitterのプロフィールを見るとオプションメニューやタブパネルを使っているのかな?と考えます。
個人的に好きな画面があって、パズドラの
進化サーチボタンを押したときに見えるこの画面。非常に簡潔で進化させるにはどの素材が必要なのかも一目瞭然で分かりやすいですね。それともう一つが、
これです。ソーシャルゲームは結構複雑になるので、いちいちそれ専用の画面を用意して遷移する。といった煩わしさがなく、ユーザー目線で良い画面だと僕は思います。コピーはするがペーストはしない
これは大学からJavaを習い始めて決めていることです。最初は全く分からなくて授業ではかなりつまづいていました。演習問題では友達に見せてもらうこともしばしば...で悔しい思いもありました。
アプリ開発をしていると参考書を見ても分からないことがありネットに頼る、なんてことは皆さんも経験があると思います。コピペは確かに楽ではありますが、それでは自分のためにならないと思います。一から自分で打ってどういう動きになるかを理解しながらプログラムを組むことが大切かなと思います。ユーザー目線
前のパズドラの画面でも触れましたが、多くの人が扱うアプリでは簡潔なものが求められます。どういう風なレイアウトにすればよいか、分かりやすくするにはどうすればよいかetc...開発者、特に個人でやっている人はかなり悩みます。実際僕もこのアプリを使う人が理解できるのか?複雑になっていないか?ユーザーを困らせるような処理・表示になっていないかなど、全員が全員「OK」と言えるようなアプリは難しいですがそういう風に近づいていけばなと思います。
アプリは当然、初めて使う人が多いと思います。例えば
無料通信アプリの『LINE』のウォレットの画面ではどうでしょうか?一つの画面に16種類以上の機能がありますね。ご覧の通りよくわからないものばかりで何も触っていないのでNewマークがついています(笑)。ある程度いじったことがあればそこまでないと思いますが、これを初めて見たらいろんなのがあって気後れしちゃうのかなー思ってしまいます。LINEを否定するつもりはありませんし、気軽に友達と繋がられる便利なツールだと思っています。機能が増えるのは良いですが、ユーザーが分かりやすいレイアウトも大事なのかなと思います。(まぁこれはこれでいいのかもしれません...)設計書的なものを書く
これはメモ程度だと考えていいと思います。僕の場合は誰かに見せるわけでも、発表会をするわけでもないのでExcelに
↑こんな感じでまとめています。また、ノートにも思いついたことや複雑なシステムを考えるときに書きなぐっています。これをするとどういう目的のアプリなのか、遷移図、詳しい機能やプログラム的な話などを振り返ったり、考え直したりできます。以上がAndroidアプリを作るときに僕が心掛けていることです。「そうなんだ~」程度で見ていただけたら幸いです。長文失礼しました。
- 投稿日:2019-11-26T18:30:58+09:00
データ復旧、デジタルフォレンジック手法の体験談(Android編)
前のCTF記事https://qiita.com/55momotara55/items/03ccd0e111b38061e6fc
はすごく簡単なADBサービスを利用してAndroidデバイスを侵入する手法なんです。
昨日も会社のインターンシップの同年生から「俺のスマホでやってみる?」聞いて、経済的な方面から考えると諦めしました(弁償の可能性無限大)。ですから今日朝もう一度この模擬Androidをチェックする時、この中に面白いサービスを発見しました。
/system/xbinの中にbusyboxというツールが入ってます:
busyboxはLINUXの常用コマンドをAndroidデバイスに実行できるようなツールです。
フォレンジック原理:busyboxをインストールしているANDROIDデバイスをddコマンドを実行し、今後のデジタルフォレンジックの解析するため、現時点でのAndroidデバイスのイメージファイルを作り、遠隔OSにこのイメージファイルを転送します。
mount命令を使い、誰かsystemを【mount】しているか、確認しましょう:(/dev/block/sda1です)
LISTENモードを使います、このAndroidデバイスは遠隔OSのコマンドを受け次第に8888ポートから/dev/block/sda1のイメージファイルを転送します:
遠隔OSではターミナルを使い、8888ポートからのイメージファイルの転送を受けます:
結果を確認します:
このまま記事終えることではない
リアルな使用環境を考えると、Androidデバイスにbusyboxを入れる一般人は少ないでしょう。問題点は:
1,なんのコマンド使ったら捜査対象デバイスにbusyboxを入れる?
2,コマンド嫌だから、どうすれば簡単に捜査対象デバイスのメモリを読み取る?
3,捜査対象デバイスを電源入り次第にデバッグモードを変更したい方法?
2番の答えは(電子系出身):
物理手段:chip-off法:ヒートガンを使い、フラッシュメモリをデバイスのマザーボードから取ります
、するとフラッシュメモリ読みとり専用設備を使います。
要注意:フラッシュメモリの耐熱温暖はほとんど低い(100-150度以内?)、ヒートガンの使い方が下手したら破損の可能性が非常に高いです。
3番の答えは:JTAG:直接マザーボードで配線し、強制的にデバイスをデバッグモードに変更させます(メーカーの設計図あれば助かります)。
1番の考え方:
目標:busyboxを/system/xbinに書き込みます。
1,従来、【READ-ONLY】のsystemファイルを書き込み状態にします。ここでmount -o remount
を使います。非rootアカウントはchmod
コマンドを使い、/system/xbinを読める状態にします。(非ROOTアカウントはxbinファイルを読めないから)
2,続いて、busyboxファイルをadb shellのpush
命令を使い、xbinに書き込みます。
3,現在、/system/xbin/busyboxというルートが作られ、Androidのターミナルでbusybox-install
というコマンドを使い、Androidにインストールします。
4,busyboxすでに捜査対象デバイスにインストールされ、以後の流れはこの記事の一番上からやるでしょう。今後YOUTUBEでビデオを作って詳しい操作方法を紹介したいと思います。
- 投稿日:2019-11-26T18:06:01+09:00
Eclipse環境でAndroid4.2.2のcognito認証を頑張る
概要
Android 4.2.2のアプリにAWS SDKを導入し、cognito認証してIDトークンを使いたい。
- OS
- Windows 10 Pro 64bit
- 開発環境
- Eclipse
- 言語
- Java
- ターゲット
- Android 4.2.2 (API 17)
- 使用したもの
- AWS SDK for Android 2.16.1
- gson-2.2.4.jar
- support-annotations-27.1.1.jar
なぜ今さら Eclipse なのか? Android 4.2.2 なのか? という点は気にしてはいけない。
結果
Android 4.2.2でcognito認証が可能になった。
が、IDトークン取得が初回は絶対に失敗する。(2回目以降は普通に成功)Android 4.2.2が暗号モードGCMに対応していない事が関係している模様。
(cognitoはデフォルトでは暗号モードGCMを使っているっぽい?)内容
AWS SDK for Androidを入れる
まずはAWS SDKを入れないと始まらないので、導入手順を見ながらやっていこう
…と思っていたら、Android Studioでの入れ方しか載っていなかった。SDKダウンロード
それならばと、このページからSDKをダウンロード。
※ここでダウンロードしたSDKではcongnito認証が動きません。
正常に動作するSDKの導入方法については、後述のこちら。Authenticationのリファレンスでも使っているAWSMobileClientを使うため、関連しそうなJARファイルをEclipseに突っ込もうとして、さっそく問題発生。
aws-android-sdk-mobile-client.aarAARファイルじゃないですか!!
このままではEclipseに突っ込んでも使えないので、ファイル拡張子を.zipにして展開。
aws-android-sdk-mobile-client.aar ↓ aws-android-sdk-mobile-client.zipaws-android-sdk-mobile-client/ ├ raw/ ├ res/ ├ values/ ├ AndroidManifest.xml ├ classes.jar └ R.txt展開したフォルダ内のclasses.jarを取り出して、リネームしておく。
classes.jar ↓ aws-android-sdk-mobile-client.jarこんな感じでJARファイルはそのままで、AARファイルについては展開⇒取り出し⇒リネームを行い、EclipseにSDKを入れることが出来た。
パーミッションの追加
自プロジェクト内のAndroidManifest.xmlにパーミッションを追加。
最低限必要な物のみ。<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>awsconfiguration.jsonの設定
https://aws-amplify.github.io/docs/android/authentication#manual-setup
を参考にして、プロジェクトのsrc/main/res/rawにawsconfiguration.jsonを置く。
このファイルに、Amazon CognitoのリージョンやユーザープールIDなどを書いておくことで、AWSMobileClientの初期化時に自動的に読み込んで設定してくれる。AppClientSecretについては、AWSで設定していない場合は必要ないらしい。
awsconfiguration.json
{ "IdentityManager": { "Default": {} }, "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "IDプールID", "Region": "リージョン" } } }, "CognitoUserPool": { "Default": { "PoolId": "ユーザープールID", "AppClientId": "アプリクライアントID", "Region": "リージョン" } } }src/ └ main/ └ res/ └ raw/ └ awsconfiguration.json ← ココに置くcognito認証してIDトークン取ってくる
リファレンスのInitializationとSignInを参考にしながら、下記のような感じに。
awsconfiguration.jsonの設定を自動で読み込んでくれるので、リージョンの設定などはしなくてもOK。onCreate内で初期化
void onCreate ( Bundle savedInstanceState ) { ~~ 省略 ~~ AWSMobileClient .getInstance() .initialize( this.getApplicationContext(), new Callback<UserStateDetails>() { @Override public void onResult( UserStateDetails userStateDetails ) { Log.d( "initialize", "onResult: " + userStateDetails.getUserState()); } @Override public void onError( Exception e ) { Log.e( "initialize", "onError: " ); e.printStackTrace(); } } ); ~~ 省略 ~~ }サインイン + IDトークン取得
この処理をタイマーで定期的に呼び出すようにする。
void signIn( String userID, String passwd ) { /* サンプルではコールバックだが、ここではコールバックを使わずに実装 */ try { /* 先にサインアウトしておく */ AWSMobileClient.getInstance().signOut(); /* cognitoにサインイン */ Log.d( "signIn", "start. " ); SignInResult result = AWSMobileClient.getInstance().signIn( userID, passwd, null ); if( SignInState.DONE == result.getSignInState()) { Log.d( "signIn", "done." ); Log.d( "getIdToken", "start." ); /* IDトークンを取得 */ String token = AWSMobileClient.getInstance().getTokens().getIdToken().getTokenString(); Log.d( "getIdToken", "done. " + token ); } else { Log.e( "signIn", "failed." ); } } catch ( Exception e ) { Log.e( "signIn", "error." ); e.printStackTrace(); } }例外発生(NoClassDefFoundError)
アプリを動かしてすぐに、NoClassDefFoundError 例外が発生した。クラス定義が見つからない?
E/AndroidRuntime( 3355): java.lang.NoClassDefFoundError: com.amazonaws.cognito.clientcontext.data.UserContextDataProvider E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool.getUserContextData(CognitoUserPool.java:540) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser.getUserContextData(CognitoUser.java:3394) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser.initiateUserSrpAuthRequest(CognitoUser.java:2899) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser.access$2200(CognitoUser.java:132) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser$24.run(CognitoUser.java:2461) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.continuations.AuthenticationContinuation.continueTask(AuthenticationContinuation.java:124) E/AndroidRuntime( 3355): at com.amazonaws.mobile.client.AWSMobileClient$6$1.getAuthenticationDetails(AWSMobileClient.java:1177) E/AndroidRuntime( 3355): at com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser.getSession(CognitoUser.java:778) E/AndroidRuntime( 3355): at com.amazonaws.mobile.client.AWSMobileClient$6.run(AWSMobileClient.java:1137) E/AndroidRuntime( 3355): at com.amazonaws.mobile.client.internal.InternalCallback.await(InternalCallback.java:115) E/AndroidRuntime( 3355): at com.amazonaws.mobile.client.AWSMobileClient.signIn(AWSMobileClient.java:1122) ... 以下略 ...調べてみると、どうやらaws-android-sdk-cognitoidentityprovider-asf-1.0.0.jar に含まれるクラスが必要らしい。
うっかり入れ忘れたみたいなので、さっきダウンロードしてきたSDKのJARファイルをチェック。
が、そんなファイルはなかった。念のためgithubもチェックしてみたが、そんなファイルはなかった。
存在しないファイルを要求されている・・・?
Mavenリポジトリから取ってね( by AWS )
調べてみると、aws-android-sdk-cognitoidentityprovider-asf-1.0.0.jar はMavenリポジトリにしかないことが分かった。
そもそもこのファイルは、バイナリでしか配布されていないのでgithubにソースコードはなく、ダウンロード出来るSDKにも含まれていない。つまり、このページでダウンロードできるSDKのJARファイルだけでは動かないということに。
(じゃあこのダウンロードリンクに何の意味があるんだ...)これを調べるだけで2時間近くかかってしまった。
Mavenを入れてみる
というわけで、AWS SDKをMavenリポジトリから取るべく、Mavenを入れてみることに。
https://maven.apache.org/download.cgi
からバイナリのzipファイルをダウンロードし、展開したフォルダを適当なところに置く。今回はC:直下に置いた。
C:\apache-maven-3.6.2環境変数に以下の場所を追加すると、mvnコマンドが使えるようになる。
C:\apache-maven-3.6.2\binMavenリポジトリから依存関係をダウンロードする
pom.xmlというのを書いてmvnコマンドを走らせるだけで、依存関係が解決できるらしいので、
https://mvnrepository.com/artifact/com.amazonaws/aws-android-sdk-mobile-client/2.16.1
に書いてあるのを参考に、下記のようなpom.xmlを作成し、適当なフォルダに配置。※そのままコピペしたら.jar拡張子のファイルをダウンロードしようとして失敗したため、
<type>aar</type>
を追加。<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>sample</artifactId> <version>1</version> <dependencies> <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-android-sdk-mobile-client --> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-android-sdk-mobile-client</artifactId> <version>2.16.1</version> <!-- ココを追加 --> <type>aar</type> </dependency> </dependencies> </project>あとはコマンドを実行して、依存関係をダウンロードする。
プロキシが設定されている場合は、Mavenにプロキシ設定が必要らしい。cd [pom.xmlを置いた場所] mkdir platforms\android-23 type nul > platforms\android-23\android.jar set ANDROID_HOME=%CD% mvn dependency:copy-dependencies途中で mkdir とか type とか使っているのは、依存関係解決の際に、下記のファイルがないとWarningが出てしまうためダミーファイルを作成している。
- %ANDROID_HOME%/platforms/android-23/android.jar
%ANDROID_HOME% のパスにファイルがちゃんとある場合はダミーファイルは必要ない。
コマンドの実行が完了するとpom.xmlと同階層にtargetというフォルダが作成される。
platforms/ ← ダミーフォルダ target/ ← コレ pom.xmltarget/dependencyフォルダにダウンロードしてきたファイルが入っている。
target/dependency/ ├ android-6.0.jar ← これはダミーファイルのコピー ├ aws-android-sdk-auth-core-2.16.1.aar ├ aws-android-sdk-cognitoidentityprovider-2.16.1.jar ├ aws-android-sdk-cognitoidentityprovider-asf-1.0.0.jar ├ aws-android-sdk-core-2.16.1.jar ├ aws-android-sdk-mobile-client-2.16.1.aar ├ gson-2.2.4.jar └ support-annotations-27.1.1.jarandroid-6.0.jarは、先ほど作ったダミーファイルがそのままコピーされているだけなので無視する。
JARファイルはそのまま、AARファイルは展開⇒取り出し⇒リネームの流れ作業で、Eclipseに突っ込む。これで本当にSDKが導入できた。
いざ実行
さっき書いたプログラムを改めて動かしてみる。
よっし、動い・・・ん?ログ抜粋
/* ----------------------------------------------------------- */ /* ここから1回目 */ /* ----------------------------------------------------------- */ D/signIn( 3359): start. D/AWSMobileClient( 3359): Inspecting user state details D/AWSMobileClient( 3359): waitForSignIn: userState:GUEST D/signIn( 3359): done. D/getIdToken( 3359): start. E/AWSKeyValueStore( 3359): Error in decrypting data. E/AWSKeyValueStore( 3359): javax.crypto.BadPaddingException: mac check in GCM failed E/AWSKeyValueStore( 3359): at com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineDoFinal(BaseBlockCipher.java:709) E/AWSKeyValueStore( 3359): at javax.crypto.Cipher.doFinal(Cipher.java:1111) E/AWSKeyValueStore( 3359): at com.amazonaws.internal.keyvaluestore.AWSKeyValueStore.decrypt(AWSKeyValueStore.java:438) E/AWSKeyValueStore( 3359): at com.amazonaws.internal.keyvaluestore.AWSKeyValueStore.get(AWSKeyValueStore.java:255) E/AWSKeyValueStore( 3359): at com.amazonaws.mobile.client.AWSMobileClientStore.get(AWSMobileClient.java:3377) E/AWSKeyValueStore( 3359): at com.amazonaws.mobile.client.AWSMobileClient.getSignInDetailsMap(AWSMobileClient.java:937 E/AWSKeyValueStore( 3359): at com.amazonaws.mobile.client.AWSMobileClient$11.run(AWSMobileClient.java:1692) E/AWSKeyValueStore( 3359): at com.amazonaws.mobile.client.internal.InternalCallback.await(InternalCallback.java:115) E/AWSKeyValueStore( 3359): at com.amazonaws.mobile.client.AWSMobileClient.getTokens(AWSMobileClient.java:1666) ・・・略・・・ D/AWSMobileClient( 3359): Inspecting user state details D/AWSMobileClient( 3359): waitForSignIn: userState:GUEST D/signIn( 3359): error. W/System.err( 3359): java.lang.Exception: getTokens does not support retrieving tokens while signed-out W/System.err( 3359): at com.amazonaws.mobile.client.AWSMobileClient$11.run(AWSMobileClient.java:1701) W/System.err( 3359): at com.amazonaws.mobile.client.internal.InternalCallback.await(InternalCallback.java:115) W/System.err( 3359): at com.amazonaws.mobile.client.AWSMobileClient.getTokens(AWSMobileClient.java:1666) ・・・略・・・ /* ----------------------------------------------------------- */ /* ここから2回目 */ /* ----------------------------------------------------------- */ D/AWSMobileClient( 3359): Inspecting user state details D/signIn( 3359): start. W/CognitoUserSession( 3359): CognitoUserSession is not valid because idToken is null. D/AWSMobileClient( 3359): Sending password. D/AWSMobileClient( 3359): _federatedSignIn: Putting provider and token in store D/AWSMobileClient( 3359): Inspecting user state details D/AWSMobileClient( 3359): Inspecting user state details D/AWSMobileClient( 3359): waitForSignIn: userState:SIGNED_IN D/AWSMobileClient( 3359): getCredentials: Validated user is signed-in D/signIn( 3359): done. D/getIdToken( 3359): start. D/AWSMobileClient( 3359): Inspecting user state details D/AWSMobileClient( 3359): waitForSignIn: userState:SIGNED_IN D/getIdToken( 3359): done. "IDトークン文字列"1回目のIDトークン取得が失敗してる?
何回やっても、なぜか初回だけ絶対に失敗する。
2回目以降は正常にIDトークンが取得出来ている為、問題ないと言えば問題ないが・・・。コード上の、
String token = AWSMobileClient.getInstance().getTokens().getIdToken().getTokenString();
の部分で例外が起きている。
エラーログにjavax.crypto.BadPaddingException: mac check in GCM failed
とあるので、暗号モードであるGCMが上手く使えていないようだ。Android 4.2.2は暗号モードGCMに対応していない
それもそのはずで、こちらのサイトで見る限り、Android 4.2.2はそもそもGCMに対応していないっぽい。なんてこったい。
2回目以降が正常に動いているのは、1回目で失敗したGCMから、暗号方式・暗号モードを別のものにフォールバックしているからだと思われる。
(ということはcognitoはデフォルトでは暗号モードGCMを使っているのか?)対処方法としては、TLS/SSLの暗号化ライブラリであるOpenSSL・BouncyCastleの最新版を入れるぐらいしか思いつかないが、今回は未実施。
感想
なんだか縛りプレイのような導入方法を取ってしまった。
リファレンスがbuild.gradleの書き方を提示してくれていたのだから、素直にGradleを使ってSDKの導入をやっていれば躓くことはなったような気がする。結果として動作は確認できたのでご容赦。
- 投稿日:2019-11-26T18:04:42+09:00
実機Android端末のChromeでデベロッパーツールを使ってデバッグする方法
要望
実機のAndroid端末で特定のWebページに対し、Webインスペクタで中身を書き換えたり、コンソール経由でスクリプトを叩いて実行した結果を見たい。
-> iPhone版
Android 端末のリモート デバッグを行う
MacのChromeやAndroidエミュレーターでもある程度の表示確認はできるが、やはり実機での操作感や見た目の確認は重要である。
少し調べてみたところ、MacとAndroid端末を接続することで、PCと同様に実機での動作確認が可能なことが判明したため、実際に試してみた。
Android 端末のリモート デバッグを行う | Tools for Web Developers
Developer ToolsのRemote devicesを使う
MacとAndroid端末をケーブル接続。Chromeのデベロッパーツールを開き、[Main Menu] > [More tools] > [Remote devices] を選択。
[Remote Device] タブの [Settings] で [Discover USB devices] のチェックボックスがオンになっていることを確認。
[New tab] テキストボックスで、URL を入力して [Open] ボタンをクリック。
Android端末のChromeで対象ページが開くので [Inspect] をクリックすると、デベロッパーツールが立ち上がる。
スクリーンキャストで実機Android端末と同期。要素検証や書き換えもでき、コンソールでスクリプト叩いて実行することも可能。便利。
- 投稿日:2019-11-26T17:14:15+09:00
[UE4] Androidで、大きいタイトルの起動を早くする
Google Playでは、ユーザーがダウンロードする圧縮APKが100MB以下である必要があります。 ほとんどのアプリでは、これはアプリのすべてのコードとアセットのための十分なスペースですが、UE4のゲームだと、超える場合が多いです。
そのために、グーグルさんがExpansion Files (OBB)というファイルシステムを提供します。最大2つのファイルで、一つごとに2GBまで追加できます。OBBファイルのホスティングがGoogle Play側で行っていて、節約で大規模のAndroidゲームの作成の味方です。
ただ、ゲームを起動する時に、OBBのファイルのチェックが行っていて、起動時間が延長する場合があります。
DefaultEngine.iniの中でこの次のフラグをTrueにすると、
Project/Config/DefaultEngine.ini
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
bDisableVerifyOBBOnStartUp=True起動時の確認を無視することができて、カットシーンやローディング画面中などの好きな時に行うことが可能になります。
ただしかし、「大いなる力には、大いなる責任が伴う」であり、自分で上手いタイミング(タイトル画面など)にDowloaderActivity.java.templateのvalidateXAPKZipFilesのメソッドで確認してください!
Joyeux Noël !
- 投稿日:2019-11-26T12:11:19+09:00
AndroidのChrome 78のPreload問題
問題になっているサービスは多いのではないかと思いつつも、あまり騒ぎになっていないので不思議だなと思っているkumanomiです。
どんな問題なの?
AndroidのChrome78.0.3904.108にて、Preloadの機能が有効になりページ内のリンクを勝手に踏んでしまい予期せぬ挙動をしてしまう問題が発生しています。
たとえば・・・
ログインをしようとした際にプリロード機能によって勝手にログアウトのリンクが踏まれてしまいログアウトしてしまう
商品の購入や予約ができない
強制的にトップページに戻されてしまう
選択したものと異なる振込先が選択された状態となる
結構致命的なものも多いと思うんですよね 。
同現象が起きているサービスなど一部抜粋
お知らせのサンプル
現在、AndroidスマートフォンでChromeの最新バージョン 78.0.3904.108 をお使いの一部のお客さまにおいて エラーメッセージが表示され、なにがしが 完了できない事象が発生しております。 Google Chromeの設定を変更していただくことで、 事象が解消されることがございますので、以下をお試しください。 ======================== 【表示されるエラーメッセージ】 hogehoge 【対処方法:Chromeの設定の変更(プリロード機能のオフ)】 ①ブラウザを開いた状態で右上のメニューボタン(・・・が縦に並んだボタン)を押す ②「設定」を選択する ③「プライバシー」を選択する ④「ページをプリロードして、閲覧と検索をすばやく行えるようにします」のチェックをはずす 事象が解決しない場合には、恐れ入りますが別のブラウザアプリまたはパソコンからのご利用をお試しください。issueは上がっていて・・・
https://bugs.chromium.org/p/chromium/issues/detail?id=1027991
issue内では解決されていそうだ
The server-side config has been disabled. It should roll out to users over the next couple of hours (サーバー側の設定は無効になっています。今後数時間でユーザーに展開されるはずです)しばらく待ってれば良いんですかね・・・?
(詳しい人教えてください)暫定対応された方の記事はこちら
- 投稿日:2019-11-26T11:51:15+09:00
ImageViewの表示領域内に収まらない大きな画像を自動スクロールで表示する
きっかけ
- 2019 Material Design Awardで入賞したTrip.comを見て、自動スクロールで画像を表示していて良い感じだなと思ったので試してみました。
動作環境
- Android Studio 3.5.2
- Android 10, 5
サンプルコードについて
- 画像の自動スクロール
- カスタムビューで実装する
画像の自動スクロール
画像の自動スクロール
activity_scroll_image_view.xml<ImageView android:id="@+id/sushi" android:layout_width="200dp" android:layout_height="wrap_content" android:contentDescription="@null" android:scaleType="matrix" android:src="@drawable/sushi" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
画像の自動スクロール
ScrollImagaeViewActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scroll_imagae_view) val image = findViewById<ImageView>(R.id.sushi) image.doOnLayout { var repeatCount = 0 // imageViewに格納したDrawableの幅を取得する val drawableWidth = image.drawable.intrinsicWidth val imageWidth = image.width var isScrollLeftToRight = true var currentX = 0 val handler = Handler() val r = object : Runnable { override fun run() { // 右端まで表示したらスクロール方向を左へ変更する if (drawableWidth < currentX + imageWidth) { isScrollLeftToRight = false } if (currentX < 0) { repeatCount++ isScrollLeftToRight = true } if (repeatCount == 2) return val scrollX = getScrollX(isScrollLeftToRight) currentX += scrollX image.scrollBy( scrollX, 0 ) handler.postDelayed(this, 10) } } handler.post(r) } }
動かしてみました
美味しそうな寿司が動きました
カスタムビューで実装する
カスタムビューで実装する
activity_scroll_image_view.xml<jp.co.yiwaisako.ui_sample.AutoHorizontalScrollImageView android:id="@+id/sushi" android:layout_width="200dp" android:layout_height="wrap_content" android:contentDescription="@null" android:src="@drawable/sushi" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
カスタムビューで実装する
AutoHorizontalScrollImageView(1)init { scaleType = ScaleType.MATRIX } // スクロールを繰り返す上限回数 var maxRepeatCount = 1 // 1回のスクロール量 var scrollValueXForOneTime = 1 // スクロールした回数 private var repeatCount = 0 // 現在のX位置 private var currentX = 0 // 繰り返すかどうか private val isAutoScroll: Boolean get() { return repeatCount < maxRepeatCount } // スクロール方向 private var direction: Direction = Direction.TO_RIGHT // スクロール量 private val scrollValueX: Int get() { return when (direction) { Direction.TO_RIGHT -> { scrollValueXForOneTime } Direction.TO_LEFT -> { -scrollValueXForOneTime } } } enum class Direction { TO_RIGHT, TO_LEFT }
カスタムビューで実装する
AutoHorizontalScrollImageView(2)override fun onDraw(canvas: Canvas?) { if (isAutoScroll) { scrollTo(scrollValueX) // scrollTo()の後に実行してください decideDirectionAfterScrolled() } super.onDraw(canvas) } private fun scrollTo(scrollValueX: Int) { currentX += scrollValueX scrollBy(scrollValueX, 0) } private fun decideDirectionAfterScrolled() { if (currentX <= 0) { repeatCount++ direction = Direction.TO_RIGHT } if (drawable.intrinsicWidth <= currentX + width) { direction = Direction.TO_LEFT } }
サンプルレイアウトに配置
まとめ
- 水平方向の自動スクロールを実装しました
- リピート回数、スクロール量などの調整をDatabindingで対応しても良さそう(今回は未対応です)
- コードの
scrollTo()の後に実行してください
のコメントを無くしたい(メソッドの実行順番を利用者に意識させない)のですがよい修正案が思いつかなく、アドバイスありましたらお願いします。
リンク
- 投稿日:2019-11-26T10:55:35+09:00
ExoPlayer追加でInflateExceptionになったときの対処法
現象
ExoPlayerライブラリ追加してレイアウトXML作成しビルドしてみたところ以下のようなエラーログが表示されました。
Process: com.example.exoplayersample, PID: 12229 android.view.InflateException: Binary XML file line #10 in : Binary XML file line #10 in : Error inflating class com.google.android.exoplayer2.ui.PlayerView Caused by: android.view.InflateException: Binary XML file line #10 in com.example.exoplayersample:layout/fragment_first: Error inflating class com.google.android.exoplayer2.ui.PlayerView Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at android.view.LayoutInflater.createView(LayoutInflater.java:854) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084) at android.view.LayoutInflater.inflate(LayoutInflater.java:682) at android.view.LayoutInflater.inflate(LayoutInflater.java:534) at com.example.exoplayersample.SampleFragment.onCreateView(SampleFragment.kt:53) at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2600) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:881) at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1238) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1303) at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439) at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2079) at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1869) at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1824) at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727) at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange(FragmentManagerImpl.java:2663) at androidx.fragment.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManagerImpl.java:2613) at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:246) at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:542) at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:201) at com.example.exoplayersample.MainActivity.onStart(MainActivity.kt:25) at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1425) at android.app.Activity.performStart(Activity.java:7825) at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3294) at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221) at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)対処法
Turn on Java 8 support
If not enabled already, you need to turn on Java 8 support in all build.gradle files depending on ExoPlayer, by adding the following to the android section:ExoPlayerのver2.9.0からはJava8サポートを適応させるために以下を
build.gradle
に追加しなければならないようです。compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }これで自分は無事アプリが起動できました。
- 投稿日:2019-11-26T01:31:34+09:00
Firebase App Distribution で App Distribution found more than 1 output file for this variant のエラーが出たときの対応
はじめに
Firebase App Distribution の
appDistributionUpload
の Gradle task を実行した際に== BUILD FAILED == FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:appDistributionUploadXxxYyy'. > App Distribution found more than 1 output file for this variant. Please contact firebase-support@google.com for help using APK splits with App Distribution.が表示された場合の解決方法です。
(APK splits を行っている Android project で再現します。)原因
ビルドを行うと複数の apk ファイルが生成され、どの apk ファイルがアップロード対象かどうかが特定できないためです。
対応
build.gradle
の distribution properties にapkPath
を指定する。例)
buildTypes { release { ... firebaseAppDistribution { apkPath = "app/build/outputs/apk/xxxYyy/debug/xxx.apk" } } }productFlavors を設定している場合は、もう少し工夫が必要そう。。。
※仕様に明記されていないので、サポートされなくなる可能性もあります。
※Gradle Plugin 1.2.0 で動作確認済み
- 投稿日:2019-11-26T00:21:48+09:00
【Android入門】英語・タイ語・日本語入力の判定とOK・NGボタンの配置♪
Androidアプリを作り始めて気が付いたが、スマホだと入力方法としてGoogle音声入力が選べる。
それは、スマホの環境設定によって入力言語を日本語、タイ語、英語など多くの言語に対応している。
そして、Google音声入力がそれらの言語で入力できる。しかも、いろいろな言語で入力できるだけではなく、イントネーションなどにより言語を自動的に選んでくれる。
逆に云うと、英語で入力しようとしても余りに日本語的だとカタカナなどで入力されてしまう。
また、英語になっても
This is a pain
などと間違った単語が選ばれることもある。ということで、今回は、このきちんと思い通りの入力が出来た時をOK、間違った選択をされた時、NGのボタンを配置してそれぞれの回数をカウントアップするようなアプリにしてみた。
ここで必要な技術は、主にボタンの配置とカウントアップするカウンターの配置である。
出来上がったアプリの画面は以下のようなものである。
ここでは、カウントも入力・出力で構成したので、余分に配置されているが以下で改善した。
英語 タイ語 英語? 日本語 コードについて
strings.xml<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">イベントとリスナサンプル</string> <string name="tv_name">音声入力してください</string> <string name="bt_click">表示</string> <string name="bt_clear">クリア</string> <string name="bt_ok">OK</string> <string name="ok_count">OKカウント</string> <string name="bt_ng">NG</string> <string name="ng_count">NGカウント</string>activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/tv_name"/> <EditText android:id="@+id/etName" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/bt_click"/> <Button android:id="@+id/btClear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/bt_clear"/> </LinearLayout> <TextView android:id="@+id/tvOutput" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="25dp" android:text="" android:textSize="25sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btOk" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/bt_ok"/> <EditText android:id="@+id/etOk" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="text" /> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btNg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/bt_ng"/> <EditText android:id="@+id/etNg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="text" /> </LinearLayout> </LinearLayout> </LinearLayout>MainActivity.ktpackage com.example.hellosample import android.content.Context import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.View import android.widget.Button import android.widget.EditText import android.widget.TextView import java.text.SimpleDateFormat import java.util.Date import java.io.File import java.io.IOException import kotlin.math.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //表示ボタンであるButtonオブジェクトを取得。 val btClick = findViewById<Button>(R.id.btClick) //リスナクラスのインスタンスを生成。 val listener = HelloListener() //表示ボタンにリスナを設定。 btClick.setOnClickListener(listener) //クリアボタンであるButtonオブジェクトを取得。 val btClear = findViewById<Button>(R.id.btClear) //クリアボタンにリスナを設定。 btClear.setOnClickListener(listener) //表示ボタンであるButtonオブジェクトを取得。 val btOk = findViewById<Button>(R.id.btOk) //表示ボタンであるButtonオブジェクトを取得。 val btNg = findViewById<Button>(R.id.btNg) //OK.NGボタンにリスナを設定。 btOk.setOnClickListener(listener) btNg.setOnClickListener(listener) } /** * ボタンをクリックしたときのリスナクラス。 */ private inner class HelloListener : View.OnClickListener { override fun onClick(view: View) { //名前入力欄であるEditTextオブジェクトを取得。 val input = findViewById<EditText>(R.id.etName) //名前入力欄であるEditTextオブジェクトを取得。 val input_ok = findViewById<EditText>(R.id.etOk) //名前入力欄であるEditTextオブジェクトを取得。 val input_ng = findViewById<EditText>(R.id.etNg) //入力されたcount文字列を取得。 val inputStr_ok_count = input_ok.text.toString() //入力されたcount文字列を取得。 val inputStr_ng_count = input_ng.text.toString() //メッセージを表示するTextViewオブジェクトを取得。 val output = findViewById<TextView>(R.id.tvOutput) //入力された名前文字列を取得。 val inputStr = input.text.toString() val df = SimpleDateFormat("HH:mm:ss") //"yyyy/MM/dd HH:mm:ss" val date = Date() //idのR値に応じて処理を分岐。 when(view.id) { //表示ボタンの場合… R.id.btClick -> { //入力された名前文字列を取得。 val inputStr = input.text.toString() //メッセージを表示。 output.text = df.format(date) + "\n"+inputStr + "さん、こんにちは!" //inputStr + "さん、こんにちは!" } //クリアボタンの場合… R.id.btClear -> { //名前入力欄を空文字に設定。 input.setText("") //メッセージ表示欄を空文字に設定。 output.text = "" input_ok.setText(inputStr_ok_count) input_ng.setText(inputStr_ng_count) } //OKボタンの場合… R.id.btOk -> { //OKカウントアップ val intVal_ok: Int = input_ok.text.toString().toInt()+1 val intStr_ok = intVal_ok.toString() val inputStr_ok_count = intStr_ok //数字を表示。 input_ok.setText(inputStr_ok_count) } //NGボタンの場合… R.id.btNg -> { //NGカウントアップ val intVal_ng: Int = input_ng.text.toString().toInt()+1 val intStr_ng = intVal_ng.toString() val inputStr_ng_count = intStr_ng //数字を表示。 input_ng.setText(inputStr_ng_count) } } } } }上記のコードでは以下のような画面になります。
上の画面と比べて、OKとNGの部分の数値が一つになってすっきりしました。
日本語 タイ語 英語 まとめ
・英語、タイ語、日本語入力の判定アプリを作ってみた
・演算によりカウントアップを配置した・履歴を保存できないので対応したい
- 投稿日:2019-11-26T00:19:35+09:00
AppiumでFlutterアプリのテストを自動化する 実践編
AppiumでFlutterアプリのテストを自動化する 環境構築編 - Qiita
の続きになります。
実際にテストコードを書いて、それを実行するところまでやります。
今回は勉強も兼ねつつ、比較的に簡単に用意できそうな「Python」を使います。前提条件
- Appiumの環境構築が完了していること
pipコマンドのインストール
PythonはMacで標準で入っているはずなので、特に新たにインストールする必要はありません。
pipコマンドもすでに使えるはずですが、もし入っていなかった場合は、
https://bootstrap.pypa.io/get-pip.py
をダウンロードし、ダウンロード先で以下のコマンドを実行してください。
(うまくいかない場合は、頭にsudo
と付けて実行してみてください)python get-pip.pyAppium-Python-Clientのインストール
以下のコマンドを実行し、Appium-Python-Clientをインストールします。
(うまくいかない場合は、頭にsudo
と付けて実行してみてください)pip install Appium-Python-Client今回使用するサンプルアプリ
ごく簡単なサンプルアプリを以下に用意しました。
(Android Studioで新規Flutterプロジェクトを作った際にデフォルトで書かれているカウントアップアプリです)Appium Desktopでテストコードを記録
Appium Desktopを起動させ、「Start Session」でセッションを開始するところまでやっておきます。
(手順については環境構築編を参照してください)セッションを開始すると、以下の画面が立ち上がります。
左側にアプリの画面が表示されます。
もし表示が端末の画面と一致していない場合、更新ボタンをクリックすると画面が更新されます。
まず、記録ボタンをクリックし、記録を開始します。すると、画面が切り替わり、記録ボタンの代わりに一時停止ボタンが表示されます。
記録を止める場合は、一時停止ボタンをクリックしてください。次に、端末ではなくAppiumの画面からプラスボタンをクリックします。
すると、右側にプラスボタンに関する情報が表示されます。Tapをクリックすると、上側にテストコードが表示されます。
これにより、ボタンを押した時のテストコードを記録できます。
記録されたテストコードは以下になります。el1 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button") el1.click()次に、「1」と表示されているテキストについて、きちんと「1」と表示されているかをテストしたいと思います。
要素に関しては、要素をクリックすると「Selected Element」の「Selector」から取得できます。が、Appium Desktopでどうやって記録すればよいのかわかりませんでした。
Appium-Python-Client · PyPI
のサイトによると、get_attribute関数を使ってテキストに設定されている値を取得すればできそうです。el = driver.find_element_by_class_name('android.widget.EditText') driver.set_value(el, 'Testing') text = el.get_attribute('text') assertEqual('Testing', text) el.set_value('More testing') text = el.get_attribute('text') assertEqual('More testing', text)以下の通りテストコードを記載しました。
el2 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.view.View[3]") text = el2.get_attribute('text') self.assertEqual('1', text)記録したテストコードを実行できるようにする
ただ記録しただけでは、まだ実行できる状態になっていないため、実行できるよう土台を作ります。
コードの全容は以下となります。flutter_app_for_appium_test.pyimport os import unittest from appium import webdriver from time import sleep class FlutterAppTests(unittest.TestCase): "Class to run tests against the Chess Free app" def setUp(self): "Setup for the test" desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '9' desired_caps['deviceName'] = '988a97354e4e4c5054' desired_caps['app'] = os.path.abspath(os.path.join(os.path.dirname(__file__), '/Users/Hitoshi/AndroidStudioProjects/flutter_app_for_appium/build/app/outputs/apk/release/app-release.apk')) self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) def tearDown(self): "Tear down the test" self.driver.quit() def test_single_player_mode(self): "Test the Flutter app launches correctly" sleep(1) # -----ここからAppium Desktopで記録したコードを貼り付ける el1 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button") el1.click() el2 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.view.View[3]") text = el2.get_attribute('text') self.assertEqual('1', text) # -----ここまでAppium Desktopで記録したコードを貼り付ける if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(FlutterAppTests) unittest.TextTestRunner(verbosity=2).run(suite)ここで、かいつまんで説明していきます。
まず、setUp関数の中で、デバイスとAPKの情報を記載します。
ここは、Appium Desktopの「Desired Capabilities」と同じ情報を記載すればOKです。desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '9' desired_caps['deviceName'] = '988a97354e4e4c5054' desired_caps['app'] = os.path.abspath(os.path.join(os.path.dirname(__file__), '/Users/Hitoshi/AndroidStudioProjects/flutter_app_for_appium/build/app/outputs/apk/release/app-release.apk'))次に、test_single_player_mode関数の中に、先ほどAppium Desktopで記録したテストコードを貼り付けます。
もちろん自前で実装しても構いません。
ここに実際のテストコードを記載していきます。el1 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.widget.Button") el1.click() el2 = self.driver.find_element_by_xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.view.View/android.view.View/android.view.View/android.view.View[3]") text = el2.get_attribute('text') self.assertEqual('1', text)最後に、tearDown関数で、ドライバを終了させます。
self.driver.quit()テストコードの実行
コマンドでテストコードを実行します。
python flutter_app_for_appium_test.pyすると、以下のように表示されるはずです。
端末側もアプリが起動し、ボタンが押されてカウントアップされるはずです。test_single_player_mode (__main__.FlutterAppTests) Test the Flutter app launches correctly ... ok ---------------------------------------------------------------------- Ran 1 test in 25.573s OK参考