u-ryo's blog

various information for coding...

Category: Android

Schedulers.newThread() or Schedulers.io()

| Comments

Robolectricでtestを書いていて、 どうも上手く行かないところがありました。 「上手く行かない」というのは「Test class内で(RxJava1系なので)RxJavaHooks.setOnIOScheduler(s -> Schedulers.immediate());しているにも関わらず、 test対象classでsubscriberが動いてしまう」というものです。

どうしてかなぁ、.subscribe(...)の中で.subscribe(...)してるからかなぁ、 RxJava のテスト(1): TestSubscriber, test(), TestSchedulerを見て、 じゃぁっていうんでTestScheduler scheduler = Schedulers.test();して何度かscheduler.triggerActions();しても現象変わらずでした。

悩んだ末、わかったのは、 test対象classでは.subscribeOn(Schedulers.newThread())していた、 ということでした。 なぁんだ。

そういえばちょっと前に書いたところだったので、 まだSchedulers.newThread()にしちゃってたんですね。 今回Schedulers.io()に変えました。 Androidでも効くんでしょうか。

Android Petite Tips

| Comments

お仕事で極悪Androidアプリを改修していて、 今日得た知見をば。

getTextSize/setTextSize

あるActivityの画面で、 本文とボタンのtext sizeを揃えようとして、 TextView#getTextSizeしてからsetTextSizeしたら、 大きくなるんですよね。何でだろう、調べると、 TextView#getTextSizesetTextSizeのデフォルト単位が違う のだそう。びっくりです。

1
renewalButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, caution.getTextSize());

というように、単位を明示する必要があるそう。

サイズ自動調整TextView

TextViewで、指示通りに改行位置を固定しようと思って。 指示中の、禁則処理に失敗しているところも含めて忠実に再現しようと。 そのためにはtext sizeを随意にせねばならず。 【Android】横幅に合わせてテキストサイズを調整するTextViewそのままで上手く行きました。 あーでもonLayout()の最初の引数changedtrueの時だけ resize()すればよかとです。

今はAutosizing TextViewsというのがあるそう。 ただ、API 26からなのでまだなかなか使えないでしょうか。

onLayout後の値の取得

上記のようにtext sizeを変えてから、 その結果のtext sizeに合わせて他のViewのtext sizeを 決定しようとすると、 onLayout()が呼ばれ終わってからでないと 目的の値が取得出来ないんですね。 そこで、 How to know when an activity finishes a layout pass? にあるように、 myView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {...}); とすれば良いです。

補足

RobolectricでUnit Test書いてたら、 このclass、testが終わらないんです。 何でかなー、とbreakpointで追ってみると、 延々とonGlobalLayout()が呼ばれ続けてるんですね。 えーっと思って。 RxJavaHooks.setOnIOScheduler(s -> Schedulers.immediate()); しても、 ShadowApplication.runBackgroundTasks(); しても効き目はなく。 androidでheightやwidthが0になって取得できない時 を見ると、用が済んだらすぐremoveするんですね。そっか。 というわけで、Android SDKのversionによって分けて、 removeOnGlobalLayoutListener(this)removeGlobalOnLayoutListener(this)でremoveするように したんですけど、今度はthisが効かない。 なるほど、lambdaだとthisは外側のclass instanceになるんですね。 じゃぁっていうんでlambda自体をListener instanceとして名前付けて、 lambdaの中でthisじゃなくてその名前で参照しようとしたんですが、 might not have been initializedとか言われ、 nullで初期化すると今度はeffectively finalじゃないと言われて、 うー、とか思って仕方なく諦めて、 初めてlambdaを解いてinner classの記述に戻しました。

Rx Androidにmaxはない?

探したんですけど見つからなかったので自分で集計しました。

1
2
3
4
5
6
float textWidth = rx.Observable
        .from(getText().toString().split("\n"))
        .map(paint::measureText)
        .reduce(Math::max)
        .toBlocking()
        .single();

TextViewで白枠

ある段落を白枠で囲って欲しいと言われました。 調べると、[android]xmlで枠を指定するというのがあり、 それ用のdrawable XMLを作ってやってandroid:background="@drawable/..."で、 それを指定すれば、望み通りのものが得られました。 背景色は、これも書いてありますが#00ffffffで透明になります。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- background -->
    <solid android:color="#00ffffff" />
    <!-- rounded corners -->
    <stroke android:width="1dp"
        android:color="@color/white" />
    <corners android:radius="1dp" />
</shape>

Robolectricで次のActivityへの遷移の確認

shadowOf(activity).peekNextStartedActivity()Intentを取得、 getComponent().getClassName()が目的のclass nameかをassert。

1
2
3
Intent intent = shadowOf(activity).peekNextStartedActivity();
assertThat(Objects.requireNonNull(intent.getComponent()).getClassName(),
        is(MainActivity.class.getName()));

Local PushでNotification

Local PushでNotificationをして欲しい、と言われました。 調べてみると、要するに、 AlarmManagerPendingIntentをsetして、 それがset時の引数のUnix Time(millisec)になると、 これもset時引数のBroadcastReceiverの子classの onReceive(context, intent)が呼ばれるので、 そこでNotificationManager.notify()をする、と。

