Skip to content
Snippets Groups Projects
Unverified Commit 2f048289 authored by mueller-ma's avatar mueller-ma Committed by GitHub
Browse files

Add ability to create sitemap shortcuts (#1357)


* Fixes #1322
* Fixes #1337

Signed-off-by: default avatarmueller-ma <mueller-ma@users.noreply.github.com>
parent 0e93042b
No related branches found
No related tags found
No related merge requests found
Showing
with 280 additions and 90 deletions
......@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-feature
android:name="android.hardware.location.gps"
......
......@@ -25,6 +25,7 @@ public abstract class LinkedPage implements Parcelable {
public abstract String id();
public abstract String title();
public abstract String icon();
public abstract String iconPath();
public abstract String link();
@AutoValue.Builder
......@@ -32,6 +33,7 @@ public abstract class LinkedPage implements Parcelable {
public abstract Builder id(String id);
public abstract Builder title(String title);
public abstract Builder icon(String icon);
public abstract Builder iconPath(String iconPath);
public abstract Builder link(String link);
public LinkedPage build() {
......@@ -67,6 +69,7 @@ public abstract class LinkedPage implements Parcelable {
.id(id)
.title(title)
.icon(icon)
.iconPath(String.format("images/%s.png", icon))
.link(link)
.build();
}
......@@ -75,10 +78,12 @@ public abstract class LinkedPage implements Parcelable {
if (jsonObject == null) {
return null;
}
String icon = jsonObject.optString("icon", null);
return new AutoValue_LinkedPage.Builder()
.id(jsonObject.optString("id", null))
.title(jsonObject.optString("title", null))
.icon(jsonObject.optString("icon", null))
.icon(icon)
.iconPath(String.format("icon/%s", icon))
.link(jsonObject.optString("link", null))
.build();
}
......
......@@ -248,7 +248,7 @@ public class MainActivity extends AbstractBaseActivity implements
}
// Create a new boolean and preference and set it to true
boolean isFirstStart = mPrefs.getBoolean("firstStart", true);
boolean isFirstStart = mPrefs.getBoolean(Constants.PREFERENCE_FIRST_START, true);
SharedPreferences.Editor prefsEditor = mPrefs.edit();
// If the activity has never started before...
......@@ -257,7 +257,7 @@ public class MainActivity extends AbstractBaseActivity implements
final Intent i = new Intent(MainActivity.this, IntroActivity.class);
startActivityForResult(i, INTRO_REQUEST_CODE);
prefsEditor.putBoolean("firstStart", false);
prefsEditor.putBoolean(Constants.PREFERENCE_FIRST_START, false);
}
OnUpdateBroadcastReceiver.updateComparableVersion(prefsEditor);
prefsEditor.apply();
......
......@@ -85,7 +85,7 @@ public class WidgetAdapter extends RecyclerView.Adapter<WidgetAdapter.ViewHolder
public interface ItemClickListener {
boolean onItemClicked(Widget item); // returns whether click was handled
void onItemLongClicked(Widget item);
boolean onItemLongClicked(Widget item);
}
private static final int TYPE_GENERICITEM = 0;
......@@ -346,7 +346,7 @@ public class WidgetAdapter extends RecyclerView.Adapter<WidgetAdapter.ViewHolder
ViewHolder holder = (ViewHolder) view.getTag();
int position = holder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
mItemClickListener.onItemLongClicked(mItems.get(position));
return mItemClickListener.onItemLongClicked(mItems.get(position));
}
return false;
}
......
......@@ -9,9 +9,19 @@
package org.openhab.habdroid.ui;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
......@@ -19,11 +29,18 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import es.dmoral.toasty.Toasty;
import org.openhab.habdroid.R;
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.Item;
import org.openhab.habdroid.model.LabeledValue;
import org.openhab.habdroid.model.LinkedPage;
......@@ -31,6 +48,7 @@ import org.openhab.habdroid.model.ParsedState;
import org.openhab.habdroid.model.Widget;
import org.openhab.habdroid.ui.widget.RecyclerViewSwipeRefreshLayout;
import org.openhab.habdroid.util.CacheManager;
import org.openhab.habdroid.util.Constants;
import org.openhab.habdroid.util.Util;
import java.util.ArrayList;
......@@ -108,7 +126,7 @@ public class WidgetListFragment extends Fragment
}
@Override
public void onItemLongClicked(final Widget widget) {
public boolean onItemLongClicked(final Widget widget) {
ArrayList<String> labels = new ArrayList<>();
ArrayList<String> commands = new ArrayList<>();
......@@ -184,6 +202,9 @@ public class WidgetListFragment extends Fragment
if (widget.linkedPage() != null) {
labels.add(getString(R.string.nfc_action_to_sitemap_page));
if (ShortcutManagerCompat.isRequestPinShortcutSupported(getContext())) {
labels.add(getString(R.string.home_shortcut_pin_to_home));
}
}
if (!labels.isEmpty()) {
......@@ -191,19 +212,22 @@ public class WidgetListFragment extends Fragment
new AlertDialog.Builder(getActivity())
.setTitle(R.string.nfc_dialog_title)
.setItems(labelArray, (dialog, which) -> {
final Intent writeTagIntent;
if (which < commands.size()) {
writeTagIntent = WriteTagActivity.createItemUpdateIntent(getActivity(),
widget.item().name(), commands.get(which),
labels.get(which), widget.item().label());
startActivity(WriteTagActivity.createItemUpdateIntent(
getActivity(), widget.item().name(), commands.get(which),
labels.get(which), widget.item().label()));
} else if (which == commands.size()) {
startActivity(WriteTagActivity.createSitemapNavigationIntent(
getActivity(), widget.linkedPage().link()));
} else {
writeTagIntent = WriteTagActivity.createSitemapNavigationIntent(
getActivity(), widget.linkedPage().link());
createShortcut(widget.linkedPage());
}
startActivityForResult(writeTagIntent, 0);
})
.show();
return true;
}
return false;
}
@Override
......@@ -251,6 +275,106 @@ public class WidgetListFragment extends Fragment
}
}
@SuppressLint("StaticFieldLeak")
private void createShortcut(LinkedPage linkedPage) {
String iconFormat = PreferenceManager.getDefaultSharedPreferences(getContext())
.getString(Constants.PREFERENCE_ICON_FORMAT, "png");
new AsyncTask<Void, Void, IconCompat>() {
@Override
protected IconCompat doInBackground(Void... voids) {
Context context = getContext();
String url = null;
if (!TextUtils.isEmpty(linkedPage.iconPath())) {
url = new Uri.Builder()
.appendEncodedPath(linkedPage.iconPath())
.appendQueryParameter("format", iconFormat)
.toString();
}
IconCompat icon = null;
Connection connection = null;
try {
connection = ConnectionFactory.getUsableConnection();
} catch (ConnectionException e) {
// ignored
}
if (context == null || connection == null) {
return null;
}
if (url != null) {
/**
* Icon size is defined in {@link AdaptiveIconDrawable}. Foreground size of
* 46dp instead of 72dp adds enough border to the icon.
* 46dp foreground + 2 * 31dp border = 108dp
**/
int foregroundSize = (int) Util.convertDpToPixel(46, context);
Bitmap bitmap = connection.getSyncHttpClient().get(url)
.asBitmap(foregroundSize).response;
if (bitmap != null) {
bitmap = addBackgroundAndBorder(bitmap,
(int) Util.convertDpToPixel(31, context));
icon = IconCompat.createWithAdaptiveBitmap(bitmap);
}
}
if (icon == null) {
// Fall back to openHAB icon
icon = IconCompat.createWithResource(context, R.mipmap.icon);
}
return icon;
}
@Override
protected void onPostExecute(IconCompat icon) {
Context context = getContext();
if (icon == null || context == null) {
return;
}
Uri sitemapUri = Uri.parse(linkedPage.link());
String shortSitemapUri = sitemapUri.getPath().substring(14);
Intent startIntent = new Intent(context, MainActivity.class);
startIntent.setAction(MainActivity.ACTION_SITEMAP_SELECTED);
startIntent.putExtra(MainActivity.EXTRA_SITEMAP_URL, shortSitemapUri);
startActivity(startIntent);
String name = linkedPage.title();
if (TextUtils.isEmpty(name)) {
name = getString(R.string.app_name);
}
ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context,
shortSitemapUri + '-' + System.currentTimeMillis())
.setShortLabel(name)
.setIcon(icon)
.setIntent(startIntent)
.build();
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
Toasty.success(context,R.string.home_shortcut_success_pinning).show();
} else {
Toasty.error(context,R.string.home_shortcut_error_pinning).show();
}
}
}.execute();
}
/**
* @author https://stackoverflow.com/a/15525394
*/
private Bitmap addBackgroundAndBorder(Bitmap bitmap, int borderSize) {
Bitmap bitmapWithBackground = Bitmap.createBitmap(bitmap.getWidth() + borderSize * 2,
bitmap.getHeight() + borderSize * 2, bitmap.getConfig());
Canvas canvas = new Canvas(bitmapWithBackground);
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(bitmap, borderSize, borderSize, null);
return bitmapWithBackground;
}
public static WidgetListFragment withPage(String pageUrl, String pageTitle) {
WidgetListFragment fragment = new WidgetListFragment();
Bundle args = new Bundle();
......
......@@ -431,7 +431,7 @@ public abstract class ContentController implements PageConnectionHolderFragment.
@Override
public String getIconFormat() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
return prefs.getString("iconFormatType","PNG");
return prefs.getString(Constants.PREFERENCE_ICON_FORMAT, "PNG");
}
@Override
......
......@@ -9,28 +9,20 @@
package org.openhab.habdroid.util;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
public class AsyncHttpClient extends HttpClient {
......@@ -57,74 +49,7 @@ public class AsyncHttpClient extends HttpClient {
@Override
public Bitmap convertBodyInBackground(ResponseBody body) throws IOException {
MediaType contentType = body.contentType();
boolean isSvg = contentType != null
&& contentType.type().equals("image")
&& contentType.subtype().contains("svg");
InputStream is = body.byteStream();
if (isSvg) {
try {
return getBitmapFromSvgInputstream(Resources.getSystem(), is, mSize);
} catch (SVGParseException e) {
throw new IOException("SVG decoding failed", e);
}
} else {
Bitmap bitmap = BitmapFactory.decodeStream(is);
if (bitmap != null) {
return bitmap;
}
throw new IOException("Bitmap decoding failed");
}
}
private static Bitmap getBitmapFromSvgInputstream(Resources res, InputStream is, int size)
throws SVGParseException {
SVG svg = SVG.getFromInputStream(is);
svg.setRenderDPI(DisplayMetrics.DENSITY_DEFAULT);
Float density = res.getDisplayMetrics().density;
svg.setDocumentHeight("100%");
svg.setDocumentWidth("100%");
int docWidth = (int) (svg.getDocumentWidth() * density);
int docHeight = (int) (svg.getDocumentHeight() * density);
if (docWidth < 0 || docHeight < 0) {
float aspectRatio = svg.getDocumentAspectRatio();
if (aspectRatio > 0) {
float heightForAspect = (float) size / aspectRatio;
float widthForAspect = (float) size * aspectRatio;
if (widthForAspect < heightForAspect) {
docWidth = Math.round(widthForAspect);
docHeight = size;
} else {
docWidth = size;
docHeight = Math.round(heightForAspect);
}
} else {
docWidth = size;
docHeight = size;
}
// we didn't take density into account anymore when calculating docWidth
// and docHeight, so don't scale with it and just let the renderer
// figure out the scaling
density = null;
}
if (docWidth != size || docHeight != size) {
float scaleWidth = (float) size / docWidth;
float scaleHeigth = (float) size / docHeight;
density = (scaleWidth + scaleHeigth) / 2;
docWidth = size;
docHeight = size;
}
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (density != null) {
canvas.scale(density, density);
}
svg.renderToCanvas(canvas);
return bitmap;
return getBitmapFromResponseBody(body, mSize);
}
}
......
......@@ -9,8 +9,15 @@
package org.openhab.habdroid.util;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.DisplayMetrics;
import androidx.annotation.VisibleForTesting;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.here.oksse.OkSse;
import com.here.oksse.ServerSentEvent;
import okhttp3.CacheControl;
......@@ -21,7 +28,10 @@ import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.TimeUnit;
......@@ -111,4 +121,76 @@ public abstract class HttpClient {
}
return builder;
}
protected static Bitmap getBitmapFromResponseBody(ResponseBody body, int size)
throws IOException {
MediaType contentType = body.contentType();
boolean isSvg = contentType != null
&& contentType.type().equals("image")
&& contentType.subtype().contains("svg");
InputStream is = body.byteStream();
if (isSvg) {
try {
return getBitmapFromSvgInputstream(Resources.getSystem(), is, size);
} catch (SVGParseException e) {
throw new IOException("SVG decoding failed", e);
}
} else {
Bitmap bitmap = BitmapFactory.decodeStream(is);
if (bitmap != null) {
return Bitmap.createScaledBitmap(bitmap, size, size, false);
}
throw new IOException("Bitmap decoding failed");
}
}
private static Bitmap getBitmapFromSvgInputstream(Resources res, InputStream is, int size)
throws SVGParseException {
SVG svg = SVG.getFromInputStream(is);
svg.setRenderDPI(DisplayMetrics.DENSITY_DEFAULT);
Float density = res.getDisplayMetrics().density;
svg.setDocumentHeight("100%");
svg.setDocumentWidth("100%");
int docWidth = (int) (svg.getDocumentWidth() * density);
int docHeight = (int) (svg.getDocumentHeight() * density);
if (docWidth < 0 || docHeight < 0) {
float aspectRatio = svg.getDocumentAspectRatio();
if (aspectRatio > 0) {
float heightForAspect = (float) size / aspectRatio;
float widthForAspect = (float) size * aspectRatio;
if (widthForAspect < heightForAspect) {
docWidth = Math.round(widthForAspect);
docHeight = size;
} else {
docWidth = size;
docHeight = Math.round(heightForAspect);
}
} else {
docWidth = size;
docHeight = size;
}
// we didn't take density into account anymore when calculating docWidth
// and docHeight, so don't scale with it and just let the renderer
// figure out the scaling
density = null;
}
if (docWidth != size || docHeight != size) {
float scaleWidth = (float) size / docWidth;
float scaleHeigth = (float) size / docHeight;
density = (scaleWidth + scaleHeigth) / 2;
docWidth = size;
docHeight = size;
}
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (density != null) {
canvas.scale(density, density);
}
svg.renderToCanvas(canvas);
return bitmap;
}
}
......@@ -9,6 +9,8 @@
package org.openhab.habdroid.util;
import android.graphics.Bitmap;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
......@@ -61,6 +63,10 @@ public class SyncHttpClient extends HttpClient {
return new HttpTextResult(this);
}
public HttpBitmapResult asBitmap(int sizeInPixels) {
return new HttpBitmapResult(this, sizeInPixels);
}
public HttpStatusResult asStatus() {
return new HttpStatusResult(this);
}
......@@ -112,6 +118,33 @@ public class SyncHttpClient extends HttpClient {
}
}
public static class HttpBitmapResult {
public final Request request;
public final Bitmap response;
public final Throwable error;
public final int statusCode;
HttpBitmapResult(HttpResult result, int size) {
this.request = result.request;
this.statusCode = result.statusCode;
if (result.response == null) {
this.response = null;
this.error = result.error;
} else {
Bitmap response = null;
Throwable error = result.error;
try {
response = getBitmapFromResponseBody(result.response, size);
} catch (IOException e) {
error = e;
}
this.response = response;
this.error = error;
}
result.close();
}
}
public SyncHttpClient(OkHttpClient client, String baseUrl, String username, String password) {
super(client, baseUrl, username, password);
}
......
......@@ -22,6 +22,7 @@ import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.webkit.WebChromeClient;
......@@ -399,4 +400,18 @@ public class Util {
R.drawable.ic_openhab_appicon_24dp, R.color.openhab_orange, Toasty.LENGTH_SHORT,
true, true).show());
}
/**
* 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
* @author https://stackoverflow.com/a/9563438
*/
public static float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
}
......@@ -255,6 +255,11 @@
<string name="clear_log">Clear log</string>
<string name="empty_log">Log is empty</string>
<!-- Home screen shortcuts -->
<string name="home_shortcut_pin_to_home">Pin to home</string>
<string name="home_shortcut_error_pinning">Error pinning to home</string>
<string name="home_shortcut_success_pinning">Pinned to home</string>
<!-- Content description for images -->
<string name="content_description_open_roller_shutter">Open roller shutter</string>
<string name="content_description_stop_roller_shutter">Stop roller shutter</string>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment