package org.commcare.heartbeat;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.commcare.CommCareApp;
import org.commcare.CommCareApplication;
import org.commcare.core.interfaces.HttpResponseProcessor;
import org.commcare.core.network.ModernHttpRequester;
import org.commcare.logging.AndroidLogger;
import org.commcare.preferences.CommCareServerPreferences;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.utils.StorageUtils;
import org.commcare.utils.SyncDetailCalculations;
import org.javarosa.core.io.StreamsUtil;
import org.javarosa.core.services.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
/**
* Responsible for making a heartbeat request to the server when signaled to do so by the current
* session's HeartbeatLifecycleManager, and then parsing and handling the response. Currently,
* the primary content of the server's response to the heartbeat request will be information
* about potential binary or app updates that the app should prompt users to conduct.
*
* Created by amstone326 on 5/5/17.
*/
public class HeartbeatRequester {
private static final String TAG = HeartbeatRequester.class.getSimpleName();
private static final String TEST_RESPONSE =
"{\"app_id\":\"73d5f08b9d55fe48602906a89672c214\",\"latest_apk_version\":{\"value\":\"2.36.1\"},\"latest_ccz_version\":{\"value\":\"75\", \"force_by_date\":\"2017-05-01\"}}";
private static final String APP_ID = "app_id";
private static final String QUARANTINED_FORMS_PARAM = "num_quarantined_forms";
private static final String UNSENT_FORMS_PARAM = "num_unsent_forms";
private static final String LAST_SYNC_TIME_PARAM = "last_sync_time";
private final HttpResponseProcessor responseProcessor = new HttpResponseProcessor() {
@Override
public void processSuccess(int responseCode, InputStream responseData) {
try {
String responseAsString = new String(StreamsUtil.inputStreamToByteArray(responseData));
JSONObject jsonResponse = new JSONObject(responseAsString);
parseHeartbeatResponse(jsonResponse);
}
catch (JSONException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Heartbeat response was not properly-formed JSON: " + e.getMessage());
}
catch (IOException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"IO error while processing heartbeat response: " + e.getMessage());
}
}
@Override
public void processRedirection(int responseCode) {
processErrorResponse(responseCode);
}
@Override
public void processClientError(int responseCode) {
processErrorResponse(responseCode);
}
@Override
public void processServerError(int responseCode) {
processErrorResponse(responseCode);
}
@Override
public void processOther(int responseCode) {
processErrorResponse(responseCode);
}
@Override
public void handleIOException(IOException exception) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Encountered IOException while getting response stream for heartbeat response: "
+ exception.getMessage());
}
private void processErrorResponse(int responseCode) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Received error response from heartbeat request: " + responseCode);
}
};
protected static void parseTestHeartbeatResponse() {
System.out.println("NOTE: Testing heartbeat response processing");
try {
parseHeartbeatResponse(new JSONObject(TEST_RESPONSE));
} catch (JSONException e) {
System.out.println("Test response was not properly formed JSON");
}
}
protected static void simulateRequestGettingStuck() {
System.out.println("Before sleeping");
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
System.out.println("TEST ERROR: sleep was interrupted");
}
System.out.println("After sleeping");
}
protected void requestHeartbeat() {
String urlString = CommCareApplication.instance().getCurrentApp().getAppPreferences()
.getString(CommCareServerPreferences.PREFS_HEARTBEAT_URL_KEY, null);
try {
Log.i(TAG, "Requesting heartbeat from " + urlString);
ModernHttpRequester requester =
CommCareApplication.instance().buildHttpRequesterForLoggedInUser(
CommCareApplication.instance(), new URL(urlString),
getParamsForHeartbeatRequest(), true, false);
requester.setResponseProcessor(responseProcessor);
requester.request();
} catch (MalformedURLException e) {
Logger.log(AndroidLogger.TYPE_ERROR_CONFIG_STRUCTURE,
"Heartbeat URL was malformed: " + e.getMessage());
}
}
private static HashMap<String, String> getParamsForHeartbeatRequest() {
HashMap<String, String> params = new HashMap<>();
params.put(APP_ID, CommCareApplication.instance().getCurrentApp().getUniqueId());
params.put(QUARANTINED_FORMS_PARAM, "" + StorageUtils.getNumQuarantinedForms());
params.put(UNSENT_FORMS_PARAM, "" + StorageUtils.getNumUnsentForms());
params.put(LAST_SYNC_TIME_PARAM, new Date(SyncDetailCalculations.getLastSyncTime()).toString());
return params;
}
private static void parseHeartbeatResponse(final JSONObject responseAsJson) {
// will run on UI thread
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (checkForAppIdMatch(responseAsJson)) {
// We only want to register this response if the current app is still the
// same as the one that sent the request originally
try {
CommCareApplication.instance().getSession().setHeartbeatSuccess();
} catch (SessionUnavailableException e) {
// Do nothing -- the session expired, so we just don't register the response
return;
}
Log.i(TAG, "Parsing heartbeat response");
attemptApkUpdateParse(responseAsJson);
attemptCczUpdateParse(responseAsJson);
}
}
});
}
private static boolean checkForAppIdMatch(JSONObject responseAsJson) {
try {
if (responseAsJson.has("app_id")) {
CommCareApp currentApp = CommCareApplication.instance().getCurrentApp();
if (currentApp != null) {
String appIdOfResponse = responseAsJson.getString("app_id");
String currentAppId = currentApp.getAppRecord().getUniqueId();
return appIdOfResponse.equals(currentAppId);
}
}
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Heartbeat response did not have required app_id param");
} catch (JSONException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"App id in heartbeat response was not formatted properly: " + e.getMessage());
}
return false;
}
private static void attemptApkUpdateParse(JSONObject responseAsJson) {
try {
if (responseAsJson.has("latest_apk_version")) {
JSONObject latestApkVersionInfo =
responseAsJson.getJSONObject("latest_apk_version");
parseUpdateToPrompt(latestApkVersionInfo, true);
}
} catch (JSONException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Latest apk version object in heartbeat response was not " +
"formatted properly: " + e.getMessage());
}
}
private static void attemptCczUpdateParse(JSONObject responseAsJson) {
try {
if (responseAsJson.has("latest_ccz_version")) {
JSONObject latestCczVersionInfo = responseAsJson.getJSONObject("latest_ccz_version");
parseUpdateToPrompt(latestCczVersionInfo, false);
}
} catch (JSONException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Latest ccz version object in heartbeat response was not " +
"formatted properly: " + e.getMessage());
}
}
private static void parseUpdateToPrompt(JSONObject latestVersionInfo, boolean isForApk) {
try {
if (latestVersionInfo.has("value")) {
String versionValue = latestVersionInfo.getString("value");
String forceByDate = null;
if (latestVersionInfo.has("force_by_date")) {
forceByDate = latestVersionInfo.getString("force_by_date");
}
UpdateToPrompt updateToPrompt = new UpdateToPrompt(versionValue, forceByDate, isForApk);
updateToPrompt.registerWithSystem();
}
} catch (JSONException e) {
Logger.log(AndroidLogger.TYPE_ERROR_SERVER_COMMS,
"Encountered malformed json while trying to parse server response into an " +
"UpdateToPrompt object : " + e.getMessage());
}
}
}