20191126のAndroidに関する記事は11件です。

僕がAndroidアプリ開発をする上で気を付けていること

 こんにちは!データベースからデータを取り出す際によくCursorIndexOutOfBoundsExceptionと怒られるヨースケです。だいたいがmoveToFirst()の書き忘れで、初めてデータベースを使ったときもこのエラーでした。最初は訳も分からずめちゃくちゃ原因を特定するのにだいぶ時間がかかりました(笑)。

 今回はアプリ開発をするときに気を付けていること、心掛けていることを書いていこうと思います。

  • 普段使っているアプリを注意深く見る
  • コピーはするがペーストはしない
  • ユーザー目線
  • 設計書的なものを書く

普段使っているアプリを注意深く見る

 これは開発する時のレイアウトの参考になります。特に意識はしていませんが、初めて使うアプリやいろんなところで見かける端末で表示されているシステムチック?なものを見るとどういうプログラムなのかなぁとか、これはダイアログを使っているのかな?とか考えちゃいます。
 例えばtwitter.jpg
Twitterのプロフィールを見るとオプションメニューやタブパネルを使っているのかな?と考えます。
 個人的に好きな画面があって、パズドラの
pzdr2.jpg
pzdr3.jpg
進化サーチボタンを押したときに見えるこの画面。非常に簡潔で進化させるにはどの素材が必要なのかも一目瞭然で分かりやすいですね。それともう一つが、
pzdr1.jpg
これです。ソーシャルゲームは結構複雑になるので、いちいちそれ専用の画面を用意して遷移する。といった煩わしさがなく、ユーザー目線で良い画面だと僕は思います。

コピーはするがペーストはしない

 これは大学からJavaを習い始めて決めていることです。最初は全く分からなくて授業ではかなりつまづいていました。演習問題では友達に見せてもらうこともしばしば...で悔しい思いもありました。
 アプリ開発をしていると参考書を見ても分からないことがありネットに頼る、なんてことは皆さんも経験があると思います。コピペは確かに楽ではありますが、それでは自分のためにならないと思います。一から自分で打ってどういう動きになるかを理解しながらプログラムを組むことが大切かなと思います。

ユーザー目線

 前のパズドラの画面でも触れましたが、多くの人が扱うアプリでは簡潔なものが求められます。どういう風なレイアウトにすればよいか、分かりやすくするにはどうすればよいかetc...開発者、特に個人でやっている人はかなり悩みます。実際僕もこのアプリを使う人が理解できるのか?複雑になっていないか?ユーザーを困らせるような処理・表示になっていないかなど、全員が全員「OK」と言えるようなアプリは難しいですがそういう風に近づいていけばなと思います。
 アプリは当然、初めて使う人が多いと思います。例えば
Screenshot_20191126-135849.png
無料通信アプリの『LINE』のウォレットの画面ではどうでしょうか?一つの画面に16種類以上の機能がありますね。ご覧の通りよくわからないものばかりで何も触っていないのでNewマークがついています(笑)。ある程度いじったことがあればそこまでないと思いますが、これを初めて見たらいろんなのがあって気後れしちゃうのかなー思ってしまいます。LINEを否定するつもりはありませんし、気軽に友達と繋がられる便利なツールだと思っています。機能が増えるのは良いですが、ユーザーが分かりやすいレイアウトも大事なのかなと思います。(まぁこれはこれでいいのかもしれません...)

設計書的なものを書く

 これはメモ程度だと考えていいと思います。僕の場合は誰かに見せるわけでも、発表会をするわけでもないのでExcelに
mydesign1.PNG
mydesign2.PNG
mydesin3.PNG
↑こんな感じでまとめています。また、ノートにも思いついたことや複雑なシステムを考えるときに書きなぐっています。これをするとどういう目的のアプリなのか、遷移図、詳しい機能やプログラム的な話などを振り返ったり、考え直したりできます。

 以上がAndroidアプリを作るときに僕が心掛けていることです。「そうなんだ~」程度で見ていただけたら幸いです。長文失礼しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データ復旧、デジタルフォレンジック手法の体験談(Android編)

前のCTF記事https://qiita.com/55momotara55/items/03ccd0e111b38061e6fc
はすごく簡単なADBサービスを利用してAndroidデバイスを侵入する手法なんです。
昨日も会社のインターンシップの同年生から「俺のスマホでやってみる?」聞いて、経済的な方面から考えると諦めしました(弁償の可能性無限大)。

ですから今日朝もう一度この模擬Androidをチェックする時、この中に面白いサービスを発見しました。
/system/xbinの中にbusyboxというツールが入ってます:
image.png

