【Android】UI Automator 2.0でUIテストをする
UI AutomatorはGoogleが開発しているAndroid向け自動UIテストフレームワークです。 2015年3月にバージョンが2.0になりGradleに対応しました。 有名なテストフレームワークのAppiumも中ではUI Automatorを実行しています。
Espressoとの違い
GoogleはテストフレームワークとしてEspressoも開発しています。EspressoとUI Automatorの違いとして最も大きいのは、UI Automatorは複数のアプリをテストできるということです。Espressoはアプリのソースコードに紐付いており、アプリのプロジェクトの中に入れる必要があります。それに対してUI Automatorは自分のアプリはもちろんのこと、設定アプリや自分で作ったツールアプリや他社のアプリ、通知バーなど自由に操作することができます。ソースコードと紐付いていないので、別プロジェクトで管理することもできますし、アプリと同じプロジェクトに含めることもできます。またUI Automatorは2.0からEspressoと併用できるようになったため、自分のアプリはEspresso、他のアプリを操作するときはUI Automatorといった書き方が可能です。
UI AutomatorはAndroid 4.3以上のみをサポートしています。まだまだAndroidでは最新のOSのみを対象とはできない状況なので、通常はAndroid 4.3以上のみを対象とていないことがほとんどかと思います。ここはProductFlavorを分けるかプロジェクトごと分けることで対応します。なおEspressoはAndroid 2.2からサポートしており、設定アプリなどを使わないテストのみであれば、Espressoのほうが幅広くテストを行えます。
設定
UI Automatorは2.0からAndroid Support Libraryの一部となり、build.gradleで簡単に使うことができるようになりました。 まずAndroid SDK ManagerからAndroid Support Repositoryをインストールします。
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion "26.0.0" defaultConfig { applicationId "com.phicdy.uiautomator2sample" minSdkVersion 14 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } productFlavors { production { minSdkVersion 14 } uiTest { minSdkVersion 18 } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:design:25.3.1' uiTestCompile 'com.android.support.test:runner:0.5' uiTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' }
testInstrumentationRunnerをandroid.support.test.runner.AndroidJUnitRunnerに指定します。これでJUnit4形式でテストが書けるようになります。productFlavorをアプリ用とUI Automator用で分け、UI Automator用のminSdkVersionを18(Android 4.3)に設定します。今回はuiTestとしました。
最後に実行に必要なライブラリであるcom.android.support.test:runner:0.5とcom.android.support.test.uiautomator:uiautomator-v18:2.1.2をuiTestCompileで読み込みます。
テストを書く時や実行時は、Android Studio上のBuild VariantsをuiTestDebugに変更します。
これでUI Automatorの設定は終わりです。
テストを書く
テストはsrc/androidTest以下にJUnit4の書き方で追加していきます。 デフォルトでApplicationTestが入っていますが必要ないので消し、新たにテストを追加します。
package com.phicdy.uiautomator2sample; import android.content.Context; import android.content.Intent; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SdkSuppress; import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.By; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; import org.junit.Test; import org.junit.runner.RunWith; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; @RunWith(AndroidJUnit4.class) @SdkSuppress(minSdkVersion = 18) public class MainUiTest { @Test public void floatingButtonTest() { UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); // Launch MainActivity Context context = InstrumentationRegistry.getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); // Click floating button UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000); if (btn == null) fail("Floating button was not found"); btn.click(); UiObject2 snakeBar = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000); assertNotNull(snakeBar); assertThat(snakeBar.getText(), is("Replace with your own action")); } }
今回はプロジェクト作成するときにTabbed Activityで作成したデフォルトのアプリのテストを作ります。 テストステップは以下の通りです。
- MainActiivtyを起動
- FloatingButtonを押す
- SnakeBarが出るので文言が"Replace with your own action"であることを確認
テストの初めにUiDeviceのインスタンスを取得します。 UiDeviceは端末の操作をしたり、UiObject2を取得したり、様々な場面で使います。 UiDeviceを管理するクラスを用意してもいいと思います。
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
MainActivity起動部分です。UI Automator 2.0ではContextが使えるようになったのでstartActivity()で起動します。 MainActivityはアプリ起動時に起動されるActivityなので、context.getPackageManager().getLaunchIntentForPackage()で対象のパッケージ名を指定して起動しています。
// Launch MainActivity Context context = InstrumentationRegistry.getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.phicdy.uiautomator2sample"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);
FloatingButtonをクリックしてSnackBarのUiObject2を取得する部分です。
UiObject2 btn = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/fab")), 3000);
UI Automator 2.0では各ViewをUiObject2として取得します。主に取得には、UiDevice#findObject()かUiDevice#wait()を使います。 UiDevice#wait()を使うとViewが描画されるまで待ってくれるのでテストの成功率が上がります。
UiDevice#wait()にはSearchConditionとtimeoutを指定します。 SearchConditionはどの条件で待つかを指定します。Untilという便利なクラスがあるので、これを使ってSearchConditionを作ります。今回は特定のUiObject2が出るまで待つので、Unitl.findObject()を使います。Unitl.findObject()にはBySelectorを指定します。BySelectorはByクラスから生成してUiObject2を特定する条件を指定します。条件はresoruse IDやテキスト、クラス名などで指定します。resource IDが確実なので、アプリ側できるだけresource IDを設定します。Android SDKのtoolsにuiautomatorviewerというツールがあるのでこれで簡単にIDやViewの階層構造を確認できます。Android Studio 2.2からはこういった機能がAndroid Studio自体に追加されるようです。
UiObject2を取得したらnullチェックをしてエラーハンドリングをした後、クリックします。
if (btn == null) fail("Floating button was not found"); btn.click();
クリックするとSnackBarが出るので、テキスト部分を同様に取得します。 最後にテキスト部分のnullチェックと文言を確認します。
UiObject2 snakeBarText = device.wait(Until.findObject(By.res("com.phicdy.uiautomator2sample:id/snackbar_text")), 3000); assertNotNull(snakeBarText); assertThat(snakeBarText.getText(), is("Replace with your own action"));
最後に
UI Automator 2.0を使うことで、UIテストが書けました。 今回は1つのアプリだけを対象としたテストでしたが、例えば設定アプリを起動して機内モードをオンにし、エラーダイアログが出るか確認するといったテストもUI Autoamtor 2.0では可能です。 UIテストを書くことでCIでのリグレッションテストや手動テストの削減ができるので、少しずつ書いていきたいです。
今回のサンプルプロジェクトはこちらです。 以上です。
【Android】MaterialShowcaseViewライブラリでチュートリアルを実装する
MyCurationの1.2.1でチュートリアルを追加しました。というのもインストールされてもすぐアンインストールされているようで、説明が足りなかったかな・・・と思い始めたためです。 実装を自分でやってもよかったのですが、デザイン的なセンスがないのと、ある程度テンプレート化されたものがあるのではということでライブラリを探しました。 今回はMaterialShowcaseViewというライブラリを使ってチュートリアルを実装してみました。
設定
GitHubのページ通りにbuild.gradleを編集
プロジェクトのbuild.gradle
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
アプリのbuild.gradle
dependencies {
compile 'com.github.deano2390:MaterialShowcaseView:1.1.0@aar'
}
使い方
MaterialShowcaseViewでは特定のViewにハイライトを当ててチュートリアルを行います。 実装できるパターンとしては、(1)単純に追加ボタンなど1つだけを説明するパターンと、(2)ボタン1を押して次にボタン2を押して・・・といったように連続して説明をするパターンがあります。
MyCurationでは最初のRSS購読のチュートリアルを入れました。 RSS購読の流れとしては
- メイン画面からToolbarの「+」ボタンを押し、検索画面に行く(メイン画面)
- RSSを購読するサイト検索する(検索画面)
- サイトを開いたら追加ボタンを押す(検索画面)
です。
まずメイン画面で実装した単純に追加ボタンなど1つだけを説明するパターンです。
@Override public boolean onCreateOptionsMenu(Menu menu) { // Menu settings... // Start tutorial at first time new Handler().post(new Runnable() { @Override public void run() { View view = findViewById(R.id.add); new MaterialShowcaseView.Builder(TopActivity.this) .setTarget(view) .setContentText(R.string.tutorial_go_to_search_rss_description) .setDismissText(R.string.tutorial_next) .singleUse(SHOWCASE_ID) .setListener(new IShowcaseListener() { @Override public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { } @Override public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { goToFeedSearch(); } }) .show(); } }); return true; }
Builderパターンを使ってMaterialShowcaseView.Builderを作成していきます。今回はToolbar上の「+」ボタン(R.id.add)にハイライトを当てたかったのでonCreateOptionsMenu()の中でチュートリアルを作成します。 まずsetTarget()でハイライトするViewを指定します。次にチュートリアルの説明文と、チュートリアルを消す部分のテキストを設定します。singleUse()でIDを指定することで初回のみチュートリアルを表示し、同じIDのチュートリアルは今後表示しないように設定できます。今回はチュートリアルが閉じたときに次の画面(検索画面)に移りたかったので、setListener()でIShowcaseListenerをセットし、チュートリアルが閉じられたときに呼ばれるonShowcaseDismissed()で次の画面に飛んでいます。設定が終わったら最後にshow()でチュートリアルを表示します。
使ったメソッドのまとめです。
メソッド | 説明 |
---|---|
setTarget(View view) | ハイライトするViewを指定 |
setContentText(int resource) | 説明文を指定 |
setDismissText(int resource) | タップして閉じる部分の文を指定 |
singleUse(String showCaseId) | 1度だけの表示を管理するためのID。これを設定するとアンインストールしない限りは同じIDのチュートリアルは表示されない |
setListener(IShowcaseListener listener) | リスナの設定。表示時、消えた時のハンドリング |
show() | 表示する |
次に検索画面で実装した連続して説明をするパターンです。
@Override public boolean onCreateOptionsMenu(Menu menu) { // Menu settings... // Start tutorial at first time new Handler().post(new Runnable() { @Override public void run() { View view = findViewById(R.id.search_rss); ShowcaseConfig config = new ShowcaseConfig(); config.setDelay(500); // half second between each showcase view MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(FeedSearchActivity.this, SHOWCASE_ID); sequence.setConfig(config); // Search tutorial sequence.addSequenceItem( new MaterialShowcaseView.Builder(FeedSearchActivity.this) .setTarget(view) .setContentText(R.string.tutorial_search_rss_description) .setDismissText(R.string.tutorial_next) .build() ); // Add button tutorial sequence.addSequenceItem( new MaterialShowcaseView.Builder(FeedSearchActivity.this) .setTarget(fab) .setContentText(R.string.tutorial_add_rss_description) .setDismissText(R.string.tutorial_close) .setDismissOnTouch(true) .build() ); // Open software keyboard if tutorial already finished if (sequence.hasFired()) { searchView.setIconified(false); } sequence.start(); } }); return true; }
今度はMaterialShowcaseSequenceというクラスのインスタンスを作り、MaterialShowcaseViewのインスタンスを追加していってチュートリアルの流れを作ります。
さきほどと同様にBuilderパターンでMaterialShowcaseView.Builderを作っていき、今回は最後にbuild()を呼ぶことでMaterialShowcaseViewを作り、MaterialShowcaseSequenceに追加します。これを繰り返すことでチュートリアルを表示→閉じる→チュートリアルを表示→閉じる→・・・と流れを作れます。作り終わったらstart()でチュートリアルを始めます。
注意した点
Toolbar上のViewがNullになる
onCreateOptionsMenu()の中でViewをfindViewById()で取るとNullが返ってきて落ちるという現象がありました。Handler#post()を使うことで遅延を発生させてこれを回避しました。
参考
How To Get Action View Of Menu Item?
チュートリアル中にキーボードが開かないようにする
検索画面では開いたときにすぐキーボードが開くようにsearchView.setIconified(false)を呼んでいたのですが、チュートリアル中は邪魔です。MaterialShowcaseSequence#hasFired()でチュートリアルが既に終わったかを判定できるので、チュートリアルでないときのみキーボードを開くようにしました。
// Open software keyboard if tutorial already finished if (sequence.hasFired()) { searchView.setIconified(false); }
終わりに
MaterialShowcaseViewを使うことでかなり簡単にチュートリアルを作成しました。さくっと作れるので、凝ったチュートリアルが必要でなければこれで十分かなと思います。以上です。
2週間毎日GitHubにコードを上げ続けて思ったこと
最近私にしては珍しく2週間毎日GitHubにコードを上げ続けています。
(2週間ごときで何を言ってるだと思う方もいらっしゃるとは思いますが・・・)
なんとなくで始めたことですが、自分にとっては結構よかったです。
よかった点としては
- 定期的なアウトプットが行える
- 毎日何かしらのコードを上げる必要があるので、機能の粒度を下げることを考える
- 機能の粒度を下げるにはどうすればいいかという設計を考えるようになる
- 今日は何を作ろうかと考えるようになる
- 毎日勉強が行える
- 隙間時間を見つけてコードを書くようになる
- 草が生えるのがなんか嬉しい
などです。
毎日コードを上げるといっても、毎日何十コミットも上げる訳ではなく、疲れた日や時間のない日は.gitignoreを更新するだけにしたり、テストを書いて終わりにしたりと、自分の生活には影響ない範囲でできているのかなと思います。
一方でアウトプットを増やすとインプットが減るという話もありますので、コードを書きつつインプットの時間も取れるようにしていければいいのかなと思います。
(もちろん自分の自由な時間を削ることなく・・・)
【Android】App Standbyがわからない
App StatndbyはAndroid 6.0になって追加された省電力の機能である。
Doze と App Standby 用に最適化する | Android Developers
アプリが以下の場合でないとき、そのアプリは"Idle"状態になり、"Idle"状態が長時間続くとバックグラウンドでの通信を1日1回しか行うことができなくなる。
- ユーザがアプリを明示的に起動する
- アプリのプロセスがフォアグラウンドにある (アクティビティまたはサービスがフォアグラウンドにある、または他のアクティビティやフォアグラウンドサービスからアプリが使わている時)
- アプリが生成した通知をユーザがロック画面または通知領域で見る
また、端末が充電中の時は"Idle"状態から解放される。
以上である。
全然わからない。
例えば以下のようなことである。
- “Idle"状態にはどの程度の時間でなるのか
- 常駐Serivceが動いている場合は"Idle"状態に移らないのか
- “Idle"状態が何時間続けばバックグラウンド通信を1日1回しかできなくなるのか
- 1日1回とは1リクエストのことなのか
- 1日でバックグラウンド通信規制→解放→バックグラウンド通信規制となった場合、2回目の規制は1回目から通信が規制されるのか
この辺りがわからないせいでどうテストを行えばいいのかがさっぱりわからない。 特に気になるのはバックグラウンド通信の規制に入る方法である。 一応GoogleにはApp Standbyをシュミレートする方法が載っているが、肝心のバックグラウンド通信規制にする方法は載っていない。
$ adb shell dumpsys battery unplug $ adb shell am set-inactive <packageName> true
テスト終了後に戻す。
$ adb shell am set-inactive <packageName> false $ adb shell am get-inactive <packageName> ← falseならOK
アプリが"Idle"状態かどうかはPowerManager#isDeviceIdleMode()で取得できるので、実機にて1時間ごとにAlarmでこの状態をログに出力し、2~3時間放置して様子を見てみたが、常にfalseだった(充電できないし、AlarmがDozeモードで規制されないようにしないといけないのでクソ面倒)。
PowerManager | Android Developers
その他海外含め色々調査してみたが、上記のApp Standbyの条件とGoogleが出しているシュミレート方法を載せている記事がほとんどで、App Standbyの詳細がわかる記事は見つからなかった(もっと探せばあるのかもしれない)。
この辺りをはっきりさせるのなら自分で全部調査するしかないのかな・・・ ただそこまでやる必要は無い気はする。
皆様はどう対応しているんでしょう。。。
【Android】 リモートリポジトリに繋がらない環境でJUnitを実行する
ネットワーク環境に問題がないときは、Googleのページのやり方で設定すればJUnitの環境設定は簡単だけど、Proxyなどでmaven centralに繋がらない場合の対処法
jarファイルのダウンロード
リモートリポジトリに繋がらないので、jarファイルをlibsフォルダに置いてコンパイルする
- maven centralのページからjunit-4.12.jarをダウンロード
- JUnitのPOMのdependencyを見るとhamcrest-coreへの依存があるのでこちらもダウンロード
<dependencies> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> <version>1.3</version> </dependency> </dependencies>
- ダウンロードしたjarファイルをlibsフォルダにコピー
build.gradle
compile fileTree(dir: 'libs', include: '*.jar', excludes: ['junit-4.12.jar', 'hamcrest-core-1.3.jar']) testCompile files('libs/junit-4.12.jar') testCompile files('libs/hamcrest-core-1.3.jar')
テストを書く
【Android】自分でキュレーションを作るフィードリーダー「MyCuration」を公開しました
MyCuration -キュレーションを作るフィードリーダー - Google Play の Android アプリ
だらだらと自分用に作ってたアプリがある程度まとまったので公開しました。
Android4.0以上対応です。
背景
開発の背景としては以下のような背景がありました。
要はfeedlyもキュレーションアプリも使いたくないけど、既存のでいいのがなかったから作ったって感じです。
機能
- RSSリーダー機能(記事の取得、未読管理など)
- キュレーション機能(特定の単語が入っている記事をまとめる)
- フィルター機能(特定の単語またはURLが入った記事を既読にする)
キュレーションの条件とフィルターの条件は今後増やしていきたいところ。考えてるのは、時間とかはてなブックマーク数、つぶやき数などなど
使っているライブラリ
- Volley (通信周り)
- jsoup (RSSパース周り)
- Android-PullToRefresh (引っ張って更新)
- FloatingActionButton (記事画面の記事送りボタン)
- Android-ProgressFragment (プログレスダイアログ)
Android-PullToRefreshはもうメンテナンスしていなくて、デフォルトでも提供していたと思うので、時間ができたらそちらに乗り換えたい。
開発環境
- Android Studio 1.4
- JUnit 3(4に乗り換えたいところ・・・)
- Git
- Mac OSX 10.10.5
テストはJUnitの単体テストで実機テストしかやってないけどできればUIAutomatorを使った自動UIテストとかも徐々にやっていきたい。
レポジトリ
終わりに
とりあえずリリースできたので、機能追加を徐々にやっていこうと思う
【Android】PreferenceActivity/PreferenceFragmentを使った設定画面
(追記) 2018/11/15 書き直しました。
Androidの設定画面について
- AndroidではAPI1から設定画面を生成してくれるPreferenceActivityが用意されている
- Android 3.0で各設定画面をFragmentに分けるためのPreferenceFragmentが追加された
- Android 4.0ではPreferenceActivityが拡張され、PreferenceFragmentと組み合わせることでハンドセットでは1画面、タブレットでは2画面の設定画面を自動的に作成できる
設定項目
クラス | 説明 |
---|---|
CheckBoxPreference | オン/オフの設定項目 |
SwitchPreference | オン/オフの設定項目 API14から |
ListPreference | リストの中から1つ選ぶ設定項目 |
MultiSelectListPreference | リストの中から複数を選ぶ設定項目 |
EditTextPreference | 入力からの設定 |
1画面の設定画面
- 設定画面を2ページに分けない場合は、画面のルートに当たるandroid.R.id.contentに直接PreferenceFragmentを設定する
PreferenceActivity
public class SettingActivity extends PreferenceActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingFragment()) .commit(); } }
PreferenceFragment
- addPreferencesFromResource()で設定画面を定義したXMLを読み込む
- 必要であれば、onActivityCreated()で各設定項目の初期値等を設定する
- XMLで設定したPreferenceを取得するにはandroid:keyを使い、PreferenceFragment#findPreference()で行う
public class SettingFragment extends PreferenceFragment { public SettingFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.setting_fragment); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initView(); } private void initView() { ListPreference listPreference = (ListPreference)findPreference(getString(R.string.key_list_preference)); listPreference.setValueIndex(0); } }
PreferenceFragmentから読み込む設定XML
- PreferenceFragmentから読み込むXMLのトップはPreferenceScreenでなければならない
- 各Preferenceにはandroid:keyを設定しなければならない。これが各Preferenceを特定するIDとなる。
- カテゴリを設定する場合はPreferenceCategoryを使う。android:keyは必須ではない。
- ListPreferenceの場合、選択候補に表示されるリスト(android:entries)と実際の値のリスト(android:entryValues)を設定する必要がある
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <PreferenceCategory android:title="Category Title" > <ListPreference android:title="Title" android:key="@string/key_list_preference" android:entries="@array/entry_sample" android:entryValues="@array/entry_sample_values"/> </PreferenceCategory> </PreferenceScreen>
その他の属性は以下を参照
http://developer.android.com/reference/android/preference/Preference.html
array.xml
ListPreferenceで使う値を定義する
<resources> <array name="entry_sample"> <item>@string/entry1</item> <item>@string/entry2</item> </array> <string-array name="entry_sample_values"> <item>1</item> <item>0</item> </string-array> </resources>
string.xml
<resources> <string name="entry1">Entry1</string> <string name="entry2">Entry2</string> <string name="key_list_preference">KeyListPreference</string> </resources>
初期値設定
XMLで初期値を設定する場合はandroid:defaultValueに設定する。SharedPreferenceから保存した値を読み込んでそれに応じて初期値を設定する場合など、Javaコードの中で初期値を設定を行う場合はPreferenceによって設定方法が異なる
SwitchPreference
setChecked()で行う
SwitchPreference switchPref = (SwitchPreference)findPreference(getString(R.string.key_internal_browser));
switchPref.setChecked(true);
ListPreference
- ListPreferenceのentryValuesのstring-arrayの各itemが数字だとsetDefaultValue()が動かなかった
- setValueIndex()を使うことで解決
- entryValuesをinterger-arrayにするとNullPointerExceptionで落ちてしまう
参考
- http://developer.android.com/reference/android/preference/PreferenceActivity.html
- http://developer.android.com/reference/android/preference/PreferenceFragment.html
- http://developer.android.com/guide/topics/ui/settings.html
- http://stackoverflow.com/questions/5197228/how-to-set-the-default-value-of-a-listpreference
- http://stachibana.biz/?p=610