From a1fa2b11a6d7f0786fb8eb07d9f9583b80e78fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Mon, 20 May 2024 23:29:49 +0100 Subject: [PATCH] Garmin: App management (WIP) --- .../appmanager/AppManagerActivity.java | 16 ++--- .../devices/garmin/GarminCoordinator.java | 23 +++++++ .../service/devices/garmin/GarminSupport.java | 25 ++++++++ .../devices/garmin/ProtocolBufferHandler.java | 45 ++++++++++++++ .../garmin/apps/GarminAppsService.java | 15 +++++ .../garmin/gdi_installed_apps_service.proto | 61 +++++++++++++++++++ .../main/proto/garmin/gdi_smart_proto.proto | 38 +----------- 7 files changed, 179 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/apps/GarminAppsService.java create mode 100644 app/src/main/proto/garmin/gdi_installed_apps_service.proto diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java index 9647d1aaa..1e1abd517 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java @@ -92,20 +92,20 @@ public class AppManagerActivity extends AbstractGBFragmentActivity { enabledTabsList.add("watchfaces"); } - FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - assert fab != null; - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + FloatingActionButton fab = findViewById(R.id.fab); + if (coordinator.supportsFlashing()) { + fab.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); startActivityForResult(intent, READ_REQUEST_CODE); - } - }); + }); + } else { + fab.setVisibility(View.GONE); + } // Set up the ViewPager with the sections adapter. - ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager); + ViewPager viewPager = findViewById(R.id.appmanager_pager); if (viewPager != null) { viewPager.setAdapter(getPagerAdapter()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index fe3003f26..f15d44b57 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin; +import android.app.Activity; + import androidx.annotation.NonNull; import java.util.List; @@ -7,6 +9,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; @@ -218,6 +221,26 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsAppsManagement(final GBDevice device) { + return true; + } + + @Override + public boolean supportsCachedAppManagement(GBDevice device) { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return AppManagerActivity.class; + } + + @Override + public boolean supportsAppListFetching() { + return true; + } + public boolean supportsAgpsUpdates(final GBDevice device) { return !getPrefs(device).getString(GarminPreferences.PREF_AGPS_KNOWN_URLS, "").isEmpty(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 8b783b430..ef90bdfbe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCore; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDeviceStatus; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch; +import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiInstalledAppsService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSettingsService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; @@ -314,6 +315,30 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni sendOutgoingMessage("delete notification " + id, notificationsHandler.onDeleteNotification(id)); } + @Override + public void onAppInfoReq() { + sendOutgoingMessage( + "request apps", + protocolBufferHandler.prepareProtobufRequest( + GdiSmartProto.Smart.newBuilder().setInstalledAppsService( + GdiInstalledAppsService.InstalledAppsService.newBuilder().setGetInstalledAppsRequest( + GdiInstalledAppsService.InstalledAppsService.GetInstalledAppsRequest.newBuilder() + .setAppType(GdiInstalledAppsService.InstalledAppsService.AppType.ALL) + ) + ).build() + ) + ); + } + + @Override + public void onAppStart(final UUID uuid, final boolean start) { + + } + + @Override + public void onAppDelete(final UUID uuid) { + + } @Override public void onSendWeather(final ArrayList weatherSpecs) { //todo: find the closest one relative to the requested lat/long diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java index c24d432f3..fe94d02b2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java @@ -14,13 +14,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCalendarService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCore; @@ -28,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDataTransferService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDeviceStatus; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService; +import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiInstalledAppsService; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmsNotification; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.DataTransferHandler; @@ -119,6 +123,47 @@ public class ProtocolBufferHandler implements MessageHandler { processed = true; processProtobufFindMyWatchResponse(smart.getFindMyWatchService()); } + if (smart.hasInstalledAppsService()) { + processed = true; + final List apps = new ArrayList<>(); + + if (smart.getInstalledAppsService().hasGetInstalledAppsResponse()) { + final GdiInstalledAppsService.InstalledAppsService.GetInstalledAppsResponse installedAppsResponse = smart.getInstalledAppsService().getGetInstalledAppsResponse(); + for (final GdiInstalledAppsService.InstalledAppsService.InstalledApp installedApp : installedAppsResponse.getInstalledAppsList()) { + GBDeviceApp.Type type = GBDeviceApp.Type.UNKNOWN; + + switch (installedApp.getType()) { + case WATCH_APP: + type = GBDeviceApp.Type.APP_SYSTEM; + continue; + case WIDGET: + type = GBDeviceApp.Type.APP_GENERIC; + break; + case WATCH_FACE: + type = GBDeviceApp.Type.WATCHFACE; + break; + case DATA_FIELD: + type = GBDeviceApp.Type.APP_ACTIVITYTRACKER; + break; + case ACTIVITY: + type = GBDeviceApp.Type.APP_ACTIVITYTRACKER; + continue; + } + + apps.add(new GBDeviceApp( + UUID.nameUUIDFromBytes(installedApp.getStoreAppId().toByteArray()), + installedApp.getName(), + "", + String.valueOf(installedApp.getVersion()), + type + )); + } + + final GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo(); + appInfoCmd.apps = apps.toArray(new GBDeviceApp[0]); + deviceSupport.evaluateGBDeviceEvent(appInfoCmd); + } + } if (!processed) { LOG.warn("Unknown protobuf request: {}", smart); message.setStatusMessage(new ProtobufStatusMessage(message.getMessageType(), GFDIMessage.Status.ACK, message.getRequestId(), message.getDataOffset(), ProtobufStatusMessage.ProtobufChunkStatus.DISCARDED, ProtobufStatusMessage.ProtobufStatusCode.UNKNOWN_REQUEST_ID)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/apps/GarminAppsService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/apps/GarminAppsService.java new file mode 100644 index 000000000..ff3298806 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/apps/GarminAppsService.java @@ -0,0 +1,15 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.apps; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; + +public class GarminAppsService { + private final GarminSupport deviceSupport; + + public GarminAppsService(final GarminSupport deviceSupport) { + this.deviceSupport = deviceSupport; + } + + public void requestAppList() { + + } +} diff --git a/app/src/main/proto/garmin/gdi_installed_apps_service.proto b/app/src/main/proto/garmin/gdi_installed_apps_service.proto new file mode 100644 index 000000000..4e2ec9445 --- /dev/null +++ b/app/src/main/proto/garmin/gdi_installed_apps_service.proto @@ -0,0 +1,61 @@ +syntax = "proto2"; + +package garmin_vivomovehr; + +option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin"; + +message InstalledAppsService { + enum AppType { + UNKNOWN_APP_TYPE = 0; + WATCH_APP = 1; + WIDGET = 2; + WATCH_FACE = 3; + DATA_FIELD = 4; + ALL = 5; + NONE = 6; + AUDIO_CONTENT_PROVIDER = 7; + ACTIVITY = 8; + } + + message InstalledApp { + required bytes storeAppId = 1; + required AppType type = 2; + required string name = 3; + required bool disabled = 4; + optional uint32 version = 5; + optional string fileName = 6; + optional uint64 fileSize = 7; + optional uint32 nativeAppId = 8; + optional bool favorite = 9; + } + + optional GetInstalledAppsRequest getInstalledAppsRequest = 1; + optional GetInstalledAppsResponse getInstalledAppsResponse = 2; + optional DeleteAppRequest deleteAppRequest = 3; + optional DeleteAppResponse deleteAppResponse = 4; + + message GetInstalledAppsRequest { + required AppType appType = 1; + } + + message GetInstalledAppsResponse { + required uint64 availableSpace = 1; + required uint64 availableSlots = 2; + repeated InstalledApp installedApps = 3; + } + + message DeleteAppRequest { + required bytes storeAppId = 1; + required AppType appType = 2; + } + + message DeleteAppResponse { + enum Status { + UNKNOWN_STATUS = 0; + OK = 1; + FAILED_TO_DELETE = 2; + } + + required Status status = 1; + } +} diff --git a/app/src/main/proto/garmin/gdi_smart_proto.proto b/app/src/main/proto/garmin/gdi_smart_proto.proto index 218592db3..406d41c4f 100644 --- a/app/src/main/proto/garmin/gdi_smart_proto.proto +++ b/app/src/main/proto/garmin/gdi_smart_proto.proto @@ -8,6 +8,7 @@ import "garmin/gdi_device_status.proto"; import "garmin/gdi_find_my_watch.proto"; import "garmin/gdi_core.proto"; import "garmin/gdi_http_service.proto"; +import "garmin/gdi_installed_apps_service.proto"; import "garmin/gdi_data_transfer_service.proto"; import "garmin/gdi_sms_notification.proto"; import "garmin/gdi_calendar_service.proto"; @@ -16,6 +17,7 @@ import "garmin/gdi_settings_service.proto"; message Smart { optional CalendarService calendar_service = 1; optional HttpService http_service = 2; + optional InstalledAppsService installed_apps_service = 3; optional DataTransferService data_transfer_service = 7; optional DeviceStatusService device_status_service = 8; optional FindMyWatchService find_my_watch_service = 12; @@ -23,39 +25,3 @@ message Smart { optional SmsNotificationService sms_notification_service = 16; optional SettingsService settings_service = 42; } - -/* -1: CALENDAR_EVENTS_SERVICE_FIELD_NUMBER -2: CONNECT_IQ_HTTP_SERVICE_FIELD_NUMBER -3: CONNECT_IQ_INSTALLED_APPS_SERVICE_FIELD_NUMBER -4: CONNECT_IQ_APP_SETTINGS_SERVICE_FIELD_NUMBER -5: INTERNATIONAL_GOLF_SERVICE_FIELD_NUMBER -6: SWING_SENSOR_SERVICE_FIELD_NUMBER -7: DATA_TRANSFER_SERVICE_FIELD_NUMBER -8: DEVICE_STATUS_SERVICE_FIELD_NUMBER -9: INCIDENT_DETECTION_SERVICE_FIELD_NUMBER -10: AUDIO_PROMPTS_SERVICE_FIELD_NUMBER -11: WIFI_SETUP_SERVICE_FIELD_NUMBER -12: FIND_MY_WATCH_SERVICE_FIELD_NUMBER -13: CORE_SERVICE_FIELD_NUMBER -14: GROUP_LIVE_TRACK_SERVICE_FIELD_NUMBER -15: EXPRESSPAY_COMMAND_SERVICE_FIELD_NUMBER -16: SMS_NOTIFICATION_SERVICE_FIELD_NUMBER -17: LIVE_TRACK_MESSAGING_SERVICE_FIELD_NUMBER -18: INSTANT_INPUT_SERVICE_FIELD_NUMBER -19: SPORT_PROFILE_SETUP_SERVICE_FIELD_NUMBER -20: HSA_DATA_SERVICE_FIELD_NUMBER -21: LIVE_TRACK_SERVICE_FIELD_NUMBER -22: EXPLORE_SYNC_SERVICE_FIELD_NUMBER -23: WAY_POINT_TRANSFER_SERVICE_FIELD_NUMBER -24: DEVICE_MESSAGE_SERVICE_FIELD_NUMBER -25: LTE_SERVICE_FIELD_NUMBER -26: ANTI_THEFT_ALARM_SERVICE_FIELD_NUMBER -27: CREDENTIALS_SERVICE_FIELD_NUMBER -28: INREACH_TRACKING_SERVICE_FIELD_NUMBER -29: INREACH_MESSAGING_SERVICE_FIELD_NUMBER -30: EVENT_SHARING_FIELD_NUMBER -31: GENERIC_ITEM_TRANSFER_SERVICE_FIELD_NUMBER -32: INREACH_CONTACT_SYNC_SERVICE_FIELD_NUMBER -33: HAND_CALIBRATION_SERVICE_FIELD_NUMBER -*/