2021年2月23日火曜日

appium windows driver 入門 1

やりたいこと
appium windows driverを使って、C#のサンプルコードを動作させたい。
電卓を操作するサンプルコードです。

ためした環境
Win10 20H2 Home
Microsoft Visual Studio Community 2019

Step1.Windows10をdevelop modeにする
設定 > 更新とセキュリティ > 開発者向け > 開発者モード : ON
※なんのためにこれをやるのかは分かっていないです。

Step2.WinAppDriver install
こちらのサイトからWindowsApplicationDriver_1.2.1.msiをダウンロードしました。
https://github.com/Microsoft/WinAppDriver/releases
インストーラを起動して、ウィザードに従ってインストールしました。

Step3.サンプルコードのダウンロード
1.こちらのサイトにアクセスします。
https://github.com/microsoft/WinAppDriver
2.右上のcode → download zipを選択します。
Step4.サンプルコードを開く
1.ダウンロードしたフォルダのC#の電卓用のサンプルコードを開きます。以下のパスにあります。
\WinAppDriver-master\Samples\C#\CalculatorTest
CalculatorTest.sln

2.CalculatorTest.slnをダブルクリックして開きます。
※Visual Studioが必要になりますので、入っていない場合はインストールしてください。


Visual Studioが起動したら、一旦このままにしておきます。

Step5.WinAppDriverを起動
C:\Program Files (x86)\Windows Application DriverにあるWinAppDriver.exeをダブルクリックします。 コマンドプロンプトが起動します。
これでWinAppDriverが起動したこのになるので、このままにしておきます。
Step6.テスト実行
いよいよテストを実行します。
Visual Studioにて、テストメニュー > 全てのテストをデバッグを選択します。
電卓が起動して、VisualStudio側で例外が発生したら、ひとまず成功です。

112行目の「splitViewPane.FindElementByName("Standard Calculator").Click();」で例外となります。
これはsampleコードが英語環境用に作られているためとなります。

電卓の左上あたりの「標準」をクリックするというプログラムなのですが、sampleでは「standard calculator」を探しているので、それがなくてエラーとなります。
Step7.UIの情報
日本語環境ではうまく動かないので、日本語のコントロールの名称に置き換えて、sampleコードをまともに動くようにしていきます。

1.UIの情報を取得するツール inspect.exeを起動します。
Visual Studio がインストールされている場合は、下記のパスにあります。
C:\Program Files (x86)\Windows Kits\10\bin\XXXXX\x64 (XXXXX はバージョン番号)
なければ、こちらのページを参考にインストールしてください。
https://qiita.com/RPAbot/items/f1d6438c8f2500db65ae

2.inspect.exeを起動したまま、電卓のクリックしたい場所をマウスでポイントすると、inspect.exeにその情報が表示されます。
sampleの「standard calculator」に該当するのは、「標準 電卓」という名前のようです。


3.先ほど例外が発生したところを下記のように書き換えて、もう一度テストを実行してみます。
(変更前)
splitViewPane.FindElementByName("Standard Calculator").Click();
(変更後)
splitViewPane.FindElementByName("標準 電卓").Click();

先ほどよりは少し進むようになりますが、また別のところで例外が発生すると思います。
あとは同じような感じで直していけば最後まで動くようになります。
最後まで直したコードがこちらになります。
Step8.コードを全面的に修正
全面的に日本語環境用に修正したコードがこちらになります。
//******************************************************************************
//
// Copyright (c) 2017 Microsoft Corporation. All rights reserved.
//
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//******************************************************************************

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium.Windows;
using System.Threading;
using System;

namespace CalculatorTest
{
    [TestClass]
    public class ScenarioStandard : CalculatorSession
    {
        private static WindowsElement header;
        private static WindowsElement calculatorResult;

        [TestMethod]
        public void Addition()
        {
            // Find the buttons by their names and click them in sequence to perform 1 + 7 = 8
            session.FindElementByName("1").Click();
            session.FindElementByName("プラス").Click();
            session.FindElementByName("7").Click();
            session.FindElementByName("等号").Click();
            Assert.AreEqual("8", GetCalculatorResultText());
        }

