diff --git a/app/src/main/java/com/albertogeniola/merossconf/ui/fragments/pair/ExecutePairingFragment.java b/app/src/main/java/com/albertogeniola/merossconf/ui/fragments/pair/ExecutePairingFragment.java index 59b351d..5b29416 100644 --- a/app/src/main/java/com/albertogeniola/merossconf/ui/fragments/pair/ExecutePairingFragment.java +++ b/app/src/main/java/com/albertogeniola/merossconf/ui/fragments/pair/ExecutePairingFragment.java @@ -20,7 +20,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.util.Base64; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -36,35 +35,27 @@ import androidx.navigation.fragment.NavHostFragment; import com.albertogeniola.merossconf.AndroidPreferencesManager; -import com.albertogeniola.merossconf.AndroidUtils; import com.albertogeniola.merossconf.R; import com.albertogeniola.merossconf.model.MqttConfiguration; -import com.albertogeniola.merossconf.ssl.DummyTrustManager; import com.albertogeniola.merossconf.ui.PairActivityViewModel; import com.albertogeniola.merossconf.ui.views.TaskLine; import com.albertogeniola.merosslib.MerossDeviceAp; -import com.albertogeniola.merosslib.model.Cipher; -import com.albertogeniola.merosslib.model.Encryption; +import com.albertogeniola.merosslib.MerossHttpClient; +import com.albertogeniola.merosslib.model.OnlineStatus; import com.albertogeniola.merosslib.model.http.ApiCredentials; - -import org.eclipse.paho.android.service.MqttAndroidClient; -import org.eclipse.paho.client.mqttv3.IMqttActionListener; -import org.eclipse.paho.client.mqttv3.IMqttToken; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; +import com.albertogeniola.merosslib.model.http.DeviceInfo; +import com.albertogeniola.merosslib.model.http.exceptions.HttpApiException; import java.io.IOException; -import java.security.KeyManagementException; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.GregorianCalendar; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; - public class ExecutePairingFragment extends Fragment { private static final String TAG = "PairingFragment"; @@ -110,10 +101,11 @@ private void stateMachine(Signal signal) { if (signal == Signal.DEVICE_CONFIGURED) {connectToLocalWifi();} break; case CONNETING_LOCAL_WIFI: - if (signal == Signal.LOCAL_WIFI_CONNECTED) {connectToMqttBorker();} + if (signal == Signal.LOCAL_WIFI_CONNECTED) { + pollDeviceList();} break; - case CONNECTING_TO_MQTT_BROKER: - if (signal == Signal.MQTT_CONNECTED) {completeActivityFragment();} + case VERIFYING_PAIRING_SUCCEEDED: + if (signal == Signal.DEVICE_PAIRED) {completeActivityFragment();} break; } @@ -237,67 +229,62 @@ public void onAvailable(Network network) { } } - private void connectToMqttBorker() { - state = State.CONNECTING_TO_MQTT_BROKER; - String uri = "ssl://" + pairActivityViewModel.getTargetMqttConfig().getValue().getHostname() + ":" + pairActivityViewModel.getTargetMqttConfig().getValue().getPort(); - MqttAndroidClient mqttAndroidClient = new MqttAndroidClient( - requireContext().getApplicationContext(), - uri, - "app:check"); // TODO: Change this check; let the app connect as the real Meross App does - MqttConnectOptions options = new MqttConnectOptions(); - String fake_mac = "00:00:00:00:00:00"; - String userId = mCreds.getUserId(); - String key = mCreds.getKey(); - options.setUserName(fake_mac); - String password = calculateMQttPassword(userId, fake_mac, key); - options.setPassword(password.toCharArray()); - options.setAutomaticReconnect(false); - options.setCleanSession(false); - options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1); - options.setServerURIs(new String[] {uri}); - - // Disable SSL checks... - // TODO: parametrize this - TrustManager[] trustManagers = new DummyTrustManager[]{new DummyTrustManager()}; - SSLContext sc = null; - try { - // FIXME: wait a bit so that the underlying network becomes available. - Thread.sleep(3000); - sc = SSLContext.getInstance ("SSL"); - sc.init (null, trustManagers, new java.security.SecureRandom ()); - options.setSocketFactory(sc.getSocketFactory()); - } catch (KeyManagementException | NoSuchAlgorithmException | InterruptedException e) { - e.printStackTrace (); - error = "Error occurred while connecting to remote broker"; - stateMachine(Signal.ERROR); - } + private void pollDeviceList() { + state = State.VERIFYING_PAIRING_SUCCEEDED; + final long timeout = GregorianCalendar.getInstance().getTimeInMillis() + 30000; // 30 seconds timeout + ScheduledFuture future = worker.schedule(new Runnable() { + private @Nullable DeviceInfo findDevice(Collection devices, String deviceUuid) { + for (DeviceInfo d : devices) { + Log.d(TAG, "Device " + d.getUuid() + " has been found with status: " + d.getOnlineStatus()); + if (d.getUuid().compareTo(deviceUuid) == 0) { + return d; + } + } + return null; + } - /* Establish an MQTT connection */ - try { - mqttAndroidClient.connect(options, null, new IMqttActionListener() { - @Override - public void onSuccess(IMqttToken asyncActionToken) { + @Override + public void run() { + MerossHttpClient client = new MerossHttpClient(mCreds); + String targetUuid = pairActivityViewModel.getDeviceInfo().getValue().getPayload().getAll().getSystem().getHardware().getUuid(); + boolean succeeed = false; + boolean timedOut = GregorianCalendar.getInstance().getTimeInMillis() >= timeout; + boolean exitNow = false; + while(!exitNow && !succeeed && !Thread.currentThread().isInterrupted() && !timedOut) { try { - asyncActionToken.getClient().disconnect(); - } catch (MqttException e) { + List devices = client.listDevices(); + DeviceInfo d = findDevice(devices, targetUuid); + if (d == null) { + Log.i(TAG, "Device " +targetUuid + " not paired yet."); + } else if (d.getOnlineStatus() == OnlineStatus.ONLINE || d.getOnlineStatus() == OnlineStatus.LAN) { + Log.i(TAG, "Device " +targetUuid + " is online."); + succeeed = true; + } else { + Log.i(TAG, "Device " +targetUuid + " is paired, but not ready yet or in an unknown status."); + } + } catch (IOException e) { e.printStackTrace(); + Log.e(TAG, "An IOException occurred while polling the HTTP API server.", e); + } catch (HttpApiException e) { + Log.e(TAG, "The HTTP API server reported status " + e.getCode(), e); + } finally { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + exitNow = true; + error = "An error occurred while poling the HTTP API server"; + stateMachine(Signal.ERROR); + } } - stateMachine(Signal.MQTT_CONNECTED); + if (succeeed) + stateMachine(Signal.DEVICE_PAIRED); + else + stateMachine(Signal.ERROR); } - - @Override - public void onFailure(IMqttToken asyncActionToken, Throwable exception) { - error = "Failed MQTT Connection: " + exception.getMessage(); - stateMachine(Signal.ERROR); - } - }); - - } catch (MqttException e) { - e.printStackTrace(); - error = "Failed MQTT Connection: " + e.getMessage(); - stateMachine(Signal.ERROR); - } + } + }, 2, TimeUnit.SECONDS); } private String calculateMQttPassword(String userId, String fake_mac, String key) { @@ -407,7 +394,7 @@ public void run() { connectLocalWifiTaskLine.setState(TaskLine.TaskState.running); currentTask = connectLocalWifiTaskLine; break; - case CONNECTING_TO_MQTT_BROKER: + case VERIFYING_PAIRING_SUCCEEDED: errorDetailsTextView.setVisibility(View.GONE); connectLocalWifiTaskLine.setState(TaskLine.TaskState.completed); testMqttBrokerTaskLine.setState(TaskLine.TaskState.running); @@ -488,7 +475,7 @@ enum State { CONNECTING_DEVICE_WIFI_AP, SENDING_PAIRING_COMMAND, CONNETING_LOCAL_WIFI, - CONNECTING_TO_MQTT_BROKER, + VERIFYING_PAIRING_SUCCEEDED, DONE, ERROR } @@ -498,7 +485,7 @@ enum Signal { DEVICE_WIFI_CONNECTED, DEVICE_CONFIGURED, LOCAL_WIFI_CONNECTED, - MQTT_CONNECTED, + DEVICE_PAIRED, ERROR } diff --git a/merosslib/src/main/java/com/albertogeniola/merosslib/MerossHttpClient.java b/merosslib/src/main/java/com/albertogeniola/merosslib/MerossHttpClient.java index c8adc4b..93a19c0 100644 --- a/merosslib/src/main/java/com/albertogeniola/merosslib/MerossHttpClient.java +++ b/merosslib/src/main/java/com/albertogeniola/merosslib/MerossHttpClient.java @@ -2,12 +2,15 @@ import com.albertogeniola.merosslib.model.http.ApiCredentials; import com.albertogeniola.merosslib.model.http.ApiResponse; +import com.albertogeniola.merosslib.model.http.DeviceInfo; import com.albertogeniola.merosslib.model.http.ErrorCodes; +import com.albertogeniola.merosslib.model.http.LoginResponseData; import com.albertogeniola.merosslib.model.http.exceptions.HttpApiException; import com.albertogeniola.merosslib.model.http.exceptions.HttpApiInvalidCredentialsException; import com.albertogeniola.merosslib.model.http.exceptions.HttpApiTokenException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; @@ -19,14 +22,17 @@ import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; +import jdk.nashorn.internal.parser.TokenType; import lombok.NonNull; import lombok.SneakyThrows; @@ -41,6 +47,7 @@ public class MerossHttpClient implements Serializable { private static final Gson g = new GsonBuilder().disableHtmlEscaping().create(); private static final String NOONCE_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static final String LOGIN_PATH = "/v1/Auth/Login"; + private static final String DEVICE_LIST = "/v1/Device/devList"; private static final String LOGOUT_PATH = "/v1/Profile/logout"; private static final String SECRET = "23x17ahWarFH6w29"; private static final HashMap DEFAULT_PARAMS = new HashMap<>(); @@ -68,23 +75,30 @@ public void login(String apiUrl, String username, String password) throws IOExce HashMap data = new HashMap<>(); data.put("email", username); data.put("password", password); - Map result = authenticatedPost( apiUrl+LOGIN_PATH, data, null); + LoginResponseData result = authenticatedPost( apiUrl+LOGIN_PATH, data, null, LoginResponseData.class); this.mCredentials = new ApiCredentials( apiUrl, - result.get("token").toString(), - result.get("userid").toString(), - result.get("email").toString(), - result.get("key").toString(), + result.getToken(), + result.getUserId(), + result.getEmail(), + result.getKey(), new Date() ); } + public List listDevices() throws IOException, HttpApiException, HttpApiInvalidCredentialsException { + HashMap data = new HashMap<>(); + TypeToken typeToken = TypeToken.getParameterized(List.class, DeviceInfo.class); + List devices = authenticatedPost( mCredentials.getApiServer()+DEVICE_LIST, data, this.mCredentials.getToken(), typeToken.getType()); + return devices; + } + public void logout() throws HttpApiInvalidCredentialsException, HttpApiException, IOException { if (mCredentials == null) { throw new IllegalStateException("Invalid logout operation: this client is not logged in."); } - authenticatedPost(mCredentials.getApiServer()+LOGOUT_PATH, null, mCredentials.getToken()); + authenticatedPost(mCredentials.getApiServer()+LOGOUT_PATH, null, mCredentials.getToken(), Object.class); } private static String generateNonce(int targetStringLength) { @@ -112,7 +126,7 @@ private static String toHexString(byte[] bytes) { } @SneakyThrows({UnsupportedEncodingException.class, NoSuchAlgorithmException.class}) - private Map authenticatedPost(@NonNull String url, HashMap data, String httpToken) throws IOException, HttpApiException, HttpApiInvalidCredentialsException { + private T authenticatedPost(@NonNull String url, HashMap data, String httpToken, Type dataType) throws IOException, HttpApiException, HttpApiInvalidCredentialsException { String nonce = generateNonce(16); long timestampMillis = new Date().getTime(); @@ -150,7 +164,8 @@ private Map authenticatedPost(@NonNull String url, HashMap token = TypeToken.getParameterized(ApiResponse.class, dataType); + ApiResponse responseData = g.fromJson(strdata, token.getType()); switch (responseData.getApiStatus()) { case CODE_NO_ERROR: diff --git a/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/ApiResponse.java b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/ApiResponse.java index 5caffe1..4b271f7 100644 --- a/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/ApiResponse.java +++ b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/ApiResponse.java @@ -7,7 +7,7 @@ import lombok.Getter; @Getter -public class ApiResponse { +public class ApiResponse { @SerializedName("apiStatus") private ErrorCodes apiStatus; @@ -15,5 +15,5 @@ public class ApiResponse { private String info; @SerializedName("data") - private Map data; + private T data; } diff --git a/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/DeviceInfo.java b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/DeviceInfo.java new file mode 100644 index 0000000..f353c17 --- /dev/null +++ b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/DeviceInfo.java @@ -0,0 +1,56 @@ +package com.albertogeniola.merosslib.model.http; + +import com.albertogeniola.merosslib.model.OnlineStatus; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class DeviceInfo { + @SerializedName("uuid") + private String uuid; + + @SerializedName("onlineStatus") + private OnlineStatus onlineStatus; + + @SerializedName("devName") + private String devName; + + @SerializedName("bindTime") + private String bindTime; + + @SerializedName("deviceType") + private String deviceType; + + @SerializedName("subType") + private String subType; + + @SerializedName("channels") + private List[] channels; + + @SerializedName("region") + private String region; + + @SerializedName("fmwareVersion") + private String fmwareVersion; + + @SerializedName("hdwareVersion") + private String hdwareVersion; + + @SerializedName("userDevIcon") + private String userDevIcon; + + @SerializedName("iconType") + private int iconType; + + @SerializedName("skillNumber") + private String skillNumber; + + @SerializedName("domain") + private String domain; + + @SerializedName("reservedDomain") + private String reservedDomain; +} diff --git a/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/LoginResponseData.java b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/LoginResponseData.java new file mode 100644 index 0000000..d879a7e --- /dev/null +++ b/merosslib/src/main/java/com/albertogeniola/merosslib/model/http/LoginResponseData.java @@ -0,0 +1,20 @@ +package com.albertogeniola.merosslib.model.http; + +import com.google.gson.annotations.SerializedName; + +import lombok.Getter; + +@Getter +public class LoginResponseData { + @SerializedName("token") + private String token; + + @SerializedName("key") + private String key; + + @SerializedName("userid") + private String userId; + + @SerializedName("email") + private String email; +}