AndroidのNotificationについては、 sample applicationを作って色々と試してみました。

  1. uninstall/端末再起動すれば登録済みのalarmは解除される
  2. 多重登録してもPendingIntent.FLAG_UPDATE_CURRENTなら最後のNotificationに上書きされる
  3. 過去の時日のalarmを登録するとすぐNotifyされてしまう
  4. 機種によっては挙動が違う(Huaweiでは、アプリが起動していない時/Sleep時にAlarmを発動させるには「保護されたアプリ」でないとならない、等)
  5. 長いtextは全文出ないで端折られる。出したいなら、.setStyle(new NotificationCompat.BigTextStyle().bigText("..."))する。但し.setBigContentTitle(intent.getStringExtra("..."))も同時に加えるとダメっぽい。

How to Get the Result From DialogFragment

| Comments

DialogFragmentをnewしてdialogを表示させ、 そこでのbutton tapによって、元のActivity上で処理をさせたい時、 どうやってfeedbackしたら良いのかなと。 呼び出し元はFragmentではなくActivityなので、 Fragmentで呼び出し元に結果を伝えるReceive result from DialogFragment等にあるようにsetTargetFragment()を使えないんですよね。

結局、DialogFragment側にsetCallback(Callback callback)と Functional InterfaceとしてCallbackを定義して、 button押したらcallback.call();とし、 呼び出し元のActivity側でnew SomeDialogFragment().setCallback(() -> someMethod())としてやりたいことをsomeMethod()に込めました。 ちょっと面倒ですけどこのようにcallback駆使するしか無いのかなぁと。 onActivityResult()getTargetFragment()が使えないのと 処理をActivity側に書きたいというのがあったので。 いや、onActivityResult()の中身はActivity側ですか。 setTargetFragment()の代わりに何かActivityのreferenceを DialogFragment側に持たせればよかった?のかな? いやいや、そもそもFragmentからgetActivity()で取得できる? からこんなことしなくてよかった? あれ?? いやーでもDialogFragment側は引数の情報を持っておらず、 Activity側しか引数持ってないんですよね。 今回のぼくの場合では、 引数を引き回すか、再度SQLで取得するかしてonActivityResult()でkickするか、 callbackを作るか、ということだったでしょうか。

Caution on ListView

| Comments

今時ListViewなんてあんまり使わないと思いますが、 ListViewのviewの使い回しでbugがあったのでメモです。 ぼくがやったんじゃないです。

public View getView(int position, View convertView, ViewGroup parent)convertViewを使い回すわけですが、 nullの時とnot nullの時の扱いが微妙に違うんですね。 具体的には、 nullの時だけsetOnItemSelectedListenerを設定して、 そのOnItemSelectedListenerが表示される初回だけ onItemSelectedが発動することを利用してobjectの 設定の一部をしているから、 not nullの時にはその経路を通らず、 objectの一部がnullのままで次に進むとNullPointerExceptionに なるという。 だからこのbugの発現条件は、 「初回表示時のリストの数が画面を2つ以上超える」時、 というわかりにくい、後からでは見つけにくいものになっています。 OnItemSelectedListenerで設定しているのも悪いし、 convertViewnullの時とnullでない時の処理に きちんと心を砕かなかったから、 こういうことになるんですね。

まーでもこんなlevelのbugはまだいぃ方ですけどねー このsoftwareについては。 色々アホなのはホントやる気無くします。

参考: Android GridViewのパフォーマンスを上げよう(1/2)←そうですよね普通気を付けますよね、ListView は Graphical Layout で作ったまま使ってはいけない←そうなんですよ最初、頭の1つだけnullで呼ばれた後画面に見える分だけnot nullで呼ばれ、その後改めてnullで呼ばれるので、何でかなーと思ったものです。ただもぅ今はRecyclerViewだからこんなtipsはもう不要かなーと。

NullPointerException on Retrofit2 With Robolectric

| Comments

Android ApplicationをRobolectricでtestしていて、 どうにも困ったのでメモです。

状況は、Android Applicationで、 Robolectricを使っていて、 Retrofit2でPOSTしにいく部分(受け手はMockWebServer)のunit testで、 突然NullPointerExceptionになってsubscribeerrorに入ってしまう、というもの。 breakpointで追っていってもcall()で突如NPEに入ってしまって、 具体的にどこでNPEに陥っているのかよく分かりませんでした。 Googleで探してみると、 Stack Overflowにそれらしき投稿があり、 .observeOn(AndroidSchedulers.mainThread())LooperSchedulerなのでここでNPEになる、 だからRxAndroidPluginsregisterSchedulersHook()Schedulers.immediate()してやると良い、 と書いてあって、やったー! と思ったものの、効果なく。

結局そうではなくて、 MockWebServer使っているからhttp://localhost:NNNN/...に requestを改装しているせいなんですけど、 SecurityPolicy絡みのExceptionが裏で出ているようで、 以下のようなShadowを用意して@Config({shadows=...})に 書いてやればそのままですんなり行きました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import android.security.NetworkSecurityPolicy;

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

@Implements(NetworkSecurityPolicy.class)
public class ShadowNetworkSecurityPolicy {
    @Implementation
    public static NetworkSecurityPolicy getInstance() {
        try {
            Class<?> shadow = ShadowNetworkSecurityPolicy.class.forName("android.security.NetworkSecurityPolicy");
            return (NetworkSecurityPolicy) shadow.newInstance();
        } catch (Exception e) {
            throw new AssertionError();
        }
    }

    @Implementation
    public boolean isCleartextTrafficPermitted(String hostname) {
        return true;
    }
}

勿論、.subscribeOn(Schedulers.io())に対しては RxJavaHooks.setOnIOScheduler(s -> Schedulers.immediate()); した上で、です。