From 9e335e0d0e1a2b7578432aa5a5635b46671b4d48 Mon Sep 17 00:00:00 2001 From: mueller-ma <mueller-ma@users.noreply.github.com> Date: Thu, 23 May 2019 18:27:40 +0200 Subject: [PATCH] Support MapView in foss flavor (#1358) See #574 Signed-off-by: mueller-ma <mueller-ma@users.noreply.github.com> --- README.md | 2 +- assets/store_descriptions/en-US/strings.xml | 2 +- mobile/build.gradle | 3 +- .../openhab/habdroid/ui/BasicWidgetTest.java | 12 +- .../openhab/habdroid/ui/MapViewHelper.java | 250 +++++++++++++++++- .../res/drawable/ic_location_on_red_24dp.xml | 5 + .../res/layout/openhabwidgetlist_mapitem.xml | 43 +++ .../res/layout/openhabwidgetlist_mapitem.xml | 2 - 8 files changed, 304 insertions(+), 15 deletions(-) create mode 100644 mobile/src/foss/res/drawable/ic_location_on_red_24dp.xml create mode 100644 mobile/src/foss/res/layout/openhabwidgetlist_mapitem.xml diff --git a/README.md b/README.md index 8ebda657..47fe7a99 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Before producing any amount of code please have a look at [contribution guidelin ## Build flavors -An optional build flavor "foss" is available for distribution through F-Droid. This build has map view, FCM and crash reporting removed and will not be able to receive push notifications from openHAB Cloud. +An optional build flavor "foss" is available for distribution through F-Droid. This build has FCM and crash reporting removed and will not be able to receive push notifications from openHAB Cloud. For using map view support in the "full" build flavor, you need to visit the [Maps API page](https://developers.google.com/maps/android) and generate an API key via the 'Get a key' button at the top. Then add a line in the following format to the 'gradle.properties' file (either in the same directory as this readme file, or in $HOME/.gradle): `mapsApiKey=<key>`, replacing `<key>` with the API key you just obtained. diff --git a/assets/store_descriptions/en-US/strings.xml b/assets/store_descriptions/en-US/strings.xml index 08b1ef7e..e3fc457a 100644 --- a/assets/store_descriptions/en-US/strings.xml +++ b/assets/store_descriptions/en-US/strings.xml @@ -25,7 +25,7 @@ <important_note>Important note</important_note> <oh_server>You need an openHAB server for this app</oh_server> <short_description>Vendor and technology agnostic open source home automation</short_description> - <fdroid>The builds on F-Droid have map view support, FCM and crash reporting removed and will not be able to receive push notifications from openHAB Cloud.</fdroid> + <fdroid>The builds on F-Droid have FCM and crash reporting removed and will not be able to receive push notifications from openHAB Cloud.</fdroid> <fdroid_beta><![CDATA[You can install the beta version alongside the <a href="https://f-droid.org/packages/org.openhab.habdroid/">stable version</a>.]]></fdroid_beta> <fdroid_privacy_policy>Privacy policy: https://www.openhabfoundation.org/privacy.html</fdroid_privacy_policy> <fdroid_anti_features>Anti Features</fdroid_anti_features> diff --git a/mobile/build.gradle b/mobile/build.gradle index 323f1960..ea9bbdef 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -112,8 +112,9 @@ dependencies { transitive false exclude group: 'com.intellij', module: 'annotations' } - // Google Maps + // MapView support fullImplementation 'com.google.android.gms:play-services-maps:16.1.0' + fossImplementation 'org.osmdroid:osmdroid-android:6.1.0' // FCM fullImplementation 'com.google.firebase:firebase-messaging:17.6.0' diff --git a/mobile/src/androidTest/java/org/openhab/habdroid/ui/BasicWidgetTest.java b/mobile/src/androidTest/java/org/openhab/habdroid/ui/BasicWidgetTest.java index 9ba7fc21..43d4d816 100644 --- a/mobile/src/androidTest/java/org/openhab/habdroid/ui/BasicWidgetTest.java +++ b/mobile/src/androidTest/java/org/openhab/habdroid/ui/BasicWidgetTest.java @@ -15,10 +15,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Test; import org.junit.runner.RunWith; -import org.openhab.habdroid.BuildConfig; import org.openhab.habdroid.R; import org.openhab.habdroid.TestWithoutIntro; -import org.openhab.habdroid.util.Util; import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.Espresso.onView; @@ -100,12 +98,10 @@ public class BasicWidgetTest extends TestWithoutIntro { .perform(scrollToPosition(10)) .check(matches(atPositionOnView(10, isDisplayed(), R.id.stop_button))); - if (Util.isFlavorFull()) { - // check whether map view is displayed - recyclerView - .perform(scrollToPosition(13)) - .check(matches(atPositionOnView(13, isDisplayed(), "MapView"))); - } + // check whether map view is displayed + recyclerView + .perform(scrollToPosition(13)) + .check(matches(atPositionOnView(13, isDisplayed(), R.id.mapview))); } public interface ChildViewCallback { diff --git a/mobile/src/foss/java/org/openhab/habdroid/ui/MapViewHelper.java b/mobile/src/foss/java/org/openhab/habdroid/ui/MapViewHelper.java index 3b5db246..69e6a2fa 100644 --- a/mobile/src/foss/java/org/openhab/habdroid/ui/MapViewHelper.java +++ b/mobile/src/foss/java/org/openhab/habdroid/ui/MapViewHelper.java @@ -1,13 +1,259 @@ package org.openhab.habdroid.ui; +import android.app.AlertDialog; +import android.content.Context; +import android.content.res.Resources; +import android.location.Location; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.core.content.ContextCompat; +import org.openhab.habdroid.R; import org.openhab.habdroid.core.connection.Connection; +import org.openhab.habdroid.model.Item; +import org.openhab.habdroid.model.ParsedState; +import org.openhab.habdroid.model.Widget; +import org.openhab.habdroid.util.Util; +import org.osmdroid.api.IMapController; +import org.osmdroid.config.Configuration; +import org.osmdroid.events.MapEventsReceiver; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.BoundingBox; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.CopyrightOverlay; +import org.osmdroid.views.overlay.MapEventsOverlay; +import org.osmdroid.views.overlay.Marker; + +import java.util.ArrayList; +import java.util.Locale; public class MapViewHelper { + private static final String TAG = MapViewHelper.class.getSimpleName(); + public static WidgetAdapter.ViewHolder createViewHolder(LayoutInflater inflater, ViewGroup parent, Connection connection, WidgetAdapter.ColorMapper colorMapper) { - return new WidgetAdapter.GenericViewHolder(inflater, parent, connection, colorMapper); + Context context = inflater.getContext(); + Configuration.getInstance().load(context, + PreferenceManager.getDefaultSharedPreferences(context)); + return new OsmViewHolder(inflater, parent, connection, colorMapper); + } + + private static class OsmViewHolder extends WidgetAdapter.LabeledItemBaseViewHolder + implements Marker.OnMarkerDragListener { + private final MapView mMapView; + private final Handler mHandler; + private final int mRowHeightPixels; + private Item mBoundItem; + private boolean mStarted; + + public OsmViewHolder(LayoutInflater inflater, ViewGroup parent, + Connection connection, WidgetAdapter.ColorMapper colorMapper) { + super(inflater, parent, R.layout.openhabwidgetlist_mapitem, connection, colorMapper); + mHandler = new Handler(); + mMapView = itemView.findViewById(R.id.mapview); + mMapView.setTileSource(TileSourceFactory.MAPNIK); + + mMapView.setVerticalMapRepetitionEnabled(false); + mMapView.getOverlays().add(new CopyrightOverlay(itemView.getContext())); + mMapView.setBuiltInZoomControls(false); + mMapView.setMultiTouchControls(false); + mMapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { + @Override + public boolean singleTapConfirmedHelper(GeoPoint p) { + openPopup(); + return true; + } + + @Override + public boolean longPressHelper(GeoPoint p) { + return false; + } + })); + + applyPositionAndLabelWhenReady(mMapView, 15.0f, false, false); + + final Resources res = itemView.getContext().getResources(); + mRowHeightPixels = res.getDimensionPixelSize(R.dimen.row_height); + } + + @Override + public void bind(Widget widget) { + super.bind(widget); + + ViewGroup.LayoutParams lp = mMapView.getLayoutParams(); + int rows = widget.height() > 0 ? widget.height() : 5; + int desiredHeightPixels = rows * mRowHeightPixels; + if (lp.height != desiredHeightPixels) { + lp.height = desiredHeightPixels; + mMapView.setLayoutParams(lp); + } + + mBoundItem = widget.item(); + applyPositionAndLabelWhenReady(mMapView, 15.0f, false, false); + } + + @Override + public void start() { + super.start(); + if (!mStarted) { + mMapView.onResume(); + mStarted = true; + } + } + + @Override + public void stop() { + super.stop(); + if (mStarted) { + mMapView.onPause(); + mStarted = false; + } + } + + @Override + public void onMarkerDragStart(Marker marker) { + // no-op, we're interested in drag end only + } + + @Override + public void onMarkerDrag(Marker marker) { + // no-op, we're interested in drag end only + } + + @Override + public void onMarkerDragEnd(Marker marker) { + String newState = String.format(Locale.US, "%f,%f", + marker.getPosition().getLatitude(), marker.getPosition().getLongitude()); + String item = marker.getId(); + Util.sendItemCommand(mConnection.getAsyncHttpClient(), item, newState); + } + + private void openPopup() { + final MapView mapView = new MapView(itemView.getContext()); + + AlertDialog dialog = new AlertDialog.Builder(itemView.getContext()) + .setView(mapView) + .setCancelable(true) + .setNegativeButton(R.string.close, null) + .create(); + + dialog.setOnDismissListener(dialogInterface -> mapView.onPause()); + dialog.setCanceledOnTouchOutside(true); + dialog.show(); + + mapView.setBuiltInZoomControls(true); + mapView.setMultiTouchControls(true); + mapView.setVerticalMapRepetitionEnabled(false); + mapView.getOverlays().add(new CopyrightOverlay(itemView.getContext())); + mapView.onResume(); + applyPositionAndLabelWhenReady(mapView, 16.0f, true, true); + } + + private void applyPositionAndLabelWhenReady(MapView mapView, float zoomLevel, + boolean allowDrag, boolean allowScroll) { + mHandler.post(() -> applyPositionAndLabel(mapView, zoomLevel, allowDrag, allowScroll)); + } + + private void applyPositionAndLabel(MapView mapView, float zoomLevel, boolean allowDrag, + boolean allowScroll) { + if (mBoundItem == null) { + return; + } + boolean canDragMarker = allowDrag && !mBoundItem.readOnly(); + if (!mBoundItem.members().isEmpty()) { + ArrayList<GeoPoint> positions = new ArrayList<>(); + for (Item item : mBoundItem.members()) { + GeoPoint position = toGeoPoint(item.state()); + if (position != null) { + setMarker(mapView, position, item, item.label(), canDragMarker, + this); + positions.add(position); + } + } + + if (!positions.isEmpty()) { + double north = -90; + double south = 90; + double west = 180; + double east = -180; + for (GeoPoint position : positions) { + north = Math.max(position.getLatitude(), north); + south = Math.min(position.getLatitude(), south); + + west = Math.min(position.getLongitude(), west); + east = Math.max(position.getLongitude(), east); + } + + Log.d(TAG, String.format("North %f, south %f, west %f, east %f", + north, south, west, east)); + BoundingBox boundingBox = new BoundingBox(north, east, south, west); + int extraPixel = (int) convertDpToPixel(24f, mapView.getContext()); + try { + mapView.zoomToBoundingBox(boundingBox, false, extraPixel); + } catch (Exception e) { + Log.d(TAG, "Error applying markers", e); + } + if (!allowScroll) { + mapView.setScrollableAreaLimitLongitude(west, east, extraPixel); + mapView.setScrollableAreaLimitLatitude(north, south, extraPixel); + } + } + } else { + GeoPoint position = toGeoPoint(mBoundItem.state()); + if (position != null) { + setMarker(mapView, position, mBoundItem, mLabelView.getText(), canDragMarker, + this); + moveCamera(mapView, zoomLevel, position); + if (!allowScroll) { + mapView.setScrollableAreaLimitLatitude(position.getLatitude(), + position.getLatitude(), 0); + mapView.setScrollableAreaLimitLongitude(position.getLongitude(), + position.getLongitude(), 0); + } + } + } + } + + /** + * This method converts dp unit to equivalent pixels, depending on device density. + * + * @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels + * @param context Context to get resources and device specific display metrics + * @return A float value to represent px equivalent to dp depending on device density + */ + private static float convertDpToPixel(float dp, Context context){ + return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + private static void moveCamera(MapView mapView, float zoom, GeoPoint geoPoint) { + IMapController mapController = mapView.getController(); + mapController.setZoom(zoom); + mapController.setCenter(geoPoint); + } + + private static void setMarker(MapView mapView, GeoPoint position, Item item, + CharSequence label, boolean canDrag, + Marker.OnMarkerDragListener onMarkerDragListener) { + Marker marker = new Marker(mapView); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + marker.setDraggable(canDrag); + marker.setPosition(position); + marker.setTitle(label != null ? label.toString() : null); + marker.setId(item.link()); + marker.setOnMarkerDragListener(onMarkerDragListener); + marker.setIcon(ContextCompat.getDrawable(mapView.getContext(), + R.drawable.ic_location_on_red_24dp)); + mapView.getOverlays().add(marker); + } + + private static GeoPoint toGeoPoint(ParsedState state) { + Location location = state != null ? state.asLocation() : null; + return location != null ? new GeoPoint(location) : null; + } } -} +} \ No newline at end of file diff --git a/mobile/src/foss/res/drawable/ic_location_on_red_24dp.xml b/mobile/src/foss/res/drawable/ic_location_on_red_24dp.xml new file mode 100644 index 00000000..1a61d964 --- /dev/null +++ b/mobile/src/foss/res/drawable/ic_location_on_red_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="36dp" android:tint="#EA4335" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/> +</vector> diff --git a/mobile/src/foss/res/layout/openhabwidgetlist_mapitem.xml b/mobile/src/foss/res/layout/openhabwidgetlist_mapitem.xml new file mode 100644 index 00000000..d03cbffc --- /dev/null +++ b/mobile/src/foss/res/layout/openhabwidgetlist_mapitem.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/widgetlist_item_left_margin" + android:paddingRight="@dimen/widgetlist_item_right_margin" + android:background="?android:activatedBackgroundIndicator" + android:paddingStart="@dimen/widgetlist_item_left_margin" + android:paddingEnd="@dimen/widgetlist_item_right_margin" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <org.openhab.habdroid.ui.widget.WidgetImageView + android:id="@+id/widgeticon" + android:layout_width="32dp" + android:layout_height="32dp" + android:padding="4dp" /> + + <TextView + android:id="@+id/widgetlabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + android:ellipsize="end" + android:maxLines="1" + android:textAppearance="?android:attr/textAppearanceSmall" /> + + </LinearLayout> + + <org.osmdroid.views.MapView + android:id="@+id/mapview" + android:layout_width="match_parent" + android:layout_height="180dp" + android:layout_margin="4dp" /> + +</LinearLayout> diff --git a/mobile/src/full/res/layout/openhabwidgetlist_mapitem.xml b/mobile/src/full/res/layout/openhabwidgetlist_mapitem.xml index 7ddd9337..681078a3 100644 --- a/mobile/src/full/res/layout/openhabwidgetlist_mapitem.xml +++ b/mobile/src/full/res/layout/openhabwidgetlist_mapitem.xml @@ -34,13 +34,11 @@ </LinearLayout> - <!-- tag used by unit test, don't remove --> <com.google.android.gms.maps.MapView android:id="@+id/mapview" android:layout_width="match_parent" android:layout_height="180dp" android:layout_margin="4dp" - android:tag="MapView" app:liteMode="true" /> </LinearLayout> -- GitLab