diff --git a/mobile/build.gradle b/mobile/build.gradle index 2b840ae35ee5cc8b55bf2bbd5a92781059461bb2..c7cae1ac0de681f3aec0fb9e95fa12c4d328b2ca 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation 'com.android.support:multidex:1.0.3' implementation 'org.jmdns:jmdns:3.5.4' implementation 'com.squareup.okhttp3:okhttp:3.11.0' + implementation 'com.github.heremaps:oksse:c92d0556f01e769d7c06c650941107642ce98fb5' implementation 'com.larswerkman:HoloColorPicker:1.5' implementation 'com.github.BigBadaboom:androidsvg:3511e136498da94018ef9fa438895984ea9b99db' implementation 'com.github.apl-devs:appintro:v4.2.3' diff --git a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java index a25f65e1056191a88dffd7f76c7fcb9ba8925007..aa80d0aa46f7c3280c9b992de1f4be2a1474ff9d 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java +++ b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java @@ -221,11 +221,27 @@ public abstract class OpenHABItem implements Parcelable { .build(); } + public static OpenHABItem updateFromEvent(OpenHABItem item, JSONObject jsonObject) + throws JSONException { + if (jsonObject == null) { + return item; + } + Builder builder = parseFromJson(jsonObject); + // Events don't contain the link property, so preserve that if previously present + if (item != null) { + builder.link(item.link()); + } + return builder.build(); + } + public static OpenHABItem fromJson(JSONObject jsonObject) throws JSONException { if (jsonObject == null) { return null; } + return parseFromJson(jsonObject).build(); + } + private static OpenHABItem.Builder parseFromJson(JSONObject jsonObject) throws JSONException { String name = jsonObject.getString("name"); String state = jsonObject.optString("state", ""); if ("NULL".equals(state) || "UNDEF".equals(state) || "undefined".equalsIgnoreCase(state)) { @@ -267,7 +283,6 @@ public abstract class OpenHABItem implements Parcelable { .members(members) .options(options) .state(state) - .readOnly(readOnly) - .build(); + .readOnly(readOnly); } } diff --git a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java index aa6a915c225c5e3d0662c73e9d14430cbb56ea4c..4bc4f3f27c4f05b701fd0851a4021f4e1bd06673 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java +++ b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java @@ -295,6 +295,19 @@ public abstract class OpenHABWidget implements Parcelable { } } + public static OpenHABWidget updateFromEvent(OpenHABWidget source, JSONObject eventPayload, + String iconFormat) throws JSONException { + OpenHABItem item = OpenHABItem.updateFromEvent( + source.item(), eventPayload.getJSONObject("item")); + String iconPath = determineOH2IconPath(item, source.type(), + source.icon(), iconFormat, !source.mappings().isEmpty()); + return source.toBuilder() + .label(eventPayload.optString("label", source.label())) + .item(item) + .iconPath(iconPath) + .build(); + } + private static String determineOH2IconPath(OpenHABItem item, Type type, String icon, String iconFormat, boolean hasMappings) { String itemState = item != null ? item.state() : null; diff --git a/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java b/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..e7bcd878306b8cb7edfe48c9909774f7d0ddaf0e --- /dev/null +++ b/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java @@ -0,0 +1,178 @@ +package org.openhab.habdroid.model; + +import android.os.Parcelable; +import android.util.Log; + +import com.google.auto.value.AutoValue; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.habdroid.core.connection.Connection; +import org.openhab.habdroid.util.AsyncHttpClient; +import org.openhab.habdroid.util.Util; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import okhttp3.Call; +import okhttp3.Headers; +import okhttp3.Request; + +@AutoValue +public abstract class ServerProperties implements Parcelable { + private static final String TAG = ServerProperties.class.getSimpleName(); + + public static final int SERVER_FLAG_JSON_REST_API = 1 << 0; + public static final int SERVER_FLAG_SSE_SUPPORT = 1 << 1; + public static final int SERVER_FLAG_ICON_FORMAT_SUPPORT = 1 << 2; + public static final int SERVER_FLAG_CHART_SCALING_SUPPORT = 1 << 3; + + public static class UpdateHandle { + public void cancel() { + if (call != null) { + call.cancel(); + call = null; + } + } + + private Call call; + private Builder builder; + } + + public interface UpdateSuccessCallback { + void handleServerPropertyUpdate(ServerProperties props); + } + public interface UpdateFailureCallback { + void handleUpdateFailure(Request request, int statusCode, Throwable error); + } + + public abstract int flags(); + public abstract List<OpenHABSitemap> sitemaps(); + + public boolean hasJsonApi() { + return (flags() & SERVER_FLAG_JSON_REST_API) != 0; + } + + public boolean hasSseSupport() { + return (flags() & SERVER_FLAG_SSE_SUPPORT) != 0; + } + + abstract Builder toBuilder(); + + @AutoValue.Builder + static abstract class Builder { + abstract Builder flags(int flags); + abstract Builder sitemaps(List<OpenHABSitemap> sitemaps); + + abstract ServerProperties build(); + abstract int flags(); + } + + public static UpdateHandle updateSitemaps(ServerProperties props, Connection connection, + UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) { + UpdateHandle handle = new UpdateHandle(); + handle.builder = props.toBuilder(); + fetchSitemaps(connection.getAsyncHttpClient(), handle, successCb, failureCb); + return handle; + } + + public static UpdateHandle fetch(Connection connection, + UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) { + final UpdateHandle handle = new UpdateHandle(); + handle.builder = new AutoValue_ServerProperties.Builder(); + fetchFlags(connection.getAsyncHttpClient(), handle, successCb, failureCb); + return handle; + } + + private static void fetchFlags(AsyncHttpClient client, UpdateHandle handle, + UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) { + handle.call = client.get("rest", new AsyncHttpClient.StringResponseHandler() { + @Override + public void onFailure(Request request, int statusCode, Throwable error) { + failureCb.handleUpdateFailure(request, statusCode, error); + } + + @Override + public void onSuccess(String response, Headers headers) { + try { + JSONObject result = new JSONObject(response); + // If this succeeded, we're talking to OH2 + int flags = SERVER_FLAG_JSON_REST_API + | SERVER_FLAG_ICON_FORMAT_SUPPORT + | SERVER_FLAG_CHART_SCALING_SUPPORT; + try { + String versionString = result.getString("version"); + int versionNumber = Integer.parseInt(versionString); + // all versions that return a number here have full SSE support + flags |= SERVER_FLAG_SSE_SUPPORT; + } catch (NumberFormatException nfe) { + // ignored: older versions without SSE support didn't return a number + } + handle.builder.flags(flags); + fetchSitemaps(client, handle, successCb, failureCb); + } catch (JSONException e) { + if (response.startsWith("<?xml")) { + // We're talking to an OH1 instance + handle.builder.flags(0); + fetchSitemaps(client, handle, successCb, failureCb); + } else { + failureCb.handleUpdateFailure(handle.call.request(), 200, e); + } + } + } + }); + } + + private static void fetchSitemaps(AsyncHttpClient client, UpdateHandle handle, + UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) { + handle.call = client.get("rest/sitemaps", new AsyncHttpClient.StringResponseHandler() { + @Override + public void onFailure(Request request, int statusCode, Throwable error) { + failureCb.handleUpdateFailure(request, statusCode, error); + } + + @Override + public void onSuccess(String response, Headers headers) { + // OH1 returns XML, later versions return JSON + List<OpenHABSitemap> result = (handle.builder.flags() & SERVER_FLAG_JSON_REST_API) != 0 + ? loadSitemapsFromJson(response) + : loadSitemapsFromXml(response); + Log.d(TAG, "Server returned sitemaps: " + result); + handle.builder.sitemaps(result != null ? result : new ArrayList<>()); + successCb.handleServerPropertyUpdate(handle.builder.build()); + } + }); + } + + private static List<OpenHABSitemap> loadSitemapsFromXml(String response) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + try { + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document sitemapsXml = builder.parse(new InputSource(new StringReader(response))); + return Util.parseSitemapList(sitemapsXml); + } catch (ParserConfigurationException | SAXException | IOException e) { + Log.e(TAG, "Failed parsing sitemap XML", e); + return null; + } + } + + private static List<OpenHABSitemap> loadSitemapsFromJson(String response) { + try { + JSONArray jsonArray = new JSONArray(response); + return Util.parseSitemapList(jsonArray); + } catch (JSONException e) { + Log.e(TAG, "Failed parsing sitemap JSON", e); + return null; + } + } +} diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java index 8327ffe0e5148df5196e51fa0ae1fc2345daa13a..20749038c4c8d2cb7d2c7d7038f8122c970f336d 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java @@ -33,6 +33,7 @@ import org.openhab.habdroid.core.CloudMessagingHelper; import org.openhab.habdroid.core.connection.Connection; import org.openhab.habdroid.core.connection.ConnectionFactory; import org.openhab.habdroid.core.connection.exception.ConnectionException; +import org.openhab.habdroid.model.ServerProperties; import org.openhab.habdroid.util.SyncHttpClient; import org.openhab.habdroid.util.Util; @@ -104,14 +105,14 @@ public class AboutActivity extends AppCompatActivity implements public static class AboutMainFragment extends MaterialAboutFragment { private final static String TAG = AboutMainFragment.class.getSimpleName(); private final static String URL_TO_GITHUB = "https://github.com/openhab/openhab-android"; - private int mOpenHABVersion; + private ServerProperties mServerProperties; private Connection mConnection; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - mOpenHABVersion = getArguments().getInt("openHABVersion", 0); + mServerProperties = getArguments().getParcelable("serverProperties"); try { mConnection = ConnectionFactory.getUsableConnection(); } catch (ConnectionException ignored) {} @@ -187,7 +188,7 @@ public class AboutActivity extends AppCompatActivity implements MaterialAboutCard.Builder ohServerCard = new MaterialAboutCard.Builder(); ohServerCard.title(R.string.about_server); - if (mConnection == null || mOpenHABVersion == 0) { + if (mConnection == null || mServerProperties == null) { ohServerCard.addItem(new MaterialAboutActionItem.Builder() .text(R.string.error_about_no_conn) .icon(R.drawable.ic_info_outline) @@ -213,7 +214,7 @@ public class AboutActivity extends AppCompatActivity implements .icon(R.drawable.ic_info_outline) .build()); - if (mOpenHABVersion == 1) { + if (!useJsonApi()) { String secret = getServerSecret(); if (!TextUtils.isEmpty(secret)) { ohServerCard.addItem(new MaterialAboutActionItem.Builder() @@ -289,8 +290,12 @@ public class AboutActivity extends AppCompatActivity implements } } + private boolean useJsonApi() { + return mServerProperties != null && mServerProperties.hasJsonApi(); + } + private String getServerUuid() { - final String uuidUrl = mOpenHABVersion == 1 ? "static/uuid" : "rest/uuid"; + final String uuidUrl = useJsonApi() ? "rest/uuid" : "static/uuid"; SyncHttpClient.HttpTextResult result = mConnection.getSyncHttpClient().get(uuidUrl).asText(); if (result.isSuccessful()) { @@ -303,14 +308,14 @@ public class AboutActivity extends AppCompatActivity implements } private String getApiVersion() { - String versionUrl = mOpenHABVersion == 1 ? "static/version" : "rest"; + String versionUrl = useJsonApi() ? "rest" : "static/version"; Log.d(TAG, "url = " + versionUrl); SyncHttpClient.HttpTextResult result = mConnection.getSyncHttpClient().get(versionUrl).asText(); if (!result.isSuccessful()) { Log.e(TAG, "Could not fetch rest API version " + result.error); } else { - if (mOpenHABVersion == 1) { + if (!useJsonApi()) { return result.response; } else { try { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java index b2c6c8d334b188544120e91ca18de689979f8f7d..ce293a6b33f35f38d362c12db7582e627f92c67c 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java @@ -62,8 +62,6 @@ import android.view.WindowManager; import android.widget.ProgressBar; import android.widget.Toast; -import org.json.JSONArray; -import org.json.JSONException; import org.openhab.habdroid.R; import org.openhab.habdroid.core.CloudMessagingHelper; import org.openhab.habdroid.core.OnUpdateBroadcastReceiver; @@ -77,17 +75,14 @@ import org.openhab.habdroid.core.connection.exception.NetworkNotSupportedExcepti import org.openhab.habdroid.core.connection.exception.NoUrlInformationException; import org.openhab.habdroid.model.OpenHABLinkedPage; import org.openhab.habdroid.model.OpenHABSitemap; +import org.openhab.habdroid.model.ServerProperties; import org.openhab.habdroid.ui.activity.ContentController; import org.openhab.habdroid.util.AsyncServiceResolver; import org.openhab.habdroid.util.Constants; import org.openhab.habdroid.util.AsyncHttpClient; import org.openhab.habdroid.util.Util; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; import java.io.IOException; -import java.io.StringReader; import java.lang.reflect.Constructor; import java.net.ConnectException; import java.net.SocketTimeoutException; @@ -97,23 +92,18 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateRevokedException; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.jmdns.ServiceInfo; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import es.dmoral.toasty.Toasty; import okhttp3.Call; import okhttp3.Headers; import okhttp3.Request; -import static org.openhab.habdroid.ui.OpenHABPreferencesActivity.START_EXTRA_OPENHAB_VERSION; import static org.openhab.habdroid.util.Util.exceptionHasCause; import static org.openhab.habdroid.util.Util.getHostFromUrl; @@ -133,12 +123,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements // Drawer item codes private static final int GROUP_ID_SITEMAPS = 1; - private enum InitState { - QUERY_SERVER_PROPS, - LOAD_SITEMAPS, - DONE - } - // preferences private SharedPreferences mSettings; private AsyncServiceResolver mServiceResolver; @@ -151,8 +135,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements private Menu mDrawerMenu; private ColorStateList mDrawerIconTintList; private RecyclerView.RecycledViewPool mViewPool; - private ArrayList<OpenHABSitemap> mSitemapList; - private int mOpenHABVersion; private ProgressBar mProgressBar; // select sitemap dialog private Dialog mSelectSitemapDialog; @@ -163,8 +145,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements private String mPendingOpenedNotificationId; private OpenHABSitemap mSelectedSitemap; private ContentController mController; - private InitState mInitState = InitState.QUERY_SERVER_PROPS; - private Call mPendingCall; + private ServerProperties mServerProperties; + private ServerProperties.UpdateHandle mPropsUpdateHandle; private boolean mStarted; /** @@ -229,10 +211,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements // Check if we have openHAB page url in saved instance state? if (savedInstanceState != null) { - mOpenHABVersion = savedInstanceState.getInt("openHABVersion"); - mSitemapList = savedInstanceState.getParcelableArrayList("sitemapList"); + mServerProperties = savedInstanceState.getParcelable("serverProperties"); mSelectedSitemap = savedInstanceState.getParcelable("sitemap"); - mInitState = InitState.values()[savedInstanceState.getInt("initState")]; int lastConnectionHash = savedInstanceState.getInt("connectionHash"); if (lastConnectionHash != -1) { try { @@ -257,7 +237,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements showSitemapSelectionDialog(); } } else { - mSitemapList = new ArrayList<>(); } processIntent(getIntent()); @@ -304,35 +283,31 @@ public class OpenHABMainActivity extends AppCompatActivity implements public void retryServerPropertyQuery() { mController.clearServerCommunicationFailure(); - if (mPendingCall != null) { - mPendingCall.cancel(); - } queryServerProperties(); } private void queryServerProperties() { - final String url = "rest/bindings"; - mInitState = InitState.QUERY_SERVER_PROPS; - mPendingCall = mConnection.getAsyncHttpClient().get(url, new AsyncHttpClient.StringResponseHandler() { - @Override - public void onFailure(Request request, int statusCode, Throwable error) { - if (statusCode == 404 && mConnection != null) { - // no bindings endpoint; we're likely talking to an OH1 instance - mOpenHABVersion = 1; - loadSitemapList(true); + if (mPropsUpdateHandle != null) { + mPropsUpdateHandle.cancel(); + } + ServerProperties.UpdateSuccessCallback successCb = props -> { + mServerProperties = props; + updateSitemapDrawerItems(); + if (props.sitemaps().isEmpty()) { + Log.e(TAG, "openHAB returned empty sitemap list"); + mController.indicateServerCommunicationFailure( + getString(R.string.error_empty_sitemap_list)); + } else { + OpenHABSitemap sitemap = selectConfiguredSitemapFromList(); + if (sitemap != null) { + openSitemap(sitemap); } else { - // other error -> use default handling - handleServerCommunicationFailure(request, statusCode, error); + showSitemapSelectionDialog(); } } - - @Override - public void onSuccess(String response, Headers headers) { - mOpenHABVersion = 2; - Log.d(TAG, "openHAB version 2"); - loadSitemapList(true); - } - }); + }; + mPropsUpdateHandle = ServerProperties.fetch(mConnection, + successCb, this::handlePropertyFetchFailure); } @Override @@ -435,7 +410,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements mConnection = newConnection; hideSnackbar(); - mSitemapList.clear(); + mServerProperties = null; mSelectedSitemap = null; // Handle pending NFC tag if initial connection determination finished @@ -495,14 +470,9 @@ public class OpenHABMainActivity extends AppCompatActivity implements onAvailableConnectionChanged(); updateNotificationDrawerItem(); - if (mConnection != null) { - if (mInitState == InitState.QUERY_SERVER_PROPS) { - mController.clearServerCommunicationFailure(); - queryServerProperties(); - } else if (mInitState == InitState.LOAD_SITEMAPS) { - mController.clearServerCommunicationFailure(); - loadSitemapList(true); - } + if (mConnection != null && mServerProperties == null) { + mController.clearServerCommunicationFailure(); + queryServerProperties(); } openPendingNfcPageIfNeeded(); openNotificationsPageIfNeeded(); @@ -521,8 +491,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements if (mSelectSitemapDialog != null && mSelectSitemapDialog.isShowing()) { mSelectSitemapDialog.dismiss(); } - if (mPendingCall != null) { - mPendingCall.cancel(); + if (mPropsUpdateHandle != null) { + mPropsUpdateHandle.cancel(); } } @@ -553,8 +523,10 @@ public class OpenHABMainActivity extends AppCompatActivity implements mDrawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { @Override public void onDrawerOpened(View drawerView) { - if (mInitState == InitState.DONE) { - loadSitemapList(false); + if (mServerProperties != null && mPropsUpdateHandle == null) { + mPropsUpdateHandle = ServerProperties.updateSitemaps(mServerProperties, mConnection, + props -> { mServerProperties = props; updateSitemapDrawerItems(); }, + OpenHABMainActivity.this::handlePropertyFetchFailure); } } }); @@ -585,7 +557,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements case R.id.settings: Intent settingsIntent = new Intent(OpenHABMainActivity.this, OpenHABPreferencesActivity.class); - settingsIntent.putExtra(START_EXTRA_OPENHAB_VERSION, getOpenHABVersion()); + settingsIntent.putExtra(OpenHABPreferencesActivity.START_EXTRA_SERVER_PROPERTIES, + mServerProperties); startActivityForResult(settingsIntent, SETTINGS_REQUEST_CODE); return true; case R.id.about: @@ -593,7 +566,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements return true; } if (item.getGroupId() == GROUP_ID_SITEMAPS) { - OpenHABSitemap sitemap = mSitemapList.get(item.getItemId()); + OpenHABSitemap sitemap = mServerProperties.sitemaps().get(item.getItemId()); openSitemap(sitemap); return true; } @@ -609,18 +582,26 @@ public class OpenHABMainActivity extends AppCompatActivity implements private void updateSitemapDrawerItems() { MenuItem sitemapItem = mDrawerMenu.findItem(R.id.sitemaps); - - if (mSitemapList.isEmpty()) { + if (mServerProperties == null) { sitemapItem.setVisible(false); } else { - sitemapItem.setVisible(true); - SubMenu menu = sitemapItem.getSubMenu(); - menu.clear(); - - for (int i = 0; i < mSitemapList.size(); i++) { - OpenHABSitemap sitemap = mSitemapList.get(i); - MenuItem item = menu.add(GROUP_ID_SITEMAPS, i, i, sitemap.label()); - loadSitemapIcon(sitemap, item); + final String defaultSitemapName = + mSettings.getString(Constants.PREFERENCE_SITEMAP_NAME, ""); + final List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps(); + Util.sortSitemapList(sitemaps, defaultSitemapName); + + if (sitemaps.isEmpty()) { + sitemapItem.setVisible(false); + } else { + sitemapItem.setVisible(true); + SubMenu menu = sitemapItem.getSubMenu(); + menu.clear(); + + for (int i = 0; i < sitemaps.size(); i++) { + OpenHABSitemap sitemap = sitemaps.get(i); + MenuItem item = menu.add(GROUP_ID_SITEMAPS, i, i, sitemap.label()); + loadSitemapIcon(sitemap, item); + } } } } @@ -693,103 +674,25 @@ public class OpenHABMainActivity extends AppCompatActivity implements private void openAbout() { Intent aboutIntent = new Intent(this, AboutActivity.class); - aboutIntent.putExtra("openHABVersion", mOpenHABVersion); + aboutIntent.putExtra("serverProperties", mServerProperties); startActivityForResult(aboutIntent, INFO_REQUEST_CODE); Util.overridePendingTransition(this, false); } - /** - * Get sitemaps from openHAB. If user already configured preferred sitemap - * just open it. If no preferred sitemap is configured let user select one. - */ - - private void loadSitemapList(final boolean selectSitemapAfterLoad) { - if (mConnection == null) { - return; - } - - Log.d(TAG, "Loading sitemap list from /rest/sitemaps"); - - mInitState = InitState.LOAD_SITEMAPS; - mPendingCall = mConnection.getAsyncHttpClient().get("rest/sitemaps", new AsyncHttpClient.StringResponseHandler() { - @Override - public void onFailure(Request request, int statusCode, Throwable error) { - handleServerCommunicationFailure(request, statusCode, error); - } - - @Override - public void onSuccess(String response, Headers headers) { - mPendingCall = null; - mInitState = InitState.DONE; - - // OH1 returns XML, later versions return JSON - List<OpenHABSitemap> result = mOpenHABVersion == 1 - ? loadSitemapsFromXml(response) - : loadSitemapsFromJson(response); - Log.d(TAG, "Server returned sitemaps: " + result); - mSitemapList.clear(); - if (result != null) { - String defaultSitemapName = - mSettings.getString(Constants.PREFERENCE_SITEMAP_NAME, ""); - mSitemapList.addAll(Util.sortSitemapList(result, defaultSitemapName)); - } - updateSitemapDrawerItems(); - - if (!selectSitemapAfterLoad) { - return; - } - - if (mSitemapList.isEmpty()) { - Log.e(TAG, "openHAB returned empty sitemap list"); - mController.indicateServerCommunicationFailure( - getString(R.string.error_empty_sitemap_list)); - } else { - OpenHABSitemap sitemap = selectConfiguredSitemapFromList(); - if (sitemap != null) { - openSitemap(sitemap); - } else { - showSitemapSelectionDialog(); - } - } - } - }); - } - - private static List<OpenHABSitemap> loadSitemapsFromXml(String response) { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - try { - DocumentBuilder builder = dbf.newDocumentBuilder(); - Document sitemapsXml = builder.parse(new InputSource(new StringReader(response))); - return Util.parseSitemapList(sitemapsXml); - } catch (ParserConfigurationException | SAXException | IOException e) { - Log.e(TAG, "Failed parsing sitemap XML", e); - return null; - } - } - - private static List<OpenHABSitemap> loadSitemapsFromJson(String response) { - try { - JSONArray jsonArray = new JSONArray(response); - return Util.parseSitemapList(jsonArray); - } catch (JSONException e) { - Log.e(TAG, "Failed parsing sitemap JSON", e); - return null; - } - } - private OpenHABSitemap selectConfiguredSitemapFromList() { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); String configuredSitemap = settings.getString(Constants.PREFERENCE_SITEMAP_NAME, ""); + List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps(); final OpenHABSitemap result; - if (mSitemapList.size() == 1) { + if (sitemaps.size() == 1) { // We only have one sitemap, use it - result = mSitemapList.get(0); + result = sitemaps.get(0); } else if (!configuredSitemap.isEmpty()) { // Select configured sitemap if still present, nothing otherwise - result = Util.getSitemapByName(mSitemapList, configuredSitemap); + result = Util.getSitemapByName(sitemaps, configuredSitemap); } else { // Nothing configured -> can't auto-select anything result = null; @@ -824,16 +727,17 @@ public class OpenHABMainActivity extends AppCompatActivity implements return; } - final String[] sitemapLabels = new String[mSitemapList.size()]; - for (int i = 0; i < mSitemapList.size(); i++) { - sitemapLabels[i] = mSitemapList.get(i).label(); + List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps(); + final String[] sitemapLabels = new String[sitemaps.size()]; + for (int i = 0; i < sitemaps.size(); i++) { + sitemapLabels[i] = sitemaps.get(i).label(); } mSelectSitemapDialog = new AlertDialog.Builder(this) .setTitle(R.string.mainmenu_openhab_selectsitemap) .setItems(sitemapLabels, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int item) { - OpenHABSitemap sitemap = mSitemapList.get(item); + OpenHABSitemap sitemap = sitemaps.get(item); Log.d(TAG, "Selected sitemap " + sitemap); PreferenceManager.getDefaultSharedPreferences(OpenHABMainActivity.this) .edit() @@ -937,15 +841,13 @@ public class OpenHABMainActivity extends AppCompatActivity implements // Save UI state changes to the savedInstanceState. // This bundle will be passed to onCreate if the process is // killed and restarted. - savedInstanceState.putInt("openHABVersion", mOpenHABVersion); - savedInstanceState.putParcelableArrayList("sitemapList", mSitemapList); + savedInstanceState.putParcelable("serverProperties", mServerProperties); savedInstanceState.putParcelable("sitemap", mSelectedSitemap); savedInstanceState.putBoolean("isSitemapSelectionDialogShown", mSelectSitemapDialog != null && mSelectSitemapDialog.isShowing()); savedInstanceState.putString("controller", mController.getClass().getCanonicalName()); savedInstanceState.putInt("connectionHash", mConnection != null ? mConnection.hashCode() : -1); - savedInstanceState.putInt("initState", mInitState.ordinal()); mController.onSaveInstanceState(savedInstanceState); super.onSaveInstanceState(savedInstanceState); } @@ -1048,7 +950,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements } } - private void handleServerCommunicationFailure(Request request, int statusCode, Throwable error) { + private void handlePropertyFetchFailure(Request request, int statusCode, Throwable error) { Log.e(TAG, "Error: " + error.toString()); Log.e(TAG, "HTTP status code: " + statusCode); CharSequence message; @@ -1125,16 +1027,15 @@ public class OpenHABMainActivity extends AppCompatActivity implements } mController.indicateServerCommunicationFailure(message); - mPendingCall = null; - mInitState = InitState.DONE; + mPropsUpdateHandle = null; } public boolean isStarted() { return mStarted; } - public int getOpenHABVersion() { - return mOpenHABVersion; + public ServerProperties getServerProperties() { + return mServerProperties; } public Connection getConnection() { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java index f1ee9c03c324d5345abe4cba181e686952846149..3ef3091ee760d53c45efad60832148341a83aaf3 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java @@ -38,6 +38,7 @@ import android.view.MenuItem; import org.openhab.habdroid.R; import org.openhab.habdroid.core.CloudMessagingHelper; +import org.openhab.habdroid.model.ServerProperties; import org.openhab.habdroid.util.CacheManager; import org.openhab.habdroid.util.Constants; import org.openhab.habdroid.util.Util; @@ -52,12 +53,11 @@ import static org.openhab.habdroid.util.Util.getHostFromUrl; public class OpenHABPreferencesActivity extends AppCompatActivity { public static final String RESULT_EXTRA_THEME_CHANGED = "theme_changed"; public static final String RESULT_EXTRA_SITEMAP_CLEARED = "sitemap_cleared"; - public static final String START_EXTRA_OPENHAB_VERSION = "openhab_version"; + public static final String START_EXTRA_SERVER_PROPERTIES = "server_properties"; private static final String STATE_KEY_RESULT = "result"; private static final String TAG = OpenHABPreferencesActivity.class.getSimpleName(); private Intent mResultIntent; - private static int mOpenhabVersion; @Override public void onCreate(Bundle savedInstanceState) { @@ -80,8 +80,6 @@ public class OpenHABPreferencesActivity extends AppCompatActivity { mResultIntent = savedInstanceState.getParcelable(STATE_KEY_RESULT); } setResult(RESULT_OK, mResultIntent); - - mOpenhabVersion = getIntent().getIntExtra(START_EXTRA_OPENHAB_VERSION, 0); } @Override @@ -343,11 +341,14 @@ public class OpenHABPreferencesActivity extends AppCompatActivity { getParent(vibrationPreference).removePreference(vibrationPreference); } - if (mOpenhabVersion == 1) { - Log.d(TAG, "Removing prefs that aren't available in openHAB 1"); + ServerProperties props = + getActivity().getIntent().getParcelableExtra(START_EXTRA_SERVER_PROPERTIES); + if (props != null && (props.flags() & ServerProperties.SERVER_FLAG_ICON_FORMAT_SUPPORT) == 0) { Preference iconFormatPreference = ps.findPreference(Constants.PREFERENCE_ICON_FORMAT); getParent(iconFormatPreference).removePreference(iconFormatPreference); + } + if (props != null && (props.flags() & ServerProperties.SERVER_FLAG_CHART_SCALING_SUPPORT) == 0) { Preference chartScalingPreference = ps.findPreference(Constants.PREFERENCE_CHART_SCALING); getParent(chartScalingPreference).removePreference(chartScalingPreference); diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java index 992038a3176c5e70ff53ff47981d32f1521320fb..b514380b8d106e70e1bc5f3aecf61576a0e0b22d 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java @@ -148,7 +148,8 @@ public class OpenHABWidgetAdapter extends RecyclerView.Adapter<OpenHABWidgetAdap if (compatibleUpdate) { for (int i = 0; i < widgets.size(); i++) { if (!mItems.get(i).equals(widgets.get(i))) { - updateAtPosition(i, widgets.get(i)); + mItems.set(i, widgets.get(i)); + notifyItemChanged(i); } } } else { @@ -158,12 +159,14 @@ public class OpenHABWidgetAdapter extends RecyclerView.Adapter<OpenHABWidgetAdap } } - public void updateAtPosition(int position, OpenHABWidget widget) { - if (position >= mItems.size()) { - return; + public void updateWidget(OpenHABWidget widget) { + for (int i = 0; i < mItems.size(); i++) { + if (mItems.get(i).id().equals(widget.id())) { + mItems.set(i, widget); + notifyItemChanged(i); + break; + } } - mItems.set(position, widget); - notifyItemChanged(position); } @Override diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java index 8a3c0f48de85637195c6d481ab97ba1c3bb594ed..a2e51ed4562c27fd1db3cd524e58af41fd2d2802 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java @@ -58,7 +58,6 @@ public class OpenHABWidgetListFragment extends Fragment // Am I visible? private boolean mIsVisible = false; private String mTitle; - private List<OpenHABWidget> mWidgets; private SwipeRefreshLayout refreshLayout; private String mHighlightedPageLink; @@ -100,10 +99,6 @@ public class OpenHABWidgetListFragment extends Fragment mRecyclerView.addItemDecoration(new OpenHABWidgetAdapter.WidgetItemDecoration(mActivity)); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setAdapter(openHABWidgetAdapter); - - if (mWidgets != null) { - update(mTitle, mWidgets); - } } @Override @@ -290,7 +285,6 @@ public class OpenHABWidgetListFragment extends Fragment public void update(String pageTitle, List<OpenHABWidget> widgets) { mTitle = pageTitle; - mWidgets = widgets; if (openHABWidgetAdapter != null) { openHABWidgetAdapter.update(widgets, refreshLayout.isRefreshing()); @@ -302,19 +296,10 @@ public class OpenHABWidgetListFragment extends Fragment } } - public boolean onWidgetUpdated(OpenHABWidget widget) { - if (mWidgets != null) { - for (int i = 0; i < mWidgets.size(); i++) { - if (mWidgets.get(i).id().equals(widget.id())) { - mWidgets.set(i, widget); - if (openHABWidgetAdapter != null) { - openHABWidgetAdapter.updateAtPosition(i, widget); - } - return true; - } - } + public void updateWidget(OpenHABWidget widget) { + if (openHABWidgetAdapter != null) { + openHABWidgetAdapter.updateWidget(widget); } - return false; } public String getDisplayPageUrl() { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java index 8bb84ad3f214ef1fd2af7d3b492f31fd4c031b16..d32db36625e71cd8638cc4bb41aac03ddc286820 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java @@ -42,6 +42,7 @@ import org.openhab.habdroid.core.connection.ConnectionFactory; import org.openhab.habdroid.model.OpenHABLinkedPage; import org.openhab.habdroid.model.OpenHABSitemap; import org.openhab.habdroid.model.OpenHABWidget; +import org.openhab.habdroid.model.ServerProperties; import org.openhab.habdroid.ui.OpenHABMainActivity; import org.openhab.habdroid.ui.OpenHABNotificationFragment; import org.openhab.habdroid.ui.OpenHABPreferencesActivity; @@ -386,7 +387,14 @@ public abstract class ContentController implements PageConnectionHolderFragment. @Override public boolean serverReturnsJson() { - return mActivity.getOpenHABVersion() != 1; + ServerProperties props = mActivity.getServerProperties(); + return props != null && props.hasJsonApi(); + } + + @Override + public boolean serverSupportsSse() { + ServerProperties props = mActivity.getServerProperties(); + return props != null && props.hasSseSupport(); } @Override @@ -413,9 +421,10 @@ public abstract class ContentController implements PageConnectionHolderFragment. } @Override - public void onWidgetUpdated(OpenHABWidget widget) { + public void onWidgetUpdated(String pageUrl, OpenHABWidget widget) { for (OpenHABWidgetListFragment f : collectWidgetFragments()) { - if (f.onWidgetUpdated(widget)) { + if (pageUrl.equals(f.getDisplayPageUrl())) { + f.updateWidget(widget); break; } } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java b/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java index db5ccbbd2efa5c37a1f88fb133e2ad241d3a294d..f6577ec5b6a635bc1747b0aa833958eff3824e50 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java @@ -1,11 +1,16 @@ package org.openhab.habdroid.ui.activity; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.util.Log; +import com.here.oksse.ServerSentEvent; + import org.json.JSONException; import org.json.JSONObject; import org.openhab.habdroid.core.connection.Connection; @@ -31,7 +36,9 @@ import javax.xml.parsers.ParserConfigurationException; import okhttp3.Call; import okhttp3.Headers; +import okhttp3.HttpUrl; import okhttp3.Request; +import okhttp3.Response; /** * Fragment that manages connections for active instances of @@ -46,29 +53,41 @@ public class PageConnectionHolderFragment extends Fragment { public interface ParentCallback { /** * Ask parent whether server returns JSON or XML + * * @return true if server returns JSON, false if it returns XML */ boolean serverReturnsJson(); + /** + * Ask parent whether server has support for Server Sent Events + * + * @return true if server supports SSE, false otherwise + */ + boolean serverSupportsSse(); + /** * Ask parent for the icon format to use + * * @return Icon format ('PNG' or 'SVG') */ String getIconFormat(); /** * Let parent know about an update to the widget list for a given URL. - * @param pageUrl URL of the updated page + * + * @param pageUrl URL of the updated page * @param pageTitle Updated page title - * @param widgets Updated list of widgets for the given page + * @param widgets Updated list of widgets for the given page */ void onPageUpdated(String pageUrl, String pageTitle, List<OpenHABWidget> widgets); /** * Let parent know about an update to the contents of a single widget. - * @param widget Updated widget + * + * @param pageUrl URL of the page the updated widget belongs to + * @param widget Updated widget */ - void onWidgetUpdated(OpenHABWidget widget); + void onWidgetUpdated(String pageUrl, OpenHABWidget widget); } private Map<String, ConnectionHandler> mConnections = new HashMap<>(); @@ -110,7 +129,7 @@ public class PageConnectionHolderFragment extends Fragment { /** * Assign parent callback - * + * <p> * To be called by the parent as early as possible, * as it's expected to be non-null at all times * @@ -126,7 +145,7 @@ public class PageConnectionHolderFragment extends Fragment { /** * Update list of page URLs to track * - * @param urls New list of URLs to track + * @param urls New list of URLs to track * @param connection Connection to use, or null if none is available */ public void updateActiveConnections(List<String> urls, Connection connection) { @@ -166,7 +185,7 @@ public class PageConnectionHolderFragment extends Fragment { /** * Ask for new data to be delivered for a given page * - * @param pageUrl URL of page to trigger update for + * @param pageUrl URL of page to trigger update for * @param forceReload true if existing data should be discarded and new data be loaded, * false if only existing data should be delivered, if it exists */ @@ -192,11 +211,22 @@ public class PageConnectionHolderFragment extends Fragment { private String mAtmosphereTrackingId; private String mLastPageTitle; private List<OpenHABWidget> mLastWidgetList; + private EventHelper mEventHelper; public ConnectionHandler(String pageUrl, Connection connection, ParentCallback cb) { mUrl = pageUrl; mHttpClient = connection.getAsyncHttpClient(); mCallback = cb; + if (cb.serverSupportsSse()) { + Uri uri = Uri.parse(mUrl); + List<String> segments = uri.getPathSegments(); + if (segments.size() > 2) { + String sitemap = segments.get(segments.size() - 2); + String pageId = segments.get(segments.size() - 1); + mEventHelper = new EventHelper(mHttpClient, sitemap, pageId, + this::handleUpdateEvent, this::handleSseSubscriptionFailure); + } + } } public boolean updateFromConnection(Connection c) { @@ -211,6 +241,9 @@ public class PageConnectionHolderFragment extends Fragment { mRequestHandle.cancel(); mRequestHandle = null; } + if (mEventHelper != null) { + mEventHelper.shutdown(); + } mLongPolling = false; } @@ -225,6 +258,11 @@ public class PageConnectionHolderFragment extends Fragment { } private void load() { + if (mEventHelper != null && mLongPolling) { + // We update via events + return; + } + Log.d(TAG, "Loading data for " + mUrl); Map<String, String> headers = new HashMap<String, String>(); if (!mCallback.serverReturnsJson()) { @@ -246,6 +284,9 @@ public class PageConnectionHolderFragment extends Fragment { } final long timeoutMillis = mLongPolling ? 300000 : 10000; mRequestHandle = mHttpClient.get(mUrl, headers, timeoutMillis, this); + if (mEventHelper != null) { + mEventHelper.connect(); + } } @Override @@ -339,5 +380,155 @@ public class PageConnectionHolderFragment extends Fragment { return false; } } + + boolean handleUpdateEvent(String payload) { + if (mLastWidgetList == null) { + return false; + } + try { + JSONObject object = new JSONObject(payload); + String widgetId = object.getString("widgetId"); + for (int i = 0; i < mLastWidgetList.size(); i++) { + OpenHABWidget widget = mLastWidgetList.get(i); + if (widgetId.equals(widget.id())) { + OpenHABWidget updatedWidget = OpenHABWidget.updateFromEvent(widget, + object, mCallback.getIconFormat()); + mLastWidgetList.set(i, updatedWidget); + mCallback.onWidgetUpdated(mUrl, updatedWidget); + return true; + } + } + } catch (JSONException e) { + Log.w(TAG, "Could not parse SSE event ('" + payload + "')", e); + } + return false; + } + + void handleSseSubscriptionFailure() { + mEventHelper = null; + if (mLongPolling) { + load(); + } + } + + private static class EventHelper implements ServerSentEvent.Listener { + interface FailureCallback { + void handleFailure(); + } + interface UpdateCallback { + void handleUpdateEvent(String message); + } + + private static final int MAX_RETRIES = 10; + + private final AsyncHttpClient mClient; + private final UpdateCallback mUpdateCb; + private final FailureCallback mFailureCb; + private final String mSitemap; + private final String mPageId; + private final Handler mHandler; + private Call mSubscribeHandle; + private ServerSentEvent mEventStream; + private int mRetries; + + EventHelper(AsyncHttpClient client, String sitemap, String pageId, + UpdateCallback updateCb, FailureCallback failureCb) { + mClient = client; + mUpdateCb = updateCb; + mFailureCb = failureCb; + mSitemap = sitemap; + mPageId = pageId; + mHandler = new Handler(Looper.getMainLooper()); + } + + void connect() { + shutdown(); + + mSubscribeHandle = mClient.post("/rest/sitemaps/events/subscribe", + "{}", "application/json", new AsyncHttpClient.StringResponseHandler() { + @Override + public void onFailure(Request request, int statusCode, Throwable error) { + if (statusCode == 404) { + Log.d(TAG, "Server does not have SSE support"); + } else { + Log.w(TAG, "Failed subscribing for SSE", error); + } + mFailureCb.handleFailure(); + } + + @Override + public void onSuccess(String body, Headers headers) { + try { + JSONObject result = new JSONObject(body); + String status = result.getString("status"); + if (!status.equals("CREATED")) { + throw new JSONException("Unexpected status " + status); + } + JSONObject headerObject = result.getJSONObject("context").getJSONObject("headers"); + String url = headerObject.getJSONArray("Location").getString(0); + HttpUrl u = HttpUrl.parse(url).newBuilder() + .addQueryParameter("sitemap", mSitemap) + .addQueryParameter("pageid", mPageId) + .build(); + Request request = new Request.Builder() + .url(u) + .build(); + mEventStream = mClient.makeSseClient() + .newServerSentEvent(request, EventHelper.this); + } catch (JSONException e) { + Log.w(TAG, "Failed parsing SSE subscription", e); + mFailureCb.handleFailure(); + } + } + }); + } + + void shutdown() { + if (mEventStream != null) { + mEventStream.close(); + mEventStream = null; + } + if (mSubscribeHandle != null) { + mSubscribeHandle.cancel(); + mSubscribeHandle = null; + } + } + + + @Override + public void onOpen(ServerSentEvent sse, Response response) { + mRetries = 0; + } + + @Override + public void onMessage(ServerSentEvent sse, String id, String event, String message) { + mHandler.post(() -> mUpdateCb.handleUpdateEvent(message)); + } + + @Override + public void onComment(ServerSentEvent sse, String comment) { + } + + @Override + public boolean onRetryTime(ServerSentEvent sse, long milliseconds) { + return true; + } + + @Override + public boolean onRetryError(ServerSentEvent sse, Throwable throwable, Response response) { + // Stop retrying after maximum amount of subsequent retries is reached + return ++mRetries < MAX_RETRIES; + } + + @Override + public void onClosed(ServerSentEvent sse) { + mFailureCb.handleFailure(); + } + + @Override + public Request onPreRetry(ServerSentEvent sse, Request originalRequest) { + return originalRequest; + } + } } } diff --git a/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java b/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java index 1e4d85828be6f4ccc9c3acdc737191f2825369d2..1075078dd60a0af8efd016454d4dd51eaa33f52b 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java @@ -11,6 +11,8 @@ package org.openhab.habdroid.util; import android.support.annotation.VisibleForTesting; +import com.here.oksse.OkSse; + import java.util.Map; import java.util.concurrent.TimeUnit; @@ -44,6 +46,10 @@ public abstract class HttpClient { mClient = client; } + public OkSse makeSseClient() { + return new OkSse(mClient); + } + public HttpUrl buildUrl(String url) { HttpUrl absoluteUrl = HttpUrl.parse(url); if (absoluteUrl == null && mBaseUrl != null) { diff --git a/mobile/src/main/java/org/openhab/habdroid/util/Util.java b/mobile/src/main/java/org/openhab/habdroid/util/Util.java index f71ed882e488c550cb14374cbb4008df9f6adc46..e56f53271d3581398b36e5479a14afc0954e1b84 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/Util.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/Util.java @@ -110,7 +110,7 @@ public class Util { return sitemapList; } - public static List<OpenHABSitemap> sortSitemapList(List<OpenHABSitemap> sitemapList, String defaultSitemapName) { + public static void sortSitemapList(List<OpenHABSitemap> sitemapList, String defaultSitemapName) { // Sort by sitename label, the default sitemap should be the first one Collections.sort(sitemapList, new Comparator<OpenHABSitemap>() { @Override @@ -124,8 +124,6 @@ public class Util { return sitemap1.label().compareToIgnoreCase(sitemap2.label()); } }); - - return sitemapList; } public static boolean sitemapExists(List<OpenHABSitemap> sitemapList, String sitemapName) { diff --git a/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java b/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java index 6d49ff92600d03cc3e9127b378c6339b7816df5a..f87d60480253253793e64549323548c5a4623581 100644 --- a/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java +++ b/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java @@ -94,7 +94,7 @@ public class UtilTest { public void testSortSitemapList() throws IOException, SAXException, ParserConfigurationException { List<OpenHABSitemap> sitemapList = Util.parseSitemapList(getSitemapOH1Document()); - sitemapList = Util.sortSitemapList(Util.parseSitemapList(getSitemapOH1Document()), ""); + Util.sortSitemapList(sitemapList, ""); // Should be sorted assertEquals("Garden", sitemapList.get(0).label()); assertEquals("Heating", sitemapList.get(1).label()); @@ -105,7 +105,7 @@ public class UtilTest { assertEquals("Scenes", sitemapList.get(6).label()); assertEquals("Schedule", sitemapList.get(7).label()); - sitemapList = Util.sortSitemapList(Util.parseSitemapList(getSitemapOH1Document()), "schedule"); + Util.sortSitemapList(sitemapList, "schedule"); // Should be sorted, but "Schedule" should be the first one assertEquals("Schedule", sitemapList.get(0).label()); assertEquals("Garden", sitemapList.get(1).label());