Android Studio のエミュレータ (Android Emulator / AVD) からホスト名でホストPC に接続したい

TL;DR

  • AVD (Android Virtual Device) の localhost (127.0.0.1) は自分自身を指す
  • 10.0.2.2 でホストにアクセスができる
  • 今回検討した方法は2つ
    • AVD からのアクセスをホストで起動した proxy 経由で処理する
      • squid を立てて lvh.me などのループバックドメインでアクセスする
    • AVD 内の hosts を書き換える
      • adb で pull & push

経緯

10.0.2.2 でホストにアクセスできるが、ホスト名でアクセスしたい時がある (サブドメインで区切られたマルチテナントなシステムとか) ので、試行錯誤してみた

シミュレータ (AVD) からのアクセスをホストで起動した proxy で処理するパターン

Squid インストール

macOS 前提です。

$ brew install squid

動作確認したのは squid-3.5.27

設定ファイル編集

必要であればよく使うポートを開けておくとよい (acl Safe_ports port 1025-65535» # unregistered portsは設定されている)

$ vi /usr/local/etc/squid.conf
# 例
acl Safe_ports port 4200
acl Safe_ports port 8000
acl Safe_ports port 8080
acl Safe_ports port 9292

起動・停止・再起動

$ brew services run squid
$ brew services stop squid
$ brew services restart squid

ログ確認

$ tail -f /usr/local/var/logs/access.log # squid access_log

AVD からのローカルホストアクセス

コマンドラインから adb でエミュレータ起動する際に引数で渡したり、AVD の GUI 設定からプロキシを設定しても 127.0.0.1 が AVD 自身にループバックされているようで期待した動作にならなかった。

参考: Set up Android Emulator networking  |  Android Developers

通常の AndroidWi-Fi 設定でプロキシを手動設定する*1

参考: Connect to Wi-Fi networks on your Android device - Android Help

プロキシのホストは Squid が起動している開発サーバ の Local IP、Squid のデフォルトポートは 3128

動作確認

lvh.me が 127.0.0.1 を返してくれるので、それにアクセスすると開発サーバの localhost にアクセスしてくれる。

(AVD 上の) http://lvh.me:8080 => (開発サーバの) http://127.0.0.1:8080

シミュレータ (AVD) 内の hosts を書き換えるパターン

事前に emulatoradb コマンドにパスを通しておいたほうが楽だが、今回は省略するのでフルパスで記載。

バーチャルデバイス (シミュレータ) 起動

$ # avd 名を確認
$ /Users/${HOME}/Library/Android/sdk/emulator/emulator -list-avds
Pixel_2_API_28
$ # avd を指定してエミュレータを起動 (書き換えるので、writable を指定する)
$ /Users/${HOME}/Library/Android/sdk/emulator/emulator -avd Pixel_2_API_28 -writable-system

Android Debug Bridge でバーチャルデバイスを操作

$ # デバイスの確認
$ /Users/${HOME}/Library/Android/sdk/platform-tools/adb devices
List of devices attached
emulator-5554   device
$
$ # root になる
$ /Users/${HOME}/Library/Android/sdk/platform-tools/adb root
$
$ # デバイスをアタッチ
$ /Users/${HOME}/Library/Android/sdk/platform-tools/adb remount
$
$ # アタッチの確認
$ /Users/${HOME}/Library/Android/sdk/platform-tools/adb devices
List of devices attached
emulator-5554   device

バーチャルデバイス上の hosts を書き換える

$ # バーチャルデバイスから hosts ファイルを取得
$ ~/library/android/sdk/platform-tools/adb -s emulator-5554 pull /system/etc/hosts ~/tmp/
/system/etc/hosts: 1 file pulled. 0.0 MB/s (56 bytes in 0.004s)
$
$ # 追記 (10.0.2.2 はホストのアドレス)
$ echo '10.0.2.2        lvh.me' >> ~/tmp/hosts
$
$ # 追記したファイルをバーチャルデバイスに上書き配置
$ ~/library/android/sdk/platform-tools/adb -s emulator-5554 pull ~/tmp/hosts /system/etc/hosts 

参考

*1:端末により設定方法は変わります

RxJS の BehaviorSubject を share しようとして上手く行かなかったやつと上手く行ったやつ

以前はまったやつの備忘録 たぶん Angular 5.0.x, RxJS 5.5.x くらい

BehaviorSubject で Shared Service を作った時に、フィールドの Observable を直接参照するのではなく、メソッドを介して Observable を返す処理があった1

以下のように share した subject を分けると1度しか subscribe できない

import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { share, tap } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <li>counter: {{counter}}
      <li>direct: {{direct|async}}
      <li>return: {{returnValue|async}}
      <li>direct2: {{direct|async}}
      <li>return2: {{returnValue|async}}
    </ul>
    `,
})
export class AppComponent  implements OnInit {
  direct: Observable<string>;
  returnValue: Observable<string>;

  message$: Observable<string>;
  subject: BehaviorSubject<string> = new BehaviorSubject<string>('initial');

  counter: number = 0;

  ngOnInit() {
    this.message$ = this.subject.asObservable().pipe(tap(() => this.counter++), share());
    this.direct = this.message$;

    this.returnValue = this.getObservable();
    this.subject.next('Hello!');
  }

  getObservable(): Observable<string> {
    return this.message$;
  }
}

share しないと複数回呼べる

stackblitz.com

Observable の Cold/Hot についてまだちゃんと理解できてないと感じたので、改めて調べよう


  1. 一度投げたエンドポイントには2回目は投げず前回取得したやつを使う、エンドポイントは不定、みたいなキャッシュ機構を作っていた

Play Framework 2.5.x (java) で Swagger 1.5.x を使う時に Ebean が _ebean_intercept を各モデルにつけてしまう問題の対処

Play Scala 2.2 + swagger 1.3.3 とかだと以下で対処できるようですが、タイトルの組み合わせではこのやり方は使えなかったので、試行錯誤の結果を残しておきます。

java - Swagger is showing _ebean_intercept with everymodel in my play application - Stack Overflow

import com.wordnik.swagger.converter.SwaggerSchemaConverter

class IgnoreConverter extends SwaggerSchemaConverter{
  override def skippedClasses: Set[String] = Set("com.avaje.ebean.bean.EntityBeanIntercept")
  override def ignoredClasses: Set[String] = Set("com.avaje.ebean.bean.EntityBeanIntercept")
  override def ignoredPackages: Set[String] = Set("com.avaje.ebean")
}
public class Global  extends GlobalSettings {
    @Override
    public void beforeStart(Application app) {
        Logger.info("Registering custom converter");
        ModelConverters.addConverter(new IgnoreConverter(), true);
    }
}

変更点

Play 2.5

GlobalSettings - 2.5.x - Play Framework

GlobalSettings が非推奨になりました。各クラスのコンストラクタで設定しましょう。

Swagger 1.5

Overriding Models · swagger-api/swagger-core Wiki

公式ドキュメントは現時点 (2017-04-10) で 1.3 系のままなので、テストクラスを参考にしてくれとのことみたいです。

How do you override models with swagger-core 1.5.3? · Issue #1499 · swagger-api/swagger-core

実装

public class IgnoreConverter implements ModelConverter {
  @Override
  public Property resolveProperty(Type type, ModelConverterContext context, Annotation[] annotations, Iterator<ModelConverter> chain) {
    JavaType _type = Json.mapper().constructType(type);
    if (_type != null) {
      Class<?> cls = _type.getRawClass();
      // Ebean class (_ebean_intercept)
      if (EntityBeanIntercept.class.isAssignableFrom(cls)) {
        return null;
      }
    }
    if (chain.hasNext()) {
      return chain.next().resolveProperty(type, context, annotations, chain);
    } else {
      return null;
    }
  }

  @Override
  public Model resolve(Type type, ModelConverterContext context, Iterator<ModelConverter> chain) {
    if (chain.hasNext()) {
      return chain.next().resolve(type, context, chain);
    } else {
      return null;
    }
  }
}
@Api
public class HomeController extends Controller {

  @Inject
  public HomeController(MessagesApi messagesApi) {
    ModelConverters.getInstance().addConverter(new IgnoreConverter());
  }

  public Result index() {
    return ok("Hello, World!");
  }
}