[webRTC for android vol2] 繋げてみる

実際につなげてみる。
Android - iOS
(※Androidを1つしかもってないので相手はIosです)

WebRTCのビルド〜プロジェクトへの追加は [前回 webRTC for android vol1](http://qiita.com/nakadoribooks/items/7950e29ad3b751ddab12)

TrickleICE は次回 [webRTC for android vol3](http://qiita.com/nakadoribooks/items/d30fdc5585657dcd8420)

Ios版とwampサーバーを作る記事は [webRTC for ios vol2](http://qiita.com/nakadoribooks/items/9f1b1cbe32f2fc6dec70)

#成果物
https://github.com/nakadoribooks/webrtc-android/releases/tag/v0.0.2

<img src="https://j.gifs.com/wjNqB8.gif" />

#参考
[AppRTCDemo](https://github.com/njovy/AppRTCDemo)

# 設定

wampのライブラリは [jawampa](https://github.com/Matthias247/jawampa)

## Gradle

```gradle:build.gradle(Project-android)
buildscript {
    repositories {
        jcenter()
        maven {
            url 'https://raw.github.com/Matthias247/jawampa/mvn-repo/'
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
    }
}

allprojects {
    repositories {
        jcenter()
        maven {
            url 'https://raw.github.com/Matthias247/jawampa/mvn-repo/'
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

```gradle:build.gradle(Module-app)
apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.nakadoribooks.webrtcexample"
        minSdkVersion 21
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        exclude 'META-INF/io.netty.versions.properties'
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile(name:'libwebrtc', ext:'aar')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile 'ws.wamp.jawampa:jawampa:0.2.0'
    compile 'io.reactivex:rxandroid:0.24.0'
}

repositories{
    flatDir{
        dirs 'libs'
    }
}
```

## AndroidManifest.xml

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.nakadoribooks.webrtcexample">

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

## Layout

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.nakadoribooks.webrtcexample.MainActivity">

    <android.support.v7.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <RelativeLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="12">

            <FrameLayout
                android:id="@+id/local_video_layout"
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:layout_marginTop="300dp"
                android:layout_marginLeft="20dp"
                android:background="@color/colorAccent">
                <org.webrtc.SurfaceViewRenderer
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:id="@+id/local_render_view"
                    />
            </FrameLayout>

            <FrameLayout
                android:id="@+id/remote_video_layout"
                android:layout_width="match_parent"
                android:layout_height="350dp"
                android:background="@color/colorPrimary">
                <org.webrtc.SurfaceViewRenderer
                    android:id="@+id/remote_render_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
            </FrameLayout>

        </RelativeLayout>

        <android.support.v7.widget.AppCompatTextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:text=""
            android:layout_weight="1"
            android:textAlignment="center"
            android:textSize="20dp"
            android:id="@+id/status_label"/>
        <android.support.v7.widget.AppCompatButton
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:text="Connect"
            android:id="@+id/control_button"
            android:textSize="20dp"
            android:layout_weight="2"/>
    </android.support.v7.widget.LinearLayoutCompat>

</android.support.constraint.ConstraintLayout>
```

# 実装抜粋

## WebRTC

```java:WebRTC.java
// interface -----------------
    public void connect(WebRTCCallbacks callbacks)
    public void startCapture()
    public void createOffer()
    public void receiveOffer(String sdp)
    public void receiveAnswer(String sdp)

// callbaks -----------------

public static interface WebRTCCallbacks{
    void onCreateLocalSdp(String sdp);
    void didReceiveRemoteStream();
}

```

## Wamp

```java:Wamp.java
// interface -------
    public void connect(WampCallbacks callbacks)
    public void publishOffer(String sdp)
    public void publishAnswer(String sdp)

// callbacks --------
 public static interface WampCallbacks{
    void onConnected();
    void onReceiveOffer(String sdp);
    void onReceiveAnswer(String sdp);
}

```

## アプリケーション

```java:MainActivity.java
// state -------
 private enum State{
        Disconnected
        , Connecting
        , Connected
        , Offering
        , ReceivedOffer
        , CreatingAnswer
        , Done
    }

// connect wamp --------
private void connect(){
    wamp.connect(new Wamp.WampCallbacks() {

        @Override
        public void onConnected() {
            changeState(State.Connected);
        }

        @Override
        public void onReceiveOffer(String sdp) {
            if(typeOffer){
                return;
            }
            changeState(State.CreatingAnswer);
            webRTC.receiveOffer(sdp);
        }

        @Override
        public void onReceiveAnswer(String sdp) {
            if(!typeOffer){
                return;
            }

            webRTC.receiveAnswer(sdp);
        }
    });
}

// start webRTC -----
private void startWebRTC(){
    webRTC = new WebRTC(this);
    webRTC.connect(new WebRTC.WebRTCCallbacks() {
        @Override
        public void onCreateLocalSdp(String sdp) {
            if(typeOffer){
                wamp.publishOffer(sdp);
            }else{
                wamp.publishAnswer(sdp);
            }
        }

        @Override
        public void didReceiveRemoteStream() {
            changeState(State.Done);
        }
    });
    webRTC.startCapture();
}
```

# 実装詳細


## WebRTC

```java:WebRTC.java

package com.nakadoribooks.webrtcexample;

import android.app.Activity;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;

import org.webrtc.*;

import java.util.Arrays;
import java.util.List;

/**
 * Created by kawase on 2017/04/13.
 */

public class WebRTC implements PeerConnection.Observer {

    public static interface WebRTCCallbacks{
        void onCreateLocalSdp(String sdp);
        void didReceiveRemoteStream();
    }

    private static abstract class SkeletalSdpObserver implements SdpObserver{

        private static final String TAG = "SkeletalSdpObserver";

        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {}
        @Override
        public void onSetSuccess() {}
        @Override
        public void onCreateFailure(String s) {}
        @Override
        public void onSetFailure(String s) {}
    }

    private static final String TAG = "WebRTC";

    private final Activity activity;
    private WebRTCCallbacks callbacks;
    private PeerConnectionFactory factory;
    private PeerConnection peerConnection;
    private MediaStream localStream;
    private VideoCapturer videoCapturer;
    private EglBase eglBase;

    private VideoTrack localVideoTrack;
    private VideoRenderer localRenderer;
    private VideoRenderer remoteRenderer;

    WebRTC(Activity activity){
        this.activity = activity;
    }

    // interface -----------------

    public void connect(WebRTCCallbacks callbacks){
        this.callbacks = callbacks;
        setupPeerConnection();
        setupLocalStream();

        peerConnection.addStream(localStream);
    }

    public void startCapture(){
        _startCapture();
    }

    public void createOffer(){
        _createOffer();
    }

    public void receiveOffer(String sdp){
        _receiveOffer(sdp);
    }

    public void receiveAnswer(String sdp){
        _receiveAnswer(sdp);
    }

    // implements -------------

    private void setupPeerConnection(){
        // rendereContext
        eglBase = EglBase.create();

        // initialize Factory
        PeerConnectionFactory.initializeAndroidGlobals(activity.getApplicationContext(), true);
        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
        factory = new PeerConnectionFactory(options);
        factory.setVideoHwAccelerationOptions(eglBase.getEglBaseContext(), eglBase.getEglBaseContext());

        // create PeerConnection
        List<PeerConnection.IceServer> iceServers = Arrays.asList(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
        peerConnection = factory.createPeerConnection(iceServers, WebRTCUtil.peerConnectionConstraints(), this);
    }

    private void _receiveAnswer(String sdp){
        SessionDescription remoteDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
        peerConnection.setRemoteDescription(new SkeletalSdpObserver() {
            @Override
            public void onSetSuccess() {

            }
        }, remoteDescription);
    }

    private void _receiveOffer(String sdp){
        // setRemoteDescription
        SessionDescription remoteDescription = new SessionDescription(SessionDescription.Type.OFFER, sdp);
        peerConnection.setRemoteDescription(new SkeletalSdpObserver() {
            @Override
            public void onSetSuccess() {

                // createAnswer
                peerConnection.createAnswer(new SkeletalSdpObserver() {
                    @Override
                    public void onCreateSuccess(SessionDescription sessionDescription) {
                        peerConnection.setLocalDescription(new SkeletalSdpObserver() {}, sessionDescription);
                    }
                }, WebRTCUtil.answerConnectionConstraints());

            }
        }, remoteDescription);
    }

    private void _createOffer(){
        peerConnection.createOffer(new SkeletalSdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                peerConnection.setLocalDescription(new SkeletalSdpObserver() {}, sessionDescription);
            }
        }, WebRTCUtil.offerConnectionConstraints());
    }

    private void setupLocalStream() {

        localStream = factory.createLocalMediaStream("android_local_stream");

        // videoTrack
        videoCapturer = createCameraCapturer(new Camera2Enumerator(activity));
        VideoSource localVideoSource = factory.createVideoSource(videoCapturer);
        localVideoTrack = factory.createVideoTrack("android_local_videotrack", localVideoSource);
        localStream.addTrack(localVideoTrack);

        // render
        localRenderer = setupRenderer(R.id.local_render_view);
        localVideoTrack.addRenderer(localRenderer);

        // audioTrack
        AudioSource audioSource = factory.createAudioSource(WebRTCUtil.mediaStreamConstraints());
        AudioTrack audioTrack = factory.createAudioTrack("android_local_audiotrack", audioSource);
        localStream.addTrack(audioTrack);
    }

    private void _startCapture(){
        DisplayMetrics displayMetrics = new DisplayMetrics();
        WindowManager windowManager =
                (WindowManager) activity.getApplication().getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
        int videoWidth = displayMetrics.widthPixels;
        int videoHeight = displayMetrics.heightPixels;

        videoCapturer.startCapture(videoWidth, videoHeight, 30);
    }

    private VideoRenderer setupRenderer(int viewId){

        SurfaceViewRenderer renderer = (SurfaceViewRenderer) activity.findViewById(viewId);
        renderer.init(eglBase.getEglBaseContext(), null);
        renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        renderer.setZOrderMediaOverlay(true);
        renderer.setEnableHardwareScaler(true);

        return new VideoRenderer(renderer);
    }

    private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
        return createBackCameraCapturer(enumerator);
    }

    private VideoCapturer createBackCameraCapturer(CameraEnumerator enumerator) {
        final String[] deviceNames = enumerator.getDeviceNames();

        for (String deviceName : deviceNames) {
            if (!enumerator.isFrontFacing(deviceName)) {
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);

                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }

        return null;
    }

    // PeerConnection.Observer -----

    @Override
    public void onSignalingChange(PeerConnection.SignalingState signalingState) {}
    @Override
    public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {}
    @Override
    public void onIceConnectionReceivingChange(boolean b) {}
    @Override
    public void onRemoveStream(MediaStream mediaStream) {}
    @Override
    public void onDataChannel(DataChannel dataChannel) {}
    @Override
    public void onRenegotiationNeeded() {}
    @Override
    public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {}
    @Override
    public void onIceCandidate(IceCandidate iceCandidate) {}
    @Override
    public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}

    @Override
    public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
        Log.d(TAG, "onIceGatheringChange");
        if(iceGatheringState == PeerConnection.IceGatheringState.COMPLETE){
            Log.d(TAG, "Complete");
            SessionDescription localSdp = peerConnection.getLocalDescription();
            callbacks.onCreateLocalSdp(localSdp.description);
        }
    }

    @Override
    public void onAddStream(MediaStream mediaStream) {

        if (mediaStream.videoTracks.size() == 0){
            return;
        }

        final VideoTrack remoteVideoTrack = mediaStream.videoTracks.getFirst();
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                remoteRenderer = setupRenderer(R.id.remote_render_view);
                remoteVideoTrack.addRenderer(remoteRenderer);

                callbacks.didReceiveRemoteStream();
            }
        });

    }
}

```


## Wamp

```java:Wamp.java
package com.nakadoribooks.webrtcexample;

import android.app.Activity;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.concurrent.TimeUnit;

import rx.android.app.AppObservable;
import rx.functions.Action0;
import rx.functions.Action1;
import ws.wamp.jawampa.PubSubData;
import ws.wamp.jawampa.WampClient;
import ws.wamp.jawampa.WampClientBuilder;
import ws.wamp.jawampa.WampError;

/**
 * Created by kawase on 2017/04/15.
 */

public class Wamp {

    public static interface WampCallbacks{
        void onConnected();
        void onReceiveOffer(String sdp);
        void onReceiveAnswer(String sdp);
    }

    private static final String TAG = "Wamp";
    private static final String AnswerTopic = "com.nakadoribook.webrtc.answer";
    private static final String OfferTopic = "com.nakadoribook.webrtc.offer";

    private final Activity activity;
    private WampClient wampClient;
    private WampCallbacks callbacks;

    Wamp(Activity activity){
        this.activity = activity;
    }

    // interface -------

    public void connect(WampCallbacks callbacks){
        this.callbacks = callbacks;
        _connect();
    }

    public void publishOffer(String sdp){
        _publishOffer(sdp);
    }

    public void publishAnswer(String sdp){
        _publishAnswer(sdp);
    }

    // implements --------

    private void _connect(){
        WampClientBuilder builder = new WampClientBuilder();

        try {
//            builder.withUri("ws://192.168.1.2:8000")
            builder.withUri("wss://nakadoribooks-webrtc.herokuapp.com")
                    .withRealm("realm1")
                    .withInfiniteReconnects()
                    .withReconnectInterval(3, TimeUnit.SECONDS);
            wampClient = builder.build();
        } catch (WampError e) {
            return;
        }

        AppObservable.bindActivity(activity, wampClient.statusChanged())
                .subscribe(new Action1<WampClient.Status>() {
                    @Override
                    public void call(final WampClient.Status status) {

                        if (status == WampClient.Status.Connected) {

                            callbacks.onConnected();

                            wampClient.makeSubscription(OfferTopic).subscribe(new Action1<PubSubData>(){
                                @Override
                                public void call(PubSubData arg0) {

                                    JsonNode json = arg0.arguments().get(0);
                                    String sdp = json.get("sdp").asText();

                                    callbacks.onReceiveOffer(sdp);
                                }

                            }, new Action1<Throwable>(){
                                @Override
                                public void call(Throwable arg0) {}
                            });

                            wampClient.makeSubscription(AnswerTopic).subscribe(new Action1<PubSubData>(){
                                @Override
                                public void call(PubSubData arg0) {
                                    JsonNode json = arg0.arguments().get(0);
                                    String sdp = json.get("sdp").asText();

                                    callbacks.onReceiveAnswer(sdp);
                                }

                            }, new Action1<Throwable>(){
                                @Override
                                public void call(Throwable arg0) {}
                            });
                        }
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(final Throwable t) {}
                }, new Action0() {
                    @Override
                    public void call() {}
                });

        wampClient.open();
    }


    public void _publishOffer(String sdp){

        final ObjectMapper mapper = new ObjectMapper();
        ObjectNode node = mapper.createObjectNode();
        node.put("type", "offer");
        node.put("sdp", sdp);
        wampClient.publish(OfferTopic, node);
    }

    public void _publishAnswer(String sdp){
        final ObjectMapper mapper = new ObjectMapper();
        ObjectNode node = mapper.createObjectNode();
        node.put("type", "answer");
        node.put("sdp", sdp);

        wampClient.publish(AnswerTopic, node);
    }

}

```

## アプリケーション

```java:MainActivity.java
package com.nakadoribooks.webrtcexample;

import android.Manifest;
import android.graphics.Color;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private enum State{
        Disconnected
        , Connecting
        , Connected
        , Offering
        , ReceivedOffer
        , CreatingAnswer
        , Done
    }

    private static final String TAG = "MainActivity";
    private static final int REQUEST_CODE_CAMERA_PERMISSION = 1;
    private WebRTC webRTC;
    private Wamp wamp;
    private State state = State.Disconnected;

    private Button controlButton;
    private TextView statusText;
    private boolean typeOffer = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // getView
        controlButton = (Button) findViewById(R.id.control_button);
        statusText = (TextView) findViewById(R.id.status_label);

        // registerEvent
        findViewById(R.id.control_button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                onTapButton();
            }
        });

        // wamp
        wamp = new Wamp(this);

        // checkPermission → onRequestPermissionsResult → startWebRTC
        checkPermission();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        if (requestCode != REQUEST_CODE_CAMERA_PERMISSION)
            return;

        startWebRTC();
    }

    private void checkPermission(){
        String[] permissioins = new String[]{ Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
        ActivityCompat.requestPermissions(this, permissioins, REQUEST_CODE_CAMERA_PERMISSION);
    }

    private void onTapButton(){
        if(state == State.Disconnected){
            changeState(State.Connecting);
            connect();
        }else if(state == State.Connected){
            typeOffer = true;
            changeState(State.Offering);
            stateOffering();
            webRTC.createOffer();
        }
    }

    private void connect(){
        wamp.connect(new Wamp.WampCallbacks() {

            @Override
            public void onConnected() {
                changeState(State.Connected);
            }

            @Override
            public void onReceiveOffer(String sdp) {
                if(typeOffer){
                    return;
                }
                changeState(State.CreatingAnswer);
                webRTC.receiveOffer(sdp);
            }

            @Override
            public void onReceiveAnswer(String sdp) {
                if(!typeOffer){
                    return;
                }

                webRTC.receiveAnswer(sdp);
            }
        });
    }

    private void startWebRTC(){

        webRTC = new WebRTC(this);
        webRTC.connect(new WebRTC.WebRTCCallbacks() {
            @Override
            public void onCreateLocalSdp(String sdp) {
                if(typeOffer){
                    wamp.publishOffer(sdp);
                }else{
                    wamp.publishAnswer(sdp);
                }
            }

            @Override
            public void didReceiveRemoteStream() {
                changeState(State.Done);
            }
        });
        webRTC.startCapture();
    }

    // view ---------------

    private void stateConnecting(){
        statusText.setText("Connecting");
        controlButton.setText("Connecting...");
        controlButton.setEnabled(false);
    }

    private void stateConnected(){
        statusText.setText("connected");
        statusText.setTextColor(Color.BLUE);
        controlButton.setText("Send Offer");
        controlButton.setEnabled(true);
    }

    private void stateOffering(){
        statusText.setText("Offering...");
        controlButton.setText("Offering...");
        controlButton.setEnabled(false);
    }

    private void stateReceivedOffer(){
        statusText.setText("ReceivedOffer");
        controlButton.setText("ReceivedOffer");
        controlButton.setEnabled(false);
    }

    private void stateCreatingAnswer(){
        statusText.setText("CreatingAnswer...");
        controlButton.setText("CreatingAnswer...");
        controlButton.setEnabled(false);
    }

    private void stateDone(){
        statusText.setText("OK!");
        controlButton.setText("OK!");
        controlButton.setEnabled(false);
    }

    private void changeState(final State state){
        this.state = state;
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                switch (state){
                    case Connected:
                        stateConnected();
                        break;
                    case Connecting:
                        stateConnecting();
                        break;
                    case CreatingAnswer:
                        stateCreatingAnswer();
                        break;
                    case ReceivedOffer:
                        stateReceivedOffer();
                        break;
                    case Done:
                        stateDone();
                    default:
                        break;
                }
            }
        });
    }
}
```

# 美酒の設計
秋田の地酒　雪の茅舎美酒の設計純米吟醸火入れタイプ１，８L　　齋弥酒造
http://www.sudasaketen.com/item/k0003/

美酒の設計 単行本 – 2009/11/26 藤田 千恵子  (著)
http://amzn.asia/efouKSJ



# 次
Trickle ICE を予定