org.deviceconnect.android.deviceplugin.irkit.IRKitManager.java Source code

Java tutorial

Introduction

Here is the source code for org.deviceconnect.android.deviceplugin.irkit.IRKitManager.java

Source

/*
 IRKitManager.java
 Copyright (c) 2014 NTT DOCOMO,INC.
 Released under the MIT license
 http://opensource.org/licenses/mit-license.php
 */
package org.deviceconnect.android.deviceplugin.irkit;

import android.content.Context;
import android.content.ContextWrapper;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.support.v4.BuildConfig;
import android.telephony.TelephonyManager;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;

/**
 * IRKit???.
 * @author NTT DOCOMO, INC.
 */
public enum IRKitManager {
    /**
     * ?IRKitManager?.
     */
    INSTANCE;

    /**
     * ??.
     */
    private static final String TAG = "IRKit";
    /**
     * Http  200.
     */
    private static final int STATUS_CODE_OK = 200;

    /**
     * IP? {@value} .
     */
    private static final int RESOLVE_TIMEOUT = 500;

    /**
     * HTTP? {@value} .
     */
    private static final int HTTP_REQUEST_TIMEOUT = 5000;

    /**
     * HTTP? {@value} .
     */
    private static final int HTTP_REQUEST_LONG_TIMEOUT = 30000;

    /**
     * ?.
     */
    private static final int MAX_PASSWORD_LENGTH = 63;

    /**
     * SSID?.
     */
    private static final int MAX_SSID_LENGTH = 32;

    /**
     * ??.
     */
    private static final int MAX_DEVICE_KEY_LENGTH = 32;
    /**
     * CRC8??.
     */
    private static final byte CRC8INIT = 0x00;
    /**
     * CRC8??.
     */
    private static final byte CRC8POLY = 0x31; // = X^8+X^5+X^4+X^0
    /**
     * IRKit?.
     */
    private static final String SERVICE_TYPE = "_irkit._tcp.local.";

    /**
     * IRKit AIP??.
     */
    private static final String INTERNET_HOST = "api.getirkit.com";

    /**
     * IRKitWiFi??????.
     */
    public static final String DEVICE_HOST = "192.168.1.1";

    /**
     * ??.
     */
    private static final String MULTI_CAST_LOCK_TAG = "org.deviceconnect.android.deviceplugin.irkit";

    /**
     * 16?.
     */
    private static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray();

    /**
     * ?. regdomain???
     */
    private static final String[] NOT_JP_COUNTRIES = { "CA", "MX", "US", "AU", "HK", "IN", "MY", "NZ", "PH", "TW",
            "RU", "AR", "BR", "CL", "CO", "CR", "DO", "DM", "EC", "PA", "PY", "PE", "PR", "VE" };

    /** . */
    private static final long SLEEP_TIME = 1000;

    /** 3.0??. */
    private static final String X_REQUESTED_WITH_HEADER_NAME = "X-Requested-With";
    /** 3.0. */
    private static final String X_REQUESTED_WITH_HEADER_VALUE = "IRKit Device Plug-in";

    /**
     * .
     */
    private DetectionListener mDetectionListener;

    /**
     * DNS.
     */
    private JmDNS mDNS;

    /**
     * .
     */
    private ServiceListener mServiceListener;

    /**
     * IP?int.
     */
    private int mIpValue;

    /**
     * .
     */
    private boolean mIsDetecting;

    /**
     * apikey.
     */
    private String mAPIKey;

    /**
     * regDomain?????.
     */
    private String mCountryCode;

    /** 
     * ?.
     */
    private MulticastLock mMultiLock;

    /** 
     * ??.
     */
    private ConcurrentHashMap<String, IRKitDevice> mServices;

    /** 
     * ?.
     */
    private ServiceRemovingDiscoveryHandler mRemoveHandler;

    /**
     * ?.
     */
    private ExecutorService mExecutor = Executors.newSingleThreadExecutor();

    /**
     * WiFi?.
     */
    public enum WiFiSecurityType {

        /**
         * ?.
         */
        NONE(0),

        /**
         * WEP.
         */
        WEP(2),

        /**
         * WPA2.
         */
        WPA2(8);

        /**
         * .
         */
        int mCode;

        /**
         * ?????.
         * 
         * @param code 
         */
        private WiFiSecurityType(final int code) {
            mCode = code;
        }
    }

    /**
     * IRKitManager???.
     */
    private IRKitManager() {
        mServiceListener = new ServiceListenerImpl();
        mServices = new ConcurrentHashMap<String, IRKitDevice>();
    }

    /**
     * IRkit???.
     */
    public ConcurrentHashMap<String, IRKitDevice> getIRKitDevices() {
        return mServices;
    }

    /**
     * ??Host?Path??GET??.
     * 
     * @param host ??
     * @param path 
     * @return HttpURLConnection?
     */
    private HttpURLConnection createGetRequest(final String host, final String path) throws IOException {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "http://" + host + path);
        }
        URL url = new URL("http://" + host + path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty(X_REQUESTED_WITH_HEADER_NAME, X_REQUESTED_WITH_HEADER_VALUE);
        conn.setReadTimeout(HTTP_REQUEST_TIMEOUT);
        conn.setConnectTimeout(HTTP_REQUEST_TIMEOUT);
        conn.setUseCaches(false);
        return conn;
    }

    /**
     * ??Host?Path??POST??.
     * 
     * @param host ??
     * @param path 
     * @return HttpURLConnection?
     */
    private HttpURLConnection createPostRequest(final String host, final String path) throws IOException {
        URL url = new URL("http://" + host + path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty(X_REQUESTED_WITH_HEADER_NAME, X_REQUESTED_WITH_HEADER_VALUE);
        conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
        conn.setReadTimeout(HTTP_REQUEST_LONG_TIMEOUT);
        conn.setConnectTimeout(HTTP_REQUEST_LONG_TIMEOUT);
        conn.setDoOutput(true);
        conn.setDoInput(true);
        conn.setUseCaches(false);
        return conn;
    }

    /**
     * ?.
     * @param conn HttpURLConnection
     * @return ??Body
     */
    private String executeRequest(final HttpURLConnection conn) {
        return executeRequest(conn, "");
    }

    /**
     * ?.
     * 
     * @param conn 
     * @param bodyData ??
     * @return ?????null?
     */
    private String executeRequest(final HttpURLConnection conn, final String bodyData) {
        String body = null;
        InputStream in = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (bodyData != null && bodyData.length() > 0) {
                OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
                out.write(bodyData);
                out.flush();
                out.close();
            }
            conn.connect();
            int resp = conn.getResponseCode();
            if (resp == 200) {
                in = conn.getInputStream();
                int len;
                byte[] buf = new byte[4096];
                while ((len = in.read(buf)) > 0) {
                    baos.write(buf, 0, len);
                }
                in.close();
            } else {
                in = conn.getErrorStream();
                int len;
                byte[] buf = new byte[4096];
                while ((len = in.read(buf)) > 0) {
                    baos.write(buf, 0, len);
                }
                in.close();
            }
            body = new String(baos.toByteArray(), "UTF-8");
        } catch (IOException e) {
            if (BuildConfig.DEBUG) {
                Log.e(TAG, "IOException", e);
            }
            body = null;
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                if (BuildConfig.DEBUG) {
                    Log.e(TAG, "Http Request Error", e);
                }
            }
            conn.disconnect();
        }
        return body;
    }

    /**
     * ?.
     * @param conn HttpURLConnection
     * @param keyValues Query
     * @return Body?
     * @throws IOExceptionStream?
     */
    private String executeRequest(final HttpURLConnection conn, final Map<String, String> keyValues)
            throws IOException {
        StringBuffer body = new StringBuffer();
        if (keyValues.size() > 0) {
            Uri.Builder builder = new Uri.Builder();
            Set<String> keys = keyValues.keySet();
            for (String key : keys) {
                builder.appendQueryParameter(key, keyValues.get(key));
            }
            String join = builder.build().getEncodedQuery();
            OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
            out.write(join);
            out.flush();
            out.close();
            conn.connect();
            String st = null;
            InputStream in = null;
            try {
                in = conn.getInputStream();
                BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
                while ((st = br.readLine()) != null) {
                    body.append(st);
                }
            } catch (IOException e) {
                if (BuildConfig.DEBUG) {
                    Log.e(TAG, "Http Request Error", e);
                }
                body = null;
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        Log.e(TAG, "Http Request Error", e);
                    }
                }
                conn.disconnect();

            }
        }
        return body.toString();
    }

    /**
     * WiFi?IP??.
     * 
     * @param ipValue IP?int
     * @return InetAddress?????null?
     */
    private InetAddress parseIPAddress(final int ipValue) {
        byte[] byteaddr = new byte[] { (byte) (ipValue & 0xff), (byte) (ipValue >> 8 & 0xff),
                (byte) (ipValue >> 16 & 0xff), (byte) (ipValue >> 24 & 0xff) };
        try {
            return InetAddress.getByAddress(byteaddr);
        } catch (UnknownHostException e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
            // IP?????????????
            return null;
        }
    }

    /**
     * 16???.
     * 
     * @param ori ??
     * @param maxLength ??
     * @return ?
     */
    private String toHex(final String ori, final int maxLength) {

        String tmp = ori;
        if (tmp.length() > maxLength) {
            tmp = tmp.substring(0, maxLength);
        }

        byte[] data = tmp.getBytes();
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte b : data) {
            sb.append(HEX_CODE[(b >> 4) & 0xF]);
            sb.append(HEX_CODE[(b & 0xF)]);
        }

        return sb.toString();
    }

    /**
     * CRC8?.
     * 
     * @param data ?
     * @param size 
     * @return ???
     */
    private byte crc8(byte[] data, int size) {
        return crc8(data, size, CRC8INIT);
    }

    /**
     * CRC8?
     * @param data ?
     * @param size 
     * @param crcinit CRC?
     * @return CRC8??
     */
    private byte crc8(byte[] data, int size, byte crcinit) {
        byte crc = crcinit;
        int dataLength = data.length;
        for (int i = 0; i < size; i++) {
            if (i < dataLength) {
                crc ^= data[i];
            }
            for (int j = 0; j < 8; j++) {
                if ((crc & 0x80) != 0x00) {
                    crc = (byte) ((crc << 1) ^ CRC8POLY);
                } else {
                    crc <<= 1;
                }
            }
        }
        return crc;
    }

    /**
     * ????.
     * @param bytes ?
     * @return ?
     */
    private String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format(Locale.US, "%02x", b));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * WiFi??????byte????.
     * 
     * @param type 
     * @param ssid SSID
     * @param password 
     * @param deviceKey ?
     * @return byte?
     */
    private String toCRC(final WiFiSecurityType type, final String ssid, final String password,
            final String deviceKey) {

        byte[] ssidBytes;
        byte[] passwordBytes;
        byte[] deviceKeyBytes;
        try {
            ssidBytes = ssid.getBytes("UTF-8");
            passwordBytes = password.getBytes("UTF-8");
            deviceKeyBytes = deviceKey.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }

        byte crc = crc8(new byte[] { (byte) type.mCode }, 1);
        crc = crc8(ssidBytes, 33, crc);
        crc = crc8(passwordBytes, 64, crc);
        crc = crc8(new byte[] { 1 }, 1, crc); // wifi_is_set == true
        crc = crc8(new byte[] { 0 }, 1, crc); // wifi_was_valid == false
        crc = crc8(deviceKeyBytes, 33, crc);
        String crcString = String.format(Locale.US, "%02x", crc).toUpperCase();

        return crcString;
    }

    /**
     * ????byte????.
     * 
     * @param str 
     * @param length ?
     * @return byte?
     */
    private byte[] toByte(final String str, final int length) {

        byte[] res = new byte[length];
        byte[] data = str.getBytes();

        for (int i = 0; i < length; i++) {
            if (i == data.length) {
                break;
            }
            res[i] = data[i];
        }

        return res;
    }

    /**
     * ?.
     * 
     * @param device 
     */
    private void addService(final IRKitDevice device) {
        mServices.put(device.getName(), device);
    }

    /**
     * ?.
     * 
     * @param device 
     */
    private synchronized void removeService(final IRKitDevice device) {

        if (!isDetecting()) {
            return;
        }

        mServices.remove(device.getName());

        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Lost Device : " + device);
        }

        if (mDetectionListener != null) {
            mDetectionListener.onLostDevice(device);
        }
    }

    /**
     * regdomain??.
     * 
     * @return regdomain
     */
    private String getRegDomain() {

        String regdomain = null;

        if (mCountryCode == null) {
            Locale locale = Locale.getDefault();
            mCountryCode = locale.getCountry();
        }

        if ("JP".equals(mCountryCode)) {
            regdomain = "2";
        } else {

            for (String code : NOT_JP_COUNTRIES) {
                if (code.equals(mCountryCode)) {
                    regdomain = "0";
                    break;
                }
            }

            if (regdomain == null) {
                regdomain = "1";
            }
        }
        return regdomain;
    }

    /**
     * ??.
     * 
     * @param context ??????
     */
    public void init(final ContextWrapper context) {
        mAPIKey = context.getString(R.string.apikey);

        TelephonyManager tm = getTelephonyManager(context);
        mCountryCode = tm.getSimCountryIso().toUpperCase(Locale.ENGLISH);
    }

    /**
     * ?.
     * 
     * @param listener DetectionListener?
     */
    public synchronized void setDetectionListener(final DetectionListener listener) {
        mDetectionListener = listener;
    }

    /**
     * ????.
     * 
     * @return ?true?????false
     */
    public synchronized boolean isDetecting() {
        return mIsDetecting;
    }

    /**
     * ?.
     * 
     * @param context 
     */
    public synchronized void startDetection(final ContextWrapper context) {

        if (isDetecting()) {
            return;
        }

        mIsDetecting = true;
        init(context);

        WifiManager wifi = getWifiManager(context);
        mMultiLock = wifi.createMulticastLock(MULTI_CAST_LOCK_TAG);
        mMultiLock.setReferenceCounted(true);
        mMultiLock.acquire();

        WifiInfo info = wifi.getConnectionInfo();
        if (info != null) {
            mIpValue = info.getIpAddress();
        } else {
            mIpValue = 0;
        }

        new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (INSTANCE) {
                    try {
                        if (mDNS != null || mIpValue == 0) {
                            mIsDetecting = false;
                            return;
                        }
                        InetAddress ia = parseIPAddress(mIpValue);
                        if (ia == null) {
                            mIsDetecting = false;
                            return;
                        }

                        mDNS = JmDNS.create(ia);
                        mDNS.addServiceListener(SERVICE_TYPE, mServiceListener);
                        mIsDetecting = true;

                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "start detection.");
                        }

                    } catch (IOException e) {
                        if (BuildConfig.DEBUG) {
                            e.printStackTrace();
                        }
                        mIsDetecting = false;
                        mDNS = null;
                    }
                }
            }
        }).start();
    }

    /**
     * ?.
     */
    public synchronized void stopDetection() {
        if (mDNS != null) {
            mRemoveHandler = null;
            mIsDetecting = false;
            mServices.clear();
            mDNS.removeServiceListener(SERVICE_TYPE, mServiceListener);
            try {
                mDNS.close();
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "close detection.");
                }
            } catch (IOException e) {
                if (BuildConfig.DEBUG) {
                    e.printStackTrace();
                }
            } finally {
                // ???????????????????????????????
                mDNS = null;
            }
        }

        if (mMultiLock != null && mMultiLock.isHeld()) {
            mMultiLock.release();
            mMultiLock = null;
        }
    }

    /**
     * ??IP?????IRKit???.
     * 
     * @param ip IRKit?IP
     * @param callback ?????
     */
    public void fetchMessage(final String ip, final GetMessageCallback callback) {
        mExecutor.execute(new Runnable() {

            @Override
            public void run() {
                HttpURLConnection req = null;
                try {
                    req = createGetRequest(ip, "/messages");
                    String message = executeRequest(req);
                    callback.onGetMessage(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // ?IRKit??????????
                // IRKit??????????????
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * ??.
     * 
     * @param ip IRKit?IP
     * @param message 
     * @param callback ??????
     */
    public void sendMessage(final String ip, final String message, final PostMessageCallback callback) {
        mExecutor.execute(new Runnable() {

            @Override
            public void run() {
                HttpURLConnection req = null;
                boolean result = false;
                try {
                    req = createPostRequest(ip, "/messages");
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "ip=" + ip + " post message : " + message);
                    }
                    req.setRequestProperty("Content-Length", String.valueOf(message.length()));
                    OutputStreamWriter out = new OutputStreamWriter(req.getOutputStream());
                    out.write(message);
                    out.flush();
                    req.connect();
                    out.close();
                    int status = req.getResponseCode();
                    if (status == STATUS_CODE_OK) {
                        result = true;
                    }

                    // ?IRKit??????????
                    // IRKit??????????????
                    try {
                        Thread.sleep(SLEEP_TIME);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    callback.onPostMessage(result);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (req != null) {
                        req.disconnect();
                    }
                    callback.onPostMessage(result);
                }

            }
        });
    }

    /**
     * clientkey??.
     * 
     * @param callback ?
     */
    public void fetchClientKey(final GetClientKeyCallback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                String clientKey = null;
                try {
                    Map<String, String> params = new HashMap<>();
                    params.put("apikey", mAPIKey);
                    HttpURLConnection req = createPostRequest(INTERNET_HOST, "/1/clients");
                    req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

                    String body = executeRequest(req, params);
                    JSONObject json = new JSONObject(body);
                    clientKey = json.getString("clientkey");
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        e.printStackTrace();
                    }
                    clientKey = null;
                } catch (JSONException e) {
                    if (BuildConfig.DEBUG) {
                        e.printStackTrace();
                    }
                    clientKey = null;
                }

                callback.onGetClientKey(clientKey);

                // ?IRKit??????????
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * ?????.
     * 
     * @param clientKey clientkey
     * @param callback ?
     */
    public void createNewDevice(final String clientKey, final GetNewDeviceCallback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                String deviceKey = null;
                String serviceId = null;
                try {
                    Map<String, String> params = new HashMap<>();
                    params.put("clientkey", clientKey);
                    HttpURLConnection req = createPostRequest(INTERNET_HOST, "/1/devices");
                    req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

                    String body = executeRequest(req, params);
                    JSONObject json = new JSONObject(body);
                    serviceId = json.getString("deviceid");
                    deviceKey = json.getString("devicekey");
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        e.printStackTrace();
                    }
                    serviceId = null;
                    deviceKey = null;
                } catch (JSONException e) {
                    if (BuildConfig.DEBUG) {
                        e.printStackTrace();
                    }
                    serviceId = null;
                    deviceKey = null;
                }
                callback.onGetDevice(serviceId, deviceKey);
            }
        }).start();
    }

    /**
     * IRKitWiFi???.
     * 
     * @param ssid ??WiFi?SSID
     * @param password 
     * @param type 
     * @param deviceKey ?
     * @param callback ?
     */
    public void connectIRKitToWiFi(final String ssid, final String password, final WiFiSecurityType type,
            final String deviceKey, final IRKitConnectionCallback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {

                String ssidHex = toHex(ssid, MAX_SSID_LENGTH + 1);
                String tmpPassword = password;
                if (type == WiFiSecurityType.WEP && (password.length() == 5 || password.length() == 13)) {
                    tmpPassword = toHex(password, MAX_PASSWORD_LENGTH);
                }
                String passHex = toHex(tmpPassword, MAX_PASSWORD_LENGTH);
                String crcHex = toCRC(type, ssid, tmpPassword, deviceKey);
                String regdomain = getRegDomain();
                String postData = String.format(Locale.ENGLISH, "%d/%s/%s/%s/%s//////%s", type.mCode, ssidHex,
                        passHex, deviceKey, regdomain, crcHex).toUpperCase(Locale.ENGLISH);

                HttpURLConnection req = null;
                boolean result = false;
                try {
                    req = createPostRequest(DEVICE_HOST, "/wifi");
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "body : " + postData);
                    }
                    req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                    executeRequest(req, postData);
                    int status = req.getResponseCode();
                    if (status == STATUS_CODE_OK) {
                        result = true;
                    }
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        Log.e(TAG, "disconnect:" + result, e);
                    }
                } finally {
                    if (req != null) {
                        req.disconnect();
                    }
                    callback.onConnectedToWiFi(result);
                }
            }
        }).start();
    }

    /**
     * ??IP???IRKit???.
     * 
     * @param ip IP
     * @param callback ?
     */
    public void checkIfTargetIsIRKit(final String ip, final CheckingIRKitCallback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {

                boolean isIRKit = false;
                HttpURLConnection req = null;
                try {
                    req = createGetRequest(ip, "/messages");
                    executeRequest(req);
                    Map<String, List<String>> headers = req.getHeaderFields();
                    for (String h : headers.keySet()) {
                        if (h == null) {
                            continue;
                        }
                        if (h.equals("Server")) {
                            for (String v : headers.get(h)) {
                                if (v.contains(TAG)) {
                                    isIRKit = true;
                                    break;
                                }
                            }
                            if (isIRKit) {
                                break;
                            }
                        }
                    }
                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        Log.e(TAG, "disconnect:" + isIRKit, e);

                    }
                } finally {
                    if (req != null) {
                        req.disconnect();
                    }
                }

                callback.onChecked(isIRKit);
            }
        }).start();
    }

    /**
     * IRKit????????.
     * 
     * @param clientKey 
     * @param serviceId ID
     * @param callback ?
     */
    public void checkIfIRKitIsConnectedToInternet(final String clientKey, final String serviceId,
            final IRKitConnectionCheckingCallback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                String hostName = null;
                try {
                    Map<String, String> params = new HashMap<>();
                    params.put("clientkey", clientKey);
                    params.put("deviceid", serviceId);
                    HttpURLConnection req = createPostRequest(INTERNET_HOST, "/1/door");
                    req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

                    String body = executeRequest(req, params);
                    JSONObject json = new JSONObject(body);
                    hostName = json.getString("hostname");

                } catch (IOException e) {
                    if (BuildConfig.DEBUG) {
                        Log.e(TAG, "disconnect:" + (hostName != null), e);
                    }
                    hostName = null;
                } catch (JSONException e) {
                    if (BuildConfig.DEBUG) {
                        Log.e(TAG, "disconnect:" + (hostName != null), e);
                    }
                    hostName = null;
                }
                callback.onConnectedToInternet(hostName != null);
            }
        }).start();
    }

    private TelephonyManager getTelephonyManager(final Context context) {
        return (TelephonyManager) context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
    }

    private WifiManager getWifiManager(final Context context) {
        return (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
    }

    /**
     * ???.
     */
    public interface DetectionListener {

        /**
         * ??????.
         * 
         * @param device ??IRKit??
         */
        void onFoundDevice(IRKitDevice device);

        /**
         * ???????.
         * 
         * @param device ??IRKit??
         */
        void onLostDevice(IRKitDevice device);
    }

    /**
     * ???.
     */
    public interface GetMessageCallback {

        /**
         * ????????.
         * 
         * @param message ???????null
         */
        void onGetMessage(String message);
    }

    /**
     * ???.
     */
    public interface PostMessageCallback {
        /**
         * ???????.
         * 
         * @param result ????true?????false?
         */
        void onPostMessage(boolean result);
    }

    /**
     * clientkey???.
     */
    public interface GetClientKeyCallback {

        /**
         * clientkey??????.
         * 
         * @param clientKey ??????????null?
         */
        void onGetClientKey(String clientKey);
    }

    /**
     * ?????.
     */
    public interface GetNewDeviceCallback {

        /**
         * ???????????.
         * 
         * @param serviceId ID
         * @param deviceKey ?
         */
        void onGetDevice(String serviceId, String deviceKey);
    }

    /**
     * IRKit?WiFi????.
     */
    public interface IRKitConnectionCallback {

        /**
         * WiFi?????????.
         * 
         * @param isConnect ??true?????false?
         */
        void onConnectedToWiFi(boolean isConnect);
    }

    /**
     * IRKit??????.
     */
    public interface IRKitConnectionCheckingCallback {

        /**
         * ??????????.
         * 
         * @param isConnect ??true?????false?
         */
        void onConnectedToInternet(boolean isConnect);
    }

    /**
     * IRKit???????.
     */
    public interface CheckingIRKitCallback {

        /**
         * IRKit???????????.
         * 
         * @param isIRKit IRKit?true?????false?
         */
        void onChecked(boolean isIRKit);
    }

    /**
     * ?.
     */
    private class ServiceListenerImpl implements ServiceListener {

        @Override
        public void serviceAdded(final ServiceEvent event) {
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "serviceAdded");
            }
            synchronized (INSTANCE) {
                if (mDetectionListener != null) {
                    mDNS.requestServiceInfo(SERVICE_TYPE, event.getName(), RESOLVE_TIMEOUT);
                }
            }
        }

        @Override
        public void serviceRemoved(final ServiceEvent event) {
            // do-nothing.
        }

        @Override
        public void serviceResolved(final ServiceEvent event) {
            ServiceInfo info = event.getInfo();
            String ip = null;

            Inet4Address[] ipv4 = info.getInet4Addresses();
            if (ipv4 != null && ipv4.length != 0) {
                ip = ipv4[0].toString();
            } else {
                Inet6Address[] ipv6 = info.getInet6Addresses();
                if (ipv6 != null && ipv6.length != 0) {
                    ip = ipv6[0].toString();
                }
            }

            if (ip == null) {
                // IP???????????
                return;
            }

            IRKitDevice device = new IRKitDevice();
            device.setName(info.getName().toUpperCase(Locale.ENGLISH));
            ip = ip.replace("/", ""); // /?????????.
            device.setIp(ip);

            synchronized (INSTANCE) {
                addService(device);

                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "serviceResolved ip=" + ip);
                    Log.d(TAG, "device=" + device);
                    Log.d(TAG, "devicename=" + device.getName());
                }

                if (mRemoveHandler == null) {
                    mRemoveHandler = new ServiceRemovingDiscoveryHandler();
                    mRemoveHandler.start();
                } else {
                    mRemoveHandler.refresh();
                }
                mDetectionListener.onFoundDevice(device);
            }
        }
    }

    /**
     * ?.
     */
    private class ServiceRemovingDiscoveryHandler extends Thread {

        /** 
         * ?. ()
         */
        private static final long MAX_INTERVAL = 512 * 1000;

        /**
         * ???. ()
         */
        private static final long INITIAL_INTERVAL = 60;

        /** 
         * ?. ()
         */
        private long mNextInterval = INITIAL_INTERVAL;

        /** 
         * ??. ()
         */
        private long mDelay;

        /**
         * .
         */
        private int mCount;

        /** 
         * .
         */
        private ArrayList<IRKitDevice> mRemoveList = new ArrayList<IRKitDevice>();

        /**
         * ??.
         */
        public ServiceRemovingDiscoveryHandler() {
        }

        /**
         * ??.
         */
        public synchronized void refresh() {
            interrupt();
            mCount = 0;
            mDelay = 0;
            mNextInterval = INITIAL_INTERVAL;
        }

        @Override
        public void run() {
            super.run();

            while (true) {

                if (!isDetecting()) {
                    break;
                }

                long pt = checkConnection();

                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Check Time : " + pt);
                }

                synchronized (this) {

                    // NOTE: ??8???????.
                    // ???????????????
                    // 2?????????.
                    if (mCount < 64) {
                        mCount++;
                    } else {
                        mNextInterval <<= 1;
                    }

                    mDelay = (mNextInterval * 1000) - pt;
                    if (mDelay < 0) {
                        mDelay = 0;
                    } else if (mDelay > MAX_INTERVAL) {
                        mDelay = MAX_INTERVAL;
                        mNextInterval = MAX_INTERVAL;
                    }
                }

                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "Start remove checking after " + mDelay + " ms.");
                }

                try {
                    if (isInterrupted()) {
                        throw new InterruptedException();
                    }
                    sleep(mDelay);
                } catch (InterruptedException e) {
                    if (BuildConfig.DEBUG) {
                        e.printStackTrace();
                    }
                    mNextInterval = INITIAL_INTERVAL;
                }
            }
        }

        /**
         * IRKit???.
         * 
         * @return 
         */
        private long checkConnection() {
            final CountDownLatch lock;
            mRemoveList.clear();
            long start = System.currentTimeMillis();
            synchronized (mServices) {
                final int max = mServices.size();
                lock = new CountDownLatch(max);
                for (final IRKitDevice device : mServices.values()) {
                    checkIfTargetIsIRKit(device.getIp(), new CheckingIRKitCallback() {
                        @Override
                        public void onChecked(final boolean isIRKit) {
                            synchronized (mRemoveList) {

                                if (!isIRKit) {
                                    mRemoveList.add(device);
                                }

                                lock.countDown();
                            }
                        }
                    });
                }
            }

            try {
                lock.await();
            } catch (InterruptedException e) {
                if (BuildConfig.DEBUG) {
                    e.printStackTrace();
                }
            } finally {
                for (IRKitDevice device : mRemoveList) {
                    removeService(device);
                }
            }

            return System.currentTimeMillis() - start;
        }
    }
}