Skip to content

Commit

Permalink
Implemented HTTP device polling at the end of pairing stage to verify…
Browse files Browse the repository at this point in the history
… paring success
  • Loading branch information
Alberto Geniola committed Jun 7, 2021
1 parent 83daeb9 commit 283d78b
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<DeviceInfo> 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<DeviceInfo> 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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
}
Expand All @@ -498,7 +485,7 @@ enum Signal {
DEVICE_WIFI_CONNECTED,
DEVICE_CONFIGURED,
LOCAL_WIFI_CONNECTED,
MQTT_CONNECTED,
DEVICE_PAIRED,
ERROR
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String, Object> DEFAULT_PARAMS = new HashMap<>();
Expand Down Expand Up @@ -68,23 +75,30 @@ public void login(String apiUrl, String username, String password) throws IOExce
HashMap<String, Object> data = new HashMap<>();
data.put("email", username);
data.put("password", password);
Map<String, Object> 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<DeviceInfo> listDevices() throws IOException, HttpApiException, HttpApiInvalidCredentialsException {
HashMap<String, Object> data = new HashMap<>();
TypeToken<?> typeToken = TypeToken.getParameterized(List.class, DeviceInfo.class);
List<DeviceInfo> 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) {
Expand Down Expand Up @@ -112,7 +126,7 @@ private static String toHexString(byte[] bytes) {
}

@SneakyThrows({UnsupportedEncodingException.class, NoSuchAlgorithmException.class})
private Map<String, Object> authenticatedPost(@NonNull String url, HashMap<String, Object> data, String httpToken) throws IOException, HttpApiException, HttpApiInvalidCredentialsException {
private <T> T authenticatedPost(@NonNull String url, HashMap<String, Object> data, String httpToken, Type dataType) throws IOException, HttpApiException, HttpApiInvalidCredentialsException {

String nonce = generateNonce(16);
long timestampMillis = new Date().getTime();
Expand Down Expand Up @@ -150,7 +164,8 @@ private Map<String, Object> authenticatedPost(@NonNull String url, HashMap<Strin
l.severe("Bad HTTP Response code: " + response.code() );
}

ApiResponse responseData = g.fromJson(strdata, ApiResponse.class);
TypeToken<?> token = TypeToken.getParameterized(ApiResponse.class, dataType);
ApiResponse<T> responseData = g.fromJson(strdata, token.getType());

switch (responseData.getApiStatus()) {
case CODE_NO_ERROR:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import lombok.Getter;

@Getter
public class ApiResponse {
public class ApiResponse<T> {
@SerializedName("apiStatus")
private ErrorCodes apiStatus;

@SerializedName("info")
private String info;

@SerializedName("data")
private Map<String, Object> data;
private T data;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 283d78b

Please sign in to comment.