busyboxはLINUXの常用コマンドをAndroidデバイスに実行できるようなツールです。

フォレンジック原理:busyboxをインストールしているANDROIDデバイスをddコマンドを実行し、今後のデジタルフォレンジックの解析するため、現時点でのAndroidデバイスのイメージファイルを作り、遠隔OSにこのイメージファイルを転送します。

まずrootを取ります。
image.png

mount命令を使い、誰かsystemを【mount】しているか、確認しましょう:(/dev/block/sda1です)
image.png

LISTENモードを使います、このAndroidデバイスは遠隔OSのコマンドを受け次第に8888ポートから/dev/block/sda1のイメージファイルを転送します:
image.png

遠隔OSではターミナルを使い、8888ポートからのイメージファイルの転送を受けます:
image.png
結果を確認します:
image.png

このまま記事終えることではない

リアルな使用環境を考えると、Androidデバイスにbusyboxを入れる一般人は少ないでしょう。問題点は:
1,なんのコマンド使ったら捜査対象デバイスにbusyboxを入れる?
2,コマンド嫌だから、どうすれば簡単に捜査対象デバイスのメモリを読み取る?
3,捜査対象デバイスを電源入り次第にデバッグモードを変更したい方法?
2番の答えは(電子系出身):
物理手段:chip-off法:ヒートガンを使い、フラッシュメモリをデバイスのマザーボードから取ります
、するとフラッシュメモリ読みとり専用設備を使います。
要注意:フラッシュメモリの耐熱温暖はほとんど低い(100-150度以内?)、ヒートガンの使い方が下手したら破損の可能性が非常に高いです。
3番の答えは:JTAG:直接マザーボードで配線し、強制的にデバイスをデバッグモードに変更させます(メーカーの設計図あれば助かります)。
image.png

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でビデオを作って詳しい操作方法を紹介したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.aar

AARファイルじゃないですか!!

このままではEclipseに突っ込んでも使えないので、ファイル拡張子を.zipにして展開。

aws-android-sdk-mobile-client.aar
↓
aws-android-sdk-mobile-client.zip
aws-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/rawawsconfiguration.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トークン取ってくる

リファレンスのInitializationSignInを参考にしながら、下記のような感じに。
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.jarMavenリポジトリにしかないことが分かった。
そもそもこのファイルは、バイナリでしか配布されていないので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\bin

Mavenリポジトリから依存関係をダウンロードする

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.xml

target/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.jar

android-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の導入をやっていれば躓くことはなったような気がする。

結果として動作は確認できたのでご容赦。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

実機Android端末のChromeでデベロッパーツールを使ってデバッグする方法

要望

実機のAndroid端末で特定のWebページに対し、Webインスペクタで中身を書き換えたり、コンソール経由でスクリプトを叩いて実行した結果を見たい。

-> iPhone版

Android 端末のリモート デバッグを行う

MacのChromeやAndroidエミュレーターでもある程度の表示確認はできるが、やはり実機での操作感や見た目の確認は重要である。

スクリーンショット 2019-11-26 16.46.24.png

少し調べてみたところ、MacとAndroid端末を接続することで、PCと同様に実機での動作確認が可能なことが判明したため、実際に試してみた。

Android 端末のリモート デバッグを行う | Tools for Web Developers

Developer ToolsのRemote devicesを使う

MacとAndroid端末をケーブル接続。Chromeのデベロッパーツールを開き、[Main Menu] > [More tools] > [Remote devices] を選択。
スクリーンショット 2019-11-26 16.36.35.png

[Remote Device] タブの [Settings] で [Discover USB devices] のチェックボックスがオンになっていることを確認。

スクリーンショット 2019-11-26 16.38.18.png

[Devices] から対象のAndroid端末を選択。
スクリーンショット 2019-11-26 16.38.59.png

[New tab] テキストボックスで、URL を入力して [Open] ボタンをクリック。
スクリーンショット 2019-11-26 16.39.06.png

Android端末のChromeで対象ページが開くので [Inspect] をクリックすると、デベロッパーツールが立ち上がる。
スクリーンショット 2019-11-26 16.39.43.png
スクリーンキャストで実機Android端末と同期。要素検証や書き換えもでき、コンソールでスクリプト叩いて実行することも可能。

便利。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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 !

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
(サーバー側の設定は無効になっています。今後数時間でユーザーに展開されるはずです)

しばらく待ってれば良いんですかね・・・?
(詳しい人教えてください)

暫定対応された方の記事はこちら

preload問題とその暫定的対応

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ImageViewの表示領域内に収まらない大きな画像を自動スクロールで表示する

 きっかけ

  • 2019 Material Design Awardで入賞したTrip.comを見て、自動スクロールで画像を表示していて良い感じだなと思ったので試してみました。

record-191125110141.gif


動作環境

  • 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.kt
    override 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)
        }
    }

動かしてみました

record-191125112627.gif


美味しそうな寿司が動きました :relaxed:


カスタムビューで実装する


カスタムビューで実装する

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
        }
    }

サンプルレイアウトに配置

  • record-191126194714.gif

まとめ

  • 水平方向の自動スクロールを実装しました
  • リピート回数、スクロール量などの調整をDatabindingで対応しても良さそう(今回は未対応です)
  • コードのscrollTo()の後に実行してくださいのコメントを無くしたい(メソッドの実行順番を利用者に意識させない)のですがよい修正案が思いつかなく、アドバイスありましたらお願いします。 :clap:

リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
}

これで自分は無事アプリが起動できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 で動作確認済み

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android入門】英語・タイ語・日本語入力の判定とOK・NGボタンの配置♪

Androidアプリを作り始めて気が付いたが、スマホだと入力方法としてGoogle音声入力が選べる。
それは、スマホの環境設定によって入力言語を日本語、タイ語、英語など多くの言語に対応している。
そして、Google音声入力がそれらの言語で入力できる。

しかも、いろいろな言語で入力できるだけではなく、イントネーションなどにより言語を自動的に選んでくれる。
逆に云うと、英語で入力しようとしても余りに日本語的だとカタカナなどで入力されてしまう。
また、英語になっても
This is a pain
などと間違った単語が選ばれることもある。

ということで、今回は、このきちんと思い通りの入力が出来た時をOK、間違った選択をされた時、NGのボタンを配置してそれぞれの回数をカウントアップするようなアプリにしてみた。

ここで必要な技術は、主にボタンの配置とカウントアップするカウンターの配置である。
出来上がったアプリの画面は以下のようなものである。
ここでは、カウントも入力・出力で構成したので、余分に配置されているが以下で改善した。

英語 タイ語 英語? 日本語
ihaveanapple.jpg kaochai.jpg thisisapeing.jpg honjitu.jpg

コードについて

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.kt
package 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の部分の数値が一つになってすっきりしました。

日本語 タイ語 英語
new_honjitu.jpg new_kaochai.jpg new_canihelpu.jpg

まとめ

・英語、タイ語、日本語入力の判定アプリを作ってみた
・演算によりカウントアップを配置した

・履歴を保存できないので対応したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppiumでFlutterアプリのテストを自動化する 実践編

AppiumでFlutterアプリのテストを自動化する 環境構築編 - Qiita
の続きになります。
実際にテストコードを書いて、それを実行するところまでやります。
今回は勉強も兼ねつつ、比較的に簡単に用意できそうな「Python」を使います。

前提条件

  • Appiumの環境構築が完了していること

pipコマンドのインストール

PythonはMacで標準で入っているはずなので、特に新たにインストールする必要はありません。
pipコマンドもすでに使えるはずですが、もし入っていなかった場合は、
https://bootstrap.pypa.io/get-pip.py
をダウンロードし、ダウンロード先で以下のコマンドを実行してください。
(うまくいかない場合は、頭にsudoと付けて実行してみてください)

python get-pip.py

Appium-Python-Clientのインストール

以下のコマンドを実行し、Appium-Python-Clientをインストールします。
(うまくいかない場合は、頭にsudoと付けて実行してみてください)

pip install Appium-Python-Client

今回使用するサンプルアプリ

ごく簡単なサンプルアプリを以下に用意しました。
(Android Studioで新規Flutterプロジェクトを作った際にデフォルトで書かれているカウントアップアプリです)

Screenshot_20191125-231835.jpg

Appium Desktopでテストコードを記録

Appium Desktopを起動させ、「Start Session」でセッションを開始するところまでやっておきます。
(手順については環境構築編を参照してください)

セッションを開始すると、以下の画面が立ち上がります。
左側にアプリの画面が表示されます。
もし表示が端末の画面と一致していない場合、更新ボタンをクリックすると画面が更新されます。
まず、記録ボタンをクリックし、記録を開始します。

1.png

すると、画面が切り替わり、記録ボタンの代わりに一時停止ボタンが表示されます。
記録を止める場合は、一時停止ボタンをクリックしてください。

2.png

次に、端末ではなくAppiumの画面からプラスボタンをクリックします。
すると、右側にプラスボタンに関する情報が表示されます。

3.png

Tapをクリックすると、上側にテストコードが表示されます。

4.png

これにより、ボタンを押した時のテストコードを記録できます。
記録されたテストコードは以下になります。

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」から取得できます。

5.png

が、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.py
import 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

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む