diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java index 8245be60a..14e60489e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java @@ -194,7 +194,8 @@ public class WidgetScreenDetailsActivity extends AbstractGBActivity { updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, 2); updateWidget(cardWidgetBotRight, labelWidgetBotRight, 3); break; - case SINGLE: + case ONE_BY_TWO_SINGLE: + case TWO_BY_TWO_SINGLE: updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, -1); updateWidget(cardWidgetTopRight, labelWidgetTopRight, -1); updateWidget(cardWidgetCenter, labelWidgetCenter, 0); @@ -202,9 +203,9 @@ public class WidgetScreenDetailsActivity extends AbstractGBActivity { updateWidget(cardWidgetBotRight, labelWidgetBotRight, -1); break; case TWO: - updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, 0); - updateWidget(cardWidgetTopRight, labelWidgetTopRight, 1); - updateWidget(cardWidgetCenter, labelWidgetCenter, -1); + updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, -1); + updateWidget(cardWidgetTopRight, labelWidgetTopRight, 0); + updateWidget(cardWidgetCenter, labelWidgetCenter, 1); updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, -1); updateWidget(cardWidgetBotRight, labelWidgetBotRight, -1); break; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java index 06341d38c..bc677be2c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java @@ -21,11 +21,17 @@ import androidx.annotation.StringRes; import nodomain.freeyourgadget.gadgetbridge.R; public enum WidgetLayout { + // Square screen layouts, 2x2 TOP_1_BOT_2(R.string.widget_layout_top_1_bot_2, WidgetType.WIDE, WidgetType.SMALL, WidgetType.SMALL), TOP_2_BOT_1(R.string.widget_layout_top_2_bot_1, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL), TOP_2_BOT_2(R.string.widget_layout_top_2_bot_2, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL), - SINGLE(R.string.widget_layout_single, WidgetType.TALL), + TWO_BY_TWO_SINGLE(R.string.widget_layout_single, WidgetType.LARGE), + + // Narrow screen layouts, 2x1 + ONE_BY_TWO_SINGLE(R.string.widget_layout_single, WidgetType.TALL), TWO(R.string.widget_layout_two, WidgetType.SMALL, WidgetType.SMALL), + + // TODO Portrait screen layouts, 2x3 ; @StringRes diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java index 53612a359..93823d20e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java @@ -20,5 +20,6 @@ public enum WidgetType { SMALL, // 1x1 TALL, // 1x2 WIDE, // 2x1 + LARGE, // 2x2 ; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java index 63649618f..e99d5261b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java @@ -16,6 +16,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; +import android.text.TextUtils; + import androidx.annotation.Nullable; import com.google.protobuf.InvalidProtocolBufferException; @@ -25,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; @@ -60,31 +63,42 @@ public class XiaomiWidgetManager implements WidgetManager { @Override public List getSupportedWidgetLayouts() { final List layouts = new ArrayList<>(); - final Set partTypes = new HashSet<>(); - final XiaomiProto.WidgetParts rawWidgetParts = getRawWidgetParts(); - - for (final XiaomiProto.WidgetPart widgetPart : rawWidgetParts.getWidgetPartList()) { - partTypes.add(fromRawWidgetType(widgetPart.getType())); + final XiaomiProto.WidgetScreens widgetScreens = getRawWidgetScreens(); + if (!widgetScreens.hasWidgetsCapabilities() || !widgetScreens.getWidgetsCapabilities().hasSupportedLayoutStyles()) { + return Collections.emptyList(); } - if (partTypes.contains(WidgetType.WIDE) && partTypes.contains(WidgetType.SMALL)) { - layouts.add(WidgetLayout.TOP_1_BOT_2); - layouts.add(WidgetLayout.TOP_2_BOT_1); - layouts.add(WidgetLayout.TOP_2_BOT_2); - } + final int supportedBitmap = getRawWidgetScreens().getWidgetsCapabilities().getSupportedLayoutStyles(); - if (partTypes.contains(WidgetType.TALL)) { - layouts.add(WidgetLayout.SINGLE); - - if (partTypes.contains(WidgetType.SMALL)) { - layouts.add(WidgetLayout.TWO); + // highest known layout style is 0x4000 (1 << 14) + for (int i = 0; i < 15; i++) { + final int layoutStyleId = 1 << i; + if ((supportedBitmap & layoutStyleId) != 0) { + layouts.add(fromRawLayout(layoutStyleId)); } } return layouts; } + private static Collection convertWorkoutTypesToPartSubtypes(final Collection workoutTypes) { + final List subtypes = new ArrayList<>(workoutTypes.size()); + + // convert workout types to subtypes + for (final XiaomiWorkoutType workoutType : workoutTypes) { + subtypes.add(new WidgetPartSubtype( + String.valueOf(workoutType.getCode()), + workoutType.getName() + )); + } + + // sort by name before returning + Collections.sort(subtypes, (it, other) -> it.getName().compareToIgnoreCase(other.getName())); + + return subtypes; + } + @Override public List getSupportedWidgetParts(final WidgetType targetWidgetType) { final List parts = new LinkedList<>(); @@ -94,41 +108,29 @@ public class XiaomiWidgetManager implements WidgetManager { final Set seenNames = new HashSet<>(); final Set duplicatedNames = new HashSet<>(); + // get supported workout types and convert to subtypes for workout widgets + final Collection subtypes = convertWorkoutTypesToPartSubtypes(XiaomiWorkoutType.getWorkoutTypesSupportedByDevice(getDevice())); + for (final XiaomiProto.WidgetPart widgetPart : rawWidgetParts.getWidgetPartList()) { - final WidgetType type = fromRawWidgetType(widgetPart.getType()); + final WidgetPart convertedPart = fromRawWidgetPart(widgetPart, subtypes); - if (type != null && type.equals(targetWidgetType)) { - final WidgetPart newPart = new WidgetPart( - String.valueOf(widgetPart.getId()), - widgetPart.getTitle(), - type - ); - - if (widgetPart.getFunction() == 16) { - if (StringUtils.isBlank(newPart.getName())) { - newPart.setName(GBApplication.getContext().getString(R.string.menuitem_workout)); - } - - final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice()); - for (final XiaomiWorkoutType workoutType : workoutTypes) { - newPart.getSupportedSubtypes().add( - new WidgetPartSubtype( - String.valueOf(workoutType.getCode()), - workoutType.getName() - ) - ); - Collections.sort(newPart.getSupportedSubtypes(), (p1, p2) -> p1.getName().compareToIgnoreCase(p2.getName())); - } - } - - if (seenNames.contains(newPart.getFullName())) { - duplicatedNames.add(newPart.getFullName()); - } else { - seenNames.add(newPart.getFullName()); - } - - parts.add(newPart); + if (convertedPart == null) { + continue; } + + if (!convertedPart.getType().equals(targetWidgetType)) { + continue; + } + + final String convertedPartName = convertedPart.getName(); + if (seenNames.contains(convertedPartName)) { + duplicatedNames.add(convertedPartName); + } else { + seenNames.add(convertedPartName); + } + + parts.add(convertedPart); + seenNames.add(convertedPart.getName()); } // Ensure that all names are unique @@ -138,66 +140,94 @@ public class XiaomiWidgetManager implements WidgetManager { } } + Collections.sort(parts, (it, other) -> it.getName().compareToIgnoreCase(other.getName())); return parts; } + private WidgetPart fromRawWidgetPart(final XiaomiProto.WidgetPart widgetPart, final Collection subtypes) { + final WidgetType type = fromRawWidgetType(widgetPart.getType()); + + if (type == null) { + LOG.warn("Unknown widget type {}", widgetPart.getType()); + return null; + } + + final String stringifiedId = String.valueOf(widgetPart.getId()); + final WidgetPart convertedPart = new WidgetPart( + stringifiedId, + GBApplication.getContext().getString(R.string.widget_name_untitled, stringifiedId), + type + ); + + if (!TextUtils.isEmpty(widgetPart.getTitle())) { + convertedPart.setName(widgetPart.getTitle()); + } else { + // some models do not provide the name of the widget in the screens list, resolve it here + final XiaomiProto.WidgetPart resolvedPart = findRawPart(widgetPart.getType(), widgetPart.getId()); + if (resolvedPart != null) { + convertedPart.setName(resolvedPart.getTitle()); + } + } + + if (widgetPart.getFunction() == 16) { + if (StringUtils.isBlank(convertedPart.getName())) { + convertedPart.setName(GBApplication.getContext().getString(R.string.menuitem_workout)); + } + + if (subtypes != null) { + convertedPart.getSupportedSubtypes().addAll(subtypes); + + if (widgetPart.getSubType() != 0) { + final String widgetSubtype = String.valueOf(widgetPart.getSubType()); + + for (final WidgetPartSubtype availableSubtype : subtypes) { + if (availableSubtype.getId().equals(widgetSubtype)) { + convertedPart.setSubtype(availableSubtype); + break; + } + } + } + } + } + + if ((widgetPart.getId() & 256) != 0) { + convertedPart.setName(GBApplication.getContext().getString(R.string.widget_name_colored_tile, convertedPart.getName())); + } + + return convertedPart; + } + @Override public List getWidgetScreens() { final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens(); - final List ret = new ArrayList<>(rawWidgetScreens.getWidgetScreenCount()); + final List convertedScreens = new ArrayList<>(rawWidgetScreens.getWidgetScreenCount()); + final Collection workoutTypes = convertWorkoutTypesToPartSubtypes(XiaomiWorkoutType.getWorkoutTypesSupportedByDevice(getDevice())); - final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice()); + for (final XiaomiProto.WidgetScreen rawScreen : rawWidgetScreens.getWidgetScreenList()) { + final WidgetLayout layout = fromRawLayout(rawScreen.getLayout()); - for (final XiaomiProto.WidgetScreen widgetScreen : rawWidgetScreens.getWidgetScreenList()) { - final WidgetLayout layout = fromRawLayout(widgetScreen.getLayout()); + final List convertedParts = new ArrayList<>(rawScreen.getWidgetPartCount()); - final List parts = new ArrayList<>(widgetScreen.getWidgetPartCount()); + for (final XiaomiProto.WidgetPart rawPart : rawScreen.getWidgetPartList()) { + final WidgetPart convertedPart = fromRawWidgetPart(rawPart, workoutTypes); - for (final XiaomiProto.WidgetPart widgetPart : widgetScreen.getWidgetPartList()) { - final WidgetType type = fromRawWidgetType(widgetPart.getType()); - - final WidgetPart newPart = new WidgetPart( - String.valueOf(widgetPart.getId()), - "Unknown (" + widgetPart.getId() + ")", - type - ); - - // Find the name - final XiaomiProto.WidgetPart rawPart1 = findRawPart(widgetPart.getType(), widgetPart.getId()); - if (rawPart1 != null) { - newPart.setName(rawPart1.getTitle()); + if (convertedPart == null) { + LOG.warn("Widget cannot be converted, result was null for following raw widget: {}", rawPart); + continue; } - if (widgetPart.getFunction() == 16) { - if (StringUtils.isBlank(newPart.getName())) { - newPart.setName(GBApplication.getContext().getString(R.string.menuitem_workout)); - } - } - - // Get the proper subtype, if any - if (widgetPart.getSubType() != 0) { - for (final XiaomiWorkoutType workoutType : workoutTypes) { - if (workoutType.getCode() == widgetPart.getSubType()) { - newPart.setSubtype(new WidgetPartSubtype( - String.valueOf(workoutType.getCode()), - workoutType.getName() - )); - } - } - } - - parts.add(newPart); + convertedParts.add(convertedPart); } - ret.add(new WidgetScreen( - String.valueOf(widgetScreen.getId()), + convertedScreens.add(new WidgetScreen( + String.valueOf(rawScreen.getId()), layout, - parts + convertedParts )); } - return ret; + return convertedScreens; } @Override @@ -219,26 +249,9 @@ public class XiaomiWidgetManager implements WidgetManager { public void saveScreen(final WidgetScreen widgetScreen) { final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens(); - final int layoutNum; - switch (widgetScreen.getLayout()) { - case TOP_2_BOT_2: - layoutNum = 1; - break; - case TOP_1_BOT_2: - layoutNum = 2; - break; - case TOP_2_BOT_1: - layoutNum = 4; - break; - case TWO: - layoutNum = 256; - break; - case SINGLE: - layoutNum = 512; - break; - default: - LOG.warn("Unknown widget screens layout {}", widgetScreen.getLayout()); - return; + final int layoutNum = toRawLayout(widgetScreen.getLayout()); + if (layoutNum == -1) { + return; } XiaomiProto.WidgetScreen.Builder rawScreen = null; @@ -265,6 +278,8 @@ public class XiaomiWidgetManager implements WidgetManager { rawScreen.setLayout(layoutNum); rawScreen.clearWidgetPart(); + final Collection workoutTypes = XiaomiWorkoutType.getWorkoutTypesSupportedByDevice(getDevice()); + for (final WidgetPart newPart : widgetScreen.getParts()) { // Find the existing raw part final XiaomiProto.WidgetPart knownRawPart = findRawPart( @@ -274,14 +289,21 @@ public class XiaomiWidgetManager implements WidgetManager { final XiaomiProto.WidgetPart.Builder newRawPartBuilder = XiaomiProto.WidgetPart.newBuilder(knownRawPart); + // TODO only support subtypes on widget with type 16 if (newPart.getSubtype() != null) { - // Get the workout type as subtype - final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice()); - for (final XiaomiWorkoutType workoutType : workoutTypes) { - if (newPart.getSubtype().getId().equals(String.valueOf(workoutType.getCode()))) { - newRawPartBuilder.setSubType(workoutType.getCode()); - break; + try { + final int rawSubtype = Integer.parseInt(newPart.getSubtype().getId()); + + // Get the workout type as subtype + for (final XiaomiWorkoutType workoutType : workoutTypes) { + if (rawSubtype == workoutType.getCode()) { + newRawPartBuilder.setSubType(workoutType.getCode()); + break; + } } + } catch (final NumberFormatException ex) { + LOG.error("Failed to convert workout type {} to a number, defaulting to 1", newPart.getSubtype()); + newRawPartBuilder.setSubType(1); } } @@ -355,6 +377,8 @@ public class XiaomiWidgetManager implements WidgetManager { return WidgetType.WIDE; case 3: return WidgetType.TALL; + case 4: + return WidgetType.LARGE; default: LOG.warn("Unknown widget type {}", rawType); return null; @@ -369,6 +393,8 @@ public class XiaomiWidgetManager implements WidgetManager { return 2; case TALL: return 3; + case LARGE: + return 4; default: throw new IllegalArgumentException("Unknown widget type " + widgetType); } @@ -377,22 +403,57 @@ public class XiaomiWidgetManager implements WidgetManager { @Nullable private WidgetLayout fromRawLayout(final int rawLayout) { switch (rawLayout) { - case 1: + case 1: // 2x2, top 2x small, bottom 2x small return WidgetLayout.TOP_2_BOT_2; - case 2: + case 2: // 2x2, top wide, bottom 2x small return WidgetLayout.TOP_1_BOT_2; - case 4: + case 4: // 2x2, top 2x small, bottom wide return WidgetLayout.TOP_2_BOT_1; - case 256: + case 128: // 2x2, full screen + return WidgetLayout.TWO_BY_TWO_SINGLE; + case 256: // 1x2, top small, bottom small return WidgetLayout.TWO; - case 512: - return WidgetLayout.SINGLE; + case 512: // 1x2, full screen + return WidgetLayout.ONE_BY_TWO_SINGLE; + case 8: // 2x2, left tall, right 2x square + case 16: // 2x2, left 2x square, right tall + case 32: // 2x2, top wide, bottom wide + case 64: // 2x2, left tall, right tall + case 1024: // 2x3, top 2x square, bottom 2x2 square + case 2048: // 2x3, top 2x2 square, bottom 2x square + case 4096: // 2x3, top wide, bottom 2x2 square + case 8192: // 2x3, top 2x2 square, bottom wide + case 16384: // 2x3, full screen default: LOG.warn("Unknown widget screens layout {}", rawLayout); return null; } } + private int toRawLayout(final WidgetLayout layout) { + if (layout == null) { + return -1; + } + + switch (layout) { + case TOP_2_BOT_2: + return 1; + case TOP_1_BOT_2: + return 2; + case TOP_2_BOT_1: + return 4; + case TWO_BY_TWO_SINGLE: + return 128; + case TWO: + return 256; + case ONE_BY_TWO_SINGLE: + return 512; + default: + LOG.warn("Widget layout {} cannot be converted to raw variant", layout); + return -1; + } + } + @Nullable private XiaomiProto.WidgetPart findRawPart(final int type, final int id) { final XiaomiProto.WidgetParts rawWidgetParts = getRawWidgetParts(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java index 35fb453c0..0141710f4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java @@ -18,7 +18,16 @@ package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; import androidx.annotation.StringRes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class XiaomiWorkoutType { private final int code; @@ -58,4 +67,23 @@ public class XiaomiWorkoutType { return -1; } + + public static Collection getWorkoutTypesSupportedByDevice(final GBDevice device) { + final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())); + final List codes = prefs.getList(XiaomiPreferences.PREF_WORKOUT_TYPES, Collections.emptyList()); + final List ret = new ArrayList<>(codes.size()); + + for (final String code : codes) { + final int codeInt = Integer.parseInt(code); + final int codeNameStringRes = XiaomiWorkoutType.mapWorkoutName(codeInt); + ret.add(new XiaomiWorkoutType( + codeInt, + codeNameStringRes != -1 ? + GBApplication.getContext().getString(codeNameStringRes) : + GBApplication.getContext().getString(R.string.widget_unknown_workout, code) + )); + } + + return ret; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java index d325b972a..556c9f451 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java @@ -91,22 +91,4 @@ public final class XiaomiPreferences { final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); return prefs.getBoolean("keep_activity_data_on_device", false); } - - // FIXME this function should not be here - public static List getWorkoutTypes(final GBDevice gbDevice) { - final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); - final List codes = prefs.getList(PREF_WORKOUT_TYPES, Collections.emptyList()); - final List ret = new ArrayList<>(codes.size()); - for (final String code : codes) { - final int codeInt = Integer.parseInt(code); - final int codeNameStringRes = XiaomiWorkoutType.mapWorkoutName(codeInt); - ret.add(new XiaomiWorkoutType( - codeInt, - codeNameStringRes != -1 ? - GBApplication.getContext().getString(codeNameStringRes) : - GBApplication.getContext().getString(R.string.widget_unknown_workout, code) - )); - } - return ret; - } } diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index fdc84a849..24c759f51 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -238,7 +238,13 @@ message WidgetScreens { message WidgetsCapabilities { optional uint32 minWidgets = 1; // 1 optional uint32 maxWidgets = 2; // 7 - optional uint32 unknown3 = 3; // 768 + + // bitmap: + // - 0b0000_0011_0000_0000 (768) on bands + // - 0b0000_0000_0000_0111 (7) on some square/round devices (Watch S1 Active) + // - 0b0000_0000_1000_0111 (135) on some square/round devices (Redmi Watch 4) + // - 0b0111_1100_0000_0000 (31744) on portrait devices (Band 8 Pro) + optional uint32 supportedLayoutStyles = 3; } message WidgetScreen { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa5a87498..f85965d3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2707,6 +2707,8 @@ Move down Please select all widgets Unknown workout - %s + %1$s (colored tile) + Untitled widget (%1$s) Navigation instructions Configure on-watch navigation app behavior Come to foreground