        [TestMethod]
        public void Division()
        {
            // Find the buttons by their accessibility ids and click them in sequence to perform 88 / 11 = 8
            session.FindElementByAccessibilityId("num8Button").Click();
            session.FindElementByAccessibilityId("num8Button").Click();
            session.FindElementByAccessibilityId("divideButton").Click();
            session.FindElementByAccessibilityId("num1Button").Click();
            session.FindElementByAccessibilityId("num1Button").Click();
            session.FindElementByAccessibilityId("equalButton").Click();
            Assert.AreEqual("8", GetCalculatorResultText());
        }

        [TestMethod]
        public void Multiplication()
        {
            // Find the buttons by their names using XPath and click them in sequence to perform 9 x 9 = 81
            session.FindElementByXPath("//Button[@Name='9']").Click();
            session.FindElementByXPath("//Button[@Name='乗算']").Click();
            session.FindElementByXPath("//Button[@Name='9']").Click();
            session.FindElementByXPath("//Button[@Name='等号']").Click();
            Assert.AreEqual("81", GetCalculatorResultText());
        }

        [TestMethod]
        public void Subtraction()
        {
            // Find the buttons by their accessibility ids using XPath and click them in sequence to perform 9 - 1 = 8
            session.FindElementByXPath("//Button[@AutomationId=\"num9Button\"]").Click();
            session.FindElementByXPath("//Button[@AutomationId=\"minusButton\"]").Click();
            session.FindElementByXPath("//Button[@AutomationId=\"num1Button\"]").Click();
            session.FindElementByXPath("//Button[@AutomationId=\"equalButton\"]").Click();
            Assert.AreEqual("8", GetCalculatorResultText());
        }

        [TestMethod]
        [DataRow("1",   "プラス", "7", "8")]
        [DataRow("9",  "マイナス", "1", "8")]
        [DataRow("8", "除算", "8", "1")]
        public void Templatized(string input1, string operation, string input2, string expectedResult)
        {
            // Run sequence of button presses specified above and validate the results
            session.FindElementByName(input1).Click();
            session.FindElementByName(operation).Click();
            session.FindElementByName(input2).Click();
            session.FindElementByName("等号").Click();
            Assert.AreEqual(expectedResult, GetCalculatorResultText());
        }

        [ClassInitialize]
        public static void ClassInitialize(TestContext context)
        {
            // Create session to launch a Calculator window
            Setup(context);

            // Identify calculator mode by locating the header
            try
            {
                header = session.FindElementByAccessibilityId("Header");
            }
            catch
            {
                header = session.FindElementByAccessibilityId("ContentPresenter");
            }            

            // Ensure that calculator is in standard mode
            if (!header.Text.Equals("Standard", StringComparison.OrdinalIgnoreCase))
            {
                session.FindElementByAccessibilityId("TogglePaneButton").Click();
                Thread.Sleep(TimeSpan.FromSeconds(1));
                var splitViewPane = session.FindElementByClassName("SplitViewPane");
                splitViewPane.FindElementByName("標準 電卓").Click();
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Assert.IsTrue(header.Text.Equals("標準", StringComparison.OrdinalIgnoreCase));
            }

            // Locate the calculatorResult element
            calculatorResult = session.FindElementByAccessibilityId("CalculatorResults");
            Assert.IsNotNull(calculatorResult);
        }

        [ClassCleanup]
        public static void ClassCleanup()
        {
            TearDown();
        }

        [TestInitialize]
        public void Clear()
        {
            session.FindElementByName("クリア").Click();
            Assert.AreEqual("0", GetCalculatorResultText());
        }

        private string GetCalculatorResultText()
        {
            string newStr = "";
            newStr = calculatorResult.Text.Replace("表示は", string.Empty).Trim();
            return newStr.Replace("です", string.Empty).Trim();
            //return calculatorResult.Text.Replace("Display is", string.Empty).Trim();
        }
    }
}
Step9.できた!!
これで実行すると、電卓が立ち上がって、計算をして、終了するところまで走ります。
できた。
参考にしたサイト
(1)Appium Windows Driverのページ
http://appium.io/docs/en/drivers/windows/
(2)Windows Application Driver を使った Windows ネイティブアプリケーションの UI テスト自動化
https://qiita.com/yuuhu04/items/5a96608ad96eccee34a0

2021年2月11日木曜日

2021年2月6日土曜日

Android Keystore パスワード 忘れた2

 5年ぐらい前に自分のアプリのkeystoreのパスワードを忘れたという記事を書きました。

こちらです。

当時は、Android_Keystore_Password_Recover_1.07.jar を使用して、忘れたパスワードを探したのですが見つからずあきらめてました。


