diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index c78be48a920a556a817897c6d7a2ae6c6dbd8e3f..ea81ce73c152d38c8339391f1d14be05cfd1b63a 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -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" diff --git a/mobile/src/main/java/org/openhab/habdroid/model/LinkedPage.java b/mobile/src/main/java/org/openhab/habdroid/model/LinkedPage.java index ecc0bd3ebc50a42f58176a47087d7908a03bf2ab..56072e9a1da84e675dcbcbcd35ba447488d6965c 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/LinkedPage.java +++ b/mobile/src/main/java/org/openhab/habdroid/model/LinkedPage.java @@ -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(); } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.java index be7ded1b9f10152b9894c8c3c066c05363ec0842..f0188f8369e13b1bf02603abf7775695d3d6c5fa 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.java @@ -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(); diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.java b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.java index 1124115864bbbd31bac562555a935fd118153cee..bcae60cae2c5066976bdd70b3165b9528f81f6e8 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.java @@ -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; } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.java b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.java index d2fdc17153ad06cdfb1e7ee5736e4224b537eca8..16793e77978a708a7c0ab5f2e5608cb8c9095773 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.java +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.java @@ -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(); 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 71dc4ca126f1835937e684ceea31ba5276790c38..4d6ed19991b7bb9e51c295927629d1ee5c5b2f9b 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 @@ -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 diff --git a/mobile/src/main/java/org/openhab/habdroid/util/AsyncHttpClient.java b/mobile/src/main/java/org/openhab/habdroid/util/AsyncHttpClient.java index e52607872ff14c3357fb87604f85c42413f80c8d..08ab6d5e7709904003046806d19a81e551812e53 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/AsyncHttpClient.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/AsyncHttpClient.java @@ -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); } } 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 aaa8c1123117162ebda9b69d9a79345aeff7224d..f5cbd682122e95a50ff37adc5c5ed4fd4aac46da 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java @@ -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; + } } diff --git a/mobile/src/main/java/org/openhab/habdroid/util/SyncHttpClient.java b/mobile/src/main/java/org/openhab/habdroid/util/SyncHttpClient.java index 61834a39b9c10a208cc871068ba3f3b6dd203512..41e0e5b64ba5a2567fb9273ec63065f37a78dfda 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/SyncHttpClient.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/SyncHttpClient.java @@ -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); } 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 49c5d55f89e089d63a2e99a6097a5dec626db300..6443810ad553dd930086f0d3695797e2a223ee3a 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/Util.java +++ b/mobile/src/main/java/org/openhab/habdroid/util/Util.java @@ -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); + } } diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index bfdf3e1a5c27d0b4a1172647b6dea06e5c552efd..c68fa3cf977783f3ea5a1c26434ece5f9d652deb 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -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>