たまたまこちらの記事を見つけて試してみたら見つかりました。

忘れてしまったAndroidの署名情報は見つかるかもしれない


よかった。


ざっくり言うと、taskartifacts.binをバイナリエディタで開いて、storePasswordで検索したところ、そのちょっと後ろにばっちり書いてありました。keyAliasもありました。

※バイナリエディタで検索するときは大文字/小文字をちゃんと意識して検索したほうがよさそうです。

↓の図では赤いところにパスワードが表示されました。



2020年8月30日日曜日

GDPR

古いアプリをAndroid10に対応する必要があるのですが、ついでにAdmobも最新のルールにそって実装しておこうかなと思ったのですが、ちょっと躓いてしまいました。

 GDPR(一般データ保護規則)です。

EU圏向けに対応する必要があるらしく、結構面倒そう。

ひとまず勉強中です。

2020年8月29日土曜日

Androidの古いアプリのターゲットを「Android 10(API レベル 29)」にしたので、その記録

やりたいこと
だいぶ前に作ったAndroidのアプリがあります。
最終更新が2015年のアプリと2016年のアプリです。
この古いアプリのターゲットをAndoroid10(APIレベル29)に変更することが目的です。

Googleから2020年11月2日までにターゲットにAndroid10を含めてねというメールが来たためとなります。

Googleからのメールには移行ガイドのリンクが張ってあったのですが、そのページをみても具体的にどうすればよいのかよくわかりませんでした。

そこで、ひとまず古いアプリのプロジェクトをAndroid Studio 4.0.1で開いてみたのですが、よくわからないエラーがいろいろでてきました。

ググりながらなんとか立ち上がるところまでできたので、手順を記録しておきます。
環境
OS : Windows10 1909
IDE : Android Studio 4.0.1
古いAndroidアプリの情報
 compileSdkVersion 21
 buildToolsVersion "21.1.2"
 minSdkVersion 9
 targetSdkVersion 21

やったこと① バックアップ
失敗すると悪いので、いったん昔のプロジェクトのフォルダをコピーしてから作業をしました。
やったこと② 開く
Android Studioでプロジェクトを開く。
開くとConvertProjectのメッセージが表示されたので、Convertボタンを押す。

そのあと、Android SDKのパスも前と違うよというメッセージが表示されました。
これはSDKのパスを以前と変えたのででたようです。通常は表示されないです。
やったこと③ versionを修正
build.gradleのバージョンを最近作ったアプリのバージョンに合わせました。
 compileSdkVersion 29
 buildToolsVersion "30.0.0"
 minSdkVersion 16
 targetSdkVersion 29

build.gradleのclasspathのバージョンも修正
classpath 'com.android.tools.build:gradle:4.0.0'

gradle-wrapper.propertiesのdistributionUrlを修正
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

このあたりはあまり深く考えずにやってみました。
やったこと④ Migrate to AndroidX
appcompat-v7, support-v4が解決できないみたいなエラーがでているので、これを解消するために下記の手順を実施しました。
Refactorメニュー - Migrate to AndroidX... を選択する。

次のメッセージが表示されるので、Migrateボタンを押す。
※backup project as Zip fileをONにしておくと、バックアップしてくれます。不要ならOFFにします。

この段階だとまだMigrateは完了してないです。Migrateするために、Do Refactorボタンを押します。
ステータスバーがくるくるしてちょっと待つと終わります。
やったこと⑤ compileを修正
compileを直した方がよいようなので修正しました。
マウスカーソルを合わせると電球のマークが表示されるので、そこからcompileをimplementationに置き換えるメニューを選択すると直してくれます。
やったこと⑥ ActionBarActivityをおきかえ
ActionBarActivityを使っていたのですが、そこでエラーがでてたので、AppCompatActivityに置換しました。
//import android.support.v7.app.ActionBarActivity;
import androidx.appcompat.app.AppCompatActivity;
~途中省略~
//public class MainActivity extends ActionBarActivity implements SensorEventListener {
public class MainActivity extends AppCompatActivity implements SensorEventListener {

おわり
ここまでやったところ、エラーの表示がなくなり、エミュレーターで起動できるようになりました。
自分のアプリはこれで解決しましたが、使ってるクラスや機能によって⑥以降は違ってくるはずです。

2020年7月30日木曜日

Android10 充電ケーブル 抜き差しのイベント来ない?

充電ケーブルの抜き差しのイベントを検知するアプリを作っているのですが、
Android10でテストしたところ、うまく動作しないことがわかりました。
(イベントを検知するやり方を記載した記事はこちらです。)

プログラムは、公式の情報を参考にしているので、間違えはなさそう。
他のOSのバージョンもテストしたところ、Android8, Android9は問題なし。

ちなみにテストはemulatorで実施しており、emulatorの電源の状態を変更するコントロールでAC charger / noneを切り替えて実施してます。

環境

Android Studio : 
スマホ : emulator Pixel 3a API29 (android10)

これが原因か?①

emulatorを起動すると、常にUSB充電中となっていることがわかりました。

これについては、開発モードにはいって(ビルド番号を何回か連打)、USBデバックをOFFにすると解除されることが分かりました。

→ USB充電中は解除されたが、電源のイベントを拾えないのは変わらず。

※Android9もUSBデバックはONだったので、emulatorはONになっているのかもしれません。


これが原因か?②

emulator (もしくはOS)が悪そう。
emulatorのバッテリーのコントロールパネルで電源の状態を変更しても、
emulator側に反映されていないことが分かりました。

左がemulator, 右がコントロールパネルです。
levelやconnectionがあっていないことが分かります。



これが原因か?③

②の原因ですが、Android10で「電池使用料のトラッキング」について変更が入ってます。
これが原因かどうかはもうちょっと情報を集めてみようと思います。

以下引用です。
------------------------
Android 10 以降では、主要な充電イベントの後にデバイスのプラグを抜くと、SystemHealthManager によって電池使用量の統計情報がリセットされます。主要な充電イベントとはつまり、デバイスが完全に充電されること、またはほとんど電池残量がない状態からほぼ完全に充電された状態になることです。

Android 10 より前は、電池残量がほとんど変化していなくても、デバイスのプラグを抜くと電池使用量の統計情報がリセットされていました。
------------------------


結論

すみません。まだよくわかってません。





2020年7月23日木曜日

Android Foreground Serviceのメモ

やりたいこと
ボタンを押すと、フォアグラウンドサービスを起動するやり方を記録しておきます。

環境
OS : Windwos10
Android Studio 4.0
言語 : java

参考にしたサイト
■Foreground Serviceの基本
https://qiita.com/naoi/items/03e76d10948fe0d45597
おおむねこちらのページを参考にしてます。

■公式情報
https://developer.android.com/guide/components/services?hl=ja
公式情報も参考にしました。

背景
※ここは読み飛ばしていただいて結構です。※
充電ケーブルが接続されたかどうかを検知して、何か処理するというアプリを検討中なのですが、通常のアプリだと裏側にいると勝手にOSによって終了させられてしまいます。

Androidの公式ページでこのように記載されてます。
「システムは RAM を解放する必要がある場合にプロセスを強制終了します。システムが特定のプロセスを強制終了する可能性は、...」
https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja

また、Android7まではマニフェストに「イベントを検知するよ。」という一文を書いておくと、アプリのプロセスがいなくても、イベントが発生すると再起動してくれたのですが、Android8以降はアプリのプロセスがいないと、イベントがひろえなくなってしまいました。
(イベントを検知する仕組みの詳細はこちらを参考にしてください。)

じゃあどうするかというと、アプリをServiceとして裏側で生かしておいて、常にイベントを検知できるようすればよいということになります。

それで終わりかというと、Android8以降ではまたルール改正が発生しており、Serviceをバックグラウンド(通知とかなにもださずに)で動作させていると、強制終了させられるというルールが追加されました。
詳しくはこちらを参考にしてください。


ルール改正がはげしい...


じゃあどうするかというと、その答えがForegroundServiceとなります。

ということで、前置きが長くなりましたがForegroundServiceの勉強をしたので、メモしておきます。
ざっくり言うと
ざっくりいうと以下の手順となります。
・serviceクラス作成
   この中で自分がForegroundであることを知らせるための処理をごちゃごちゃいれる。
・通知用のアイコン準備 ・Manifestにserviceクラスを登録
・MainActivityからserviceを起動

それでは、細かく書いていきます。

Step1.準備
1.Android StudioでEmpty Activityでプロジェクトを作成する。
Name : MyApp
Package name : test.test ← 適当につけました。
Minimum SDK : Api 16 (android 4.1) ←デフォルトのまま

2.ボタンを1個配置して、OnClickListenerを作る。
後でこのボタンを押すとServiceが起動するようにします。
大事なところはまだですが、ここまではこんな感じのコードになります。
package test.test;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    @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) {

                // サービスを呼ぶ
            }
        });
    }
}
Step2.通知用のアイコン作成
serviceクラスから通知(notification)を行う必要があるのですが、通知用の小さいアイコンが必須になるので、事前に作っておきます。

(1)app - res - drawable - 右クリック - New - Image Assetを選択

(2)下図の①のところをNotification Iconsに設定、
    ②のところに任意の名称を設定(ここではic_service_notificationに設定しました)、
    ③でiconの形状を設定(ここではデフォルトのままにしました。)します。

  (3)設定したらNext - Finish
Step3.serviceクラス作成
package test.test;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;

import androidx.core.app.NotificationCompat;


//(1) Serviceの派生クラスを作成
public class MyService  extends Service {

    //(2) 必須のメソッド。 バインドしない場合はnullを返す。
    @Override
    public IBinder onBind(Intent intent){
        return null;
    }

    //(3.3)の一部  startforegroudnに渡すIDを定義する。 0以外の数字 。
    private static final int ONGOING_NOTIFICATION_ID = 1;

    //(3) 呼び出し側でstartService()かstartForegroundService() をすると呼び出されます。
    @Override
    public int onStartCommand(Intent intent,
                              int flags,
                              int startId) {

        //(3.1)  チャンネルIDの登録
        String channelID = "MY_CHANNEL_ID"; // 通知チャンネル用のID。適当な名前を定義 
        createNotificationChannel(channelID);

        // 通知をタップしたときにアプリを呼び出す準備
        Intent notificationIntent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent =  PendingIntent.getActivity(this, 0, notificationIntent, 0);

        // 通知の準備
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelID)
                .setContentTitle("通知のタイトル")
                .setContentText("通知の内容")
                .setSmallIcon(R.drawable.ic_service_notification)
                .setContentIntent(pendingIntent)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);

        // (3.3) このAPIを呼ぶと、フォアグラウンドサービスとなる。
        startForeground(ONGOING_NOTIFICATION_ID, builder.build());

        // (3.4) 戻り値を指定。
        return START_STICKY;
    }


    // 通知のチャンネルを作成する。この関数はほぼもってきたそのまま。
    // https://developer.android.com/training/notify-user/build-notification?hl=ja
    private void createNotificationChannel(String channelID) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            //アプリ - 通知 に表示される情報
            CharSequence name = "サービス起動中の通知"; // なんでもOK。
            String description = "サービス起動中の通知は、XXXのための通知です。"; // なんでもOK。

            // 通知のレベルとか名称とか設定
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(channelID, name, importance);
            channel.setDescription(description);

            // 通知の登録
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }
}

上記コードの説明
(1)serviceクラスを継承したクラスを作成します。
(2)onBind()を実装。必須のメソッドとなります。
  bindしない場合はreturn nullでOKです。
(3)onStartCommand()を実装。
  親アプリがstartService()をするとこちらが呼ばれます。
(3.1) createNotificationChannelをして、通知チャンネルを作成。Android8以降で必要。
    設定 - 通知で使用される機能になります。
(3.2) 通知の準備
フォアグラウンドサービスにするには、通知を表示して、生きていることをユーザーに知らせる必要があるため、必須となります。 その準備として、通知のタイトルや通知のアイコンなどを準備してます。

 (3.3) startForeground()を実行
このAPIを呼び出すことでフォアグラウンドサービスとなります。第一引数のidには0以外 の値を設定する必要があります。ここでは1を指定しました。

(3.4) 戻り値を設定( 3種類あるので用途に合わせて設定 )

このコードでは文字列は直値を記載してますが、実際はstrings.xmlに定義して、使用したほうが良いと思います。

Step4.manifest
(1) applicationの中に、<service android:name=".MyService">を追加します。
(2) application>の外に、<uses-permission android:name="android.permission.FOREGROUND_SERVICE">を追加します。

(2)のほうは、Android9以降では必須となります。

以下xml全体となります。うまくコードをはれなかったので、図を張ってます。

Step5.MainActivityからサービスを呼ぶ
package test.test;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    @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) {

                // サービスを呼ぶ
                Intent serviceIntent = new Intent(MainActivity.this, MyService.class);
                startService(serviceIntent);
            }
        });
    }
}
Step5.テスト
emulatorの5.1と10で動くことだけ確認しました。