Compare commits

...

11 Commits

Author SHA1 Message Date
Roberto Viola
3a250fe3a3 Enhance Virtual Gearing: Android-only visibility and coordinate feedback
- Make Virtual Gearing Device visible only on Android platform
- Add customizable coordinate settings for different cycling apps (MyWhoosh, IndieVelo, etc.)
- Implement app selection ComboBox with auto-populated default coordinates
- Add real-time coordinate customization UI with percentage-based values
- Show toast messages with exact touch coordinates when gear changes occur
- Integrate settings bridge between Java and C++ for dynamic coordinate access
- Add debug logging for screen dimensions and coordinate calculations
- Replace hardcoded coordinates with user-configurable system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 08:10:00 +02:00
Roberto Viola
094d2c88cb adding new files 2025-09-24 11:58:43 +02:00
Roberto Viola
7f3dda70fd Update settings.qml 2025-09-22 13:30:17 +02:00
Roberto Viola
79d96ba182 fix 2025-09-22 08:29:07 +02:00
Roberto Viola
5ec861dab3 Merge branch 'virtualgear_device_android' of https://github.com/cagnulein/qdomyos-zwift into virtualgear_device_android 2025-09-22 04:57:15 +02:00
Roberto Viola
f90edbd632 log 2025-09-22 04:57:00 +02:00
Roberto Viola
621ed69627 Merge branch 'master' into virtualgear_device_android 2025-09-21 14:33:30 +02:00
Roberto Viola
b754b7f773 other files 2025-09-21 14:04:07 +02:00
Roberto Viola
efb9dfbdb1 virtual gear device on android 2025-09-21 14:01:01 +02:00
Roberto Viola
a40fec4082 adding LSApplicationCategoryType on iOS 2025-09-21 07:38:50 +02:00
Roberto Viola
f6a9d8ca4e removed gears gain changes from Wizard.qml 2025-09-18 16:30:25 +02:00
20 changed files with 951 additions and 25 deletions

View File

@@ -371,4 +371,5 @@ The ProForm 995i implementation serves as the reference example:
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list
* Add the property at the END of the properties list
- #usa le qdebug invece che le emit debug

View File

@@ -4455,7 +4455,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1165;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4655,7 +4655,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1165;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -4891,7 +4891,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1165;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4987,7 +4987,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1165;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5079,7 +5079,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1166;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5195,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1164;
CURRENT_PROJECT_VERSION = 1166;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -845,7 +845,6 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 0.5;
stackViewLocal.push(finalStepComponent);
}
}
@@ -904,7 +903,6 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 1;
stackViewLocal.push(finalStepComponent);
}
}

View File

@@ -106,6 +106,16 @@
android:name=".ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
<service android:name=".VirtualGearingService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/virtual_gearing_service_config" />
</service>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="virtual_gearing_service_description">Virtual Gearing Service for QZ - Enables touch simulation for virtual shifting in cycling apps</string>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/virtual_gearing_service_description"
android:packageNames="@null"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true" />

View File

@@ -0,0 +1,116 @@
package org.cagnulen.qdomyoszwift;
import org.cagnulen.qdomyoszwift.QLog;
public class AppConfiguration {
private static final String TAG = "AppConfiguration";
public static class TouchCoordinate {
public final double xPercent;
public final double yPercent;
public TouchCoordinate(double xPercent, double yPercent) {
this.xPercent = xPercent;
this.yPercent = yPercent;
}
public int getX(int screenWidth) {
return (int) (screenWidth * xPercent);
}
public int getY(int screenHeight) {
return (int) (screenHeight * yPercent);
}
}
public static class AppConfig {
public final String appName;
public final String packageName;
public final TouchCoordinate shiftUp;
public final TouchCoordinate shiftDown;
public AppConfig(String appName, String packageName, TouchCoordinate shiftUp, TouchCoordinate shiftDown) {
this.appName = appName;
this.packageName = packageName;
this.shiftUp = shiftUp;
this.shiftDown = shiftDown;
}
}
// Predefined configurations based on SwiftControl
private static final AppConfig[] SUPPORTED_APPS = {
// MyWhoosh - coordinates from SwiftControl repository
new AppConfig(
"MyWhoosh",
"com.mywhoosh.whooshgame",
new TouchCoordinate(0.98, 0.94), // Shift Up - bottom right corner
new TouchCoordinate(0.80, 0.94) // Shift Down - more to the left
),
// IndieVelo / TrainingPeaks
new AppConfig(
"IndieVelo",
"com.indieVelo.client",
new TouchCoordinate(0.66, 0.74), // Shift Up - center right
new TouchCoordinate(0.575, 0.74) // Shift Down - center left
),
// Biketerra.com
new AppConfig(
"Biketerra",
"biketerra",
new TouchCoordinate(0.8, 0.5), // Generic coordinates for now
new TouchCoordinate(0.2, 0.5)
),
// Default configuration for unrecognized apps
new AppConfig(
"Default",
"*",
new TouchCoordinate(0.85, 0.9), // Conservative coordinates
new TouchCoordinate(0.15, 0.9)
)
};
public static AppConfig getConfigForPackage(String packageName) {
// Use custom coordinates from settings instead of hardcoded values
return getCurrentConfig();
}
// Get current configuration from user settings
public static AppConfig getCurrentConfig() {
try {
double shiftUpX = VirtualGearingBridge.getVirtualGearingShiftUpX();
double shiftUpY = VirtualGearingBridge.getVirtualGearingShiftUpY();
double shiftDownX = VirtualGearingBridge.getVirtualGearingShiftDownX();
double shiftDownY = VirtualGearingBridge.getVirtualGearingShiftDownY();
int appIndex = VirtualGearingBridge.getVirtualGearingApp();
String appName = "Custom";
if (appIndex >= 0 && appIndex < SUPPORTED_APPS.length) {
appName = SUPPORTED_APPS[appIndex].appName;
}
QLog.d(TAG, "Using custom coordinates: shiftUp(" + shiftUpX + "," + shiftUpY +
") shiftDown(" + shiftDownX + "," + shiftDownY + ") for " + appName);
return new AppConfig(
appName,
"*", // Package name not relevant for custom config
new TouchCoordinate(shiftUpX, shiftUpY),
new TouchCoordinate(shiftDownX, shiftDownY)
);
} catch (Exception e) {
QLog.e(TAG, "Error getting custom config, using fallback", e);
return getDefaultConfig();
}
}
public static AppConfig getDefaultConfig() {
return SUPPORTED_APPS[SUPPORTED_APPS.length - 1]; // Last element is the default
}
public static AppConfig[] getAllConfigs() {
return SUPPORTED_APPS;
}
}

View File

@@ -0,0 +1,145 @@
package org.cagnulen.qdomyoszwift;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import org.cagnulen.qdomyoszwift.QLog;
public class VirtualGearingBridge {
private static final String TAG = "VirtualGearingBridge";
public static boolean isAccessibilityServiceEnabled(Context context) {
String settingValue = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
QLog.d(TAG, "Enabled accessibility services: " + settingValue);
if (settingValue != null) {
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(':');
splitter.setString(settingValue);
while (splitter.hasNext()) {
String service = splitter.next();
QLog.d(TAG, "Checking service: " + service);
if (service.contains("org.cagnulen.qdomyoszwift/.VirtualGearingService") ||
service.contains("VirtualGearingService")) {
QLog.d(TAG, "VirtualGearingService is enabled");
return true;
}
}
}
QLog.d(TAG, "VirtualGearingService is not enabled");
return false;
}
public static void openAccessibilitySettings(Context context) {
try {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
QLog.d(TAG, "Opened accessibility settings");
} catch (Exception e) {
QLog.e(TAG, "Failed to open accessibility settings", e);
}
}
public static void simulateShiftUp() {
QLog.d(TAG, "Simulating shift up with app-specific coordinates");
VirtualGearingService.shiftUpSmart();
}
public static void simulateShiftDown() {
QLog.d(TAG, "Simulating shift down with app-specific coordinates");
VirtualGearingService.shiftDownSmart();
}
public static String getCurrentAppPackageName(Context context) {
try {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
ActivityManager.RunningAppProcessInfo myProcess = new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(myProcess);
// For Android 5.0+ we should use UsageStatsManager, but for simplicity
// we use a more direct approach via current foreground process
// In a complete implementation we should use UsageStatsManager
// For now return null and let the service detect the app
return null;
}
} catch (Exception e) {
QLog.e(TAG, "Error getting current app package name", e);
}
return null;
}
public static int[] getScreenSize(Context context) {
try {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return new int[]{displayMetrics.widthPixels, displayMetrics.heightPixels};
} catch (Exception e) {
QLog.e(TAG, "Error getting screen size", e);
return new int[]{1080, 1920}; // Default fallback
}
}
public static void simulateTouch(int x, int y) {
QLog.d(TAG, "Simulating touch at (" + x + ", " + y + ")");
VirtualGearingService.simulateKeypress(x, y);
}
public static boolean isServiceRunning() {
boolean running = VirtualGearingService.isServiceEnabled();
QLog.d(TAG, "Service running: " + running);
return running;
}
// Native methods to get settings from C++ side
public static native double getVirtualGearingShiftUpX();
public static native double getVirtualGearingShiftUpY();
public static native double getVirtualGearingShiftDownX();
public static native double getVirtualGearingShiftDownY();
public static native int getVirtualGearingApp();
// Methods to get coordinates that will be/were sent
public static String getShiftUpCoordinates() {
try {
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
// Use VirtualGearingService to get screen size (it has access to service context)
int[] screenSize = VirtualGearingService.getScreenSize();
int x = config.shiftUp.getX(screenSize[0]);
int y = config.shiftUp.getY(screenSize[1]);
return x + "," + y;
} catch (Exception e) {
QLog.e(TAG, "Error getting shift up coordinates", e);
return "0,0";
}
}
public static String getShiftDownCoordinates() {
try {
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
// Use VirtualGearingService to get screen size (it has access to service context)
int[] screenSize = VirtualGearingService.getScreenSize();
int x = config.shiftDown.getX(screenSize[0]);
int y = config.shiftDown.getY(screenSize[1]);
return x + "," + y;
} catch (Exception e) {
QLog.e(TAG, "Error getting shift down coordinates", e);
return "0,0";
}
}
public static String getLastTouchCoordinates() {
// For now, return the last coordinates that would be sent for shift up
// This could be enhanced to track actual last touch
return getShiftUpCoordinates();
}
}

View File

@@ -0,0 +1,152 @@
package org.cagnulen.qdomyoszwift;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import org.cagnulen.qdomyoszwift.QLog;
public class VirtualGearingService extends AccessibilityService {
private static final String TAG = "VirtualGearingService";
private static VirtualGearingService instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
QLog.d(TAG, "VirtualGearingService created");
}
@Override
public void onDestroy() {
super.onDestroy();
instance = null;
QLog.d(TAG, "VirtualGearingService destroyed");
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// Capture foreground app package name for smart coordinates
if (event != null && event.getPackageName() != null) {
String packageName = event.getPackageName().toString();
if (!packageName.equals(currentPackageName)) {
currentPackageName = packageName;
QLog.d(TAG, "App changed to: " + packageName);
}
}
}
@Override
public void onInterrupt() {
QLog.d(TAG, "VirtualGearingService interrupted");
}
public static boolean isServiceEnabled() {
return instance != null;
}
public static void simulateKeypress(int x, int y) {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate keypress");
return;
}
try {
GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(x, y);
path.lineTo(x + 1, y);
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(
path, 0, ViewConfiguration.getTapTimeout(), false);
gestureBuilder.addStroke(stroke);
instance.dispatchGesture(gestureBuilder.build(), null, null);
QLog.d(TAG, "Simulated keypress at (" + x + ", " + y + ")");
} catch (Exception e) {
QLog.e(TAG, "Error simulating keypress", e);
}
}
// Legacy methods for backward compatibility
public static void shiftUp() {
QLog.d(TAG, "Using legacy shiftUp - consider using shiftUpSmart()");
simulateKeypress(100, 200);
}
public static void shiftDown() {
QLog.d(TAG, "Using legacy shiftDown - consider using shiftDownSmart()");
simulateKeypress(100, 300);
}
// New smart methods with app-specific coordinates
public static void shiftUpSmart() {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate smart shift up");
return;
}
try {
// Try to detect app from package name of last AccessibilityEvent
String currentPackage = getCurrentPackageName();
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
// Calculate coordinates based on screen dimensions
int[] screenSize = getScreenSize();
int x = config.shiftUp.getX(screenSize[0]);
int y = config.shiftUp.getY(screenSize[1]);
QLog.d(TAG, "Smart shift up for " + config.appName + " at (" + x + ", " + y + ")");
simulateKeypress(x, y);
} catch (Exception e) {
QLog.e(TAG, "Error in shiftUpSmart, falling back to legacy", e);
shiftUp();
}
}
public static void shiftDownSmart() {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate smart shift down");
return;
}
try {
String currentPackage = getCurrentPackageName();
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
int[] screenSize = getScreenSize();
int x = config.shiftDown.getX(screenSize[0]);
int y = config.shiftDown.getY(screenSize[1]);
QLog.d(TAG, "Smart shift down for " + config.appName + " at (" + x + ", " + y + ")");
simulateKeypress(x, y);
} catch (Exception e) {
QLog.e(TAG, "Error in shiftDownSmart, falling back to legacy", e);
shiftDown();
}
}
private static String currentPackageName = null;
private static String getCurrentPackageName() {
return currentPackageName != null ? currentPackageName : "unknown";
}
public static int[] getScreenSize() {
if (instance != null) {
try {
android.content.res.Resources resources = instance.getResources();
android.util.DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
QLog.d(TAG, "Screen size: " + width + "x" + height + " (density=" + displayMetrics.density + ")");
return new int[]{width, height};
} catch (Exception e) {
QLog.e(TAG, "Error getting screen size from service", e);
}
}
QLog.w(TAG, "Using fallback screen size");
return new int[]{1080, 1920}; // Default fallback
}
}

View File

@@ -2,6 +2,7 @@
#include "devices/bike.h"
#include "qdebugfixup.h"
#include "homeform.h"
#include "virtualgearingdevice.h"
#include <QSettings>
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
@@ -466,7 +467,81 @@ double bike::gearsZwiftRatio() {
case 23:
return 5.14;
case 24:
return 5.49;
return 5.49;
}
return 1;
}
void bike::gearUp() {
QSettings settings;
// Check if virtual gearing device is enabled
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
#ifdef Q_OS_ANDROID
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
if (vgd) {
// Check if accessibility service is enabled
if (!vgd->isAccessibilityServiceEnabled()) {
static bool warned = false;
if (!warned) {
qDebug() << "bike::gearUp() - VirtualGearingService not enabled in accessibility settings";
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
warned = true;
}
} else if (vgd->isServiceRunning()) {
qDebug() << "bike::gearUp() - Using virtual gearing device";
QString coordinates = vgd->getShiftUpCoordinates();
vgd->simulateShiftUp();
// Show toast with coordinates
homeform::singleton()->setToastRequested("Virtual Gear Up → " + coordinates);
return;
} else {
qDebug() << "bike::gearUp() - Virtual gearing service not running, falling back to normal gearing";
}
}
#endif
}
// Normal gearing logic
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() + (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
void bike::gearDown() {
QSettings settings;
// Check if virtual gearing device is enabled
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
#ifdef Q_OS_ANDROID
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
if (vgd) {
// Check if accessibility service is enabled
if (!vgd->isAccessibilityServiceEnabled()) {
static bool warned = false;
if (!warned) {
qDebug() << "bike::gearDown() - VirtualGearingService not enabled in accessibility settings";
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
warned = true;
}
} else if (vgd->isServiceRunning()) {
qDebug() << "bike::gearDown() - Using virtual gearing device";
QString coordinates = vgd->getShiftDownCoordinates();
vgd->simulateShiftDown();
// Show toast with coordinates
homeform::singleton()->setToastRequested("Virtual Gear Down → " + coordinates);
return;
} else {
qDebug() << "bike::gearDown() - Virtual gearing service not running, falling back to normal gearing";
}
}
#endif
}
// Normal gearing logic
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() - (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}

View File

@@ -64,18 +64,8 @@ class bike : public bluetoothdevice {
void changeInclination(double grade, double percentage) override;
virtual void changeSteeringAngle(double angle) { m_steeringAngle = angle; }
virtual void resistanceFromFTMSAccessory(resistance_t res) { Q_UNUSED(res); }
void gearUp() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() + (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
void gearDown() {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() - (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
void gearUp();
void gearDown();
Q_SIGNALS:
void bikeStarted();

View File

@@ -39,6 +39,8 @@
<array>
<string>gcm-ciq</string>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.healthcare-fitness</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>

View File

@@ -30,6 +30,7 @@
#include "mqttpublisher.h"
#include "androidstatusbar.h"
#include "fontmanager.h"
#include "virtualgearingdevice.h"
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
@@ -782,6 +783,12 @@ int main(int argc, char *argv[]) {
bikeResistanceOffset,
bikeResistanceGain); // FIXED: clang-analyzer-cplusplus.NewDeleteLeaks - potential leak
#ifdef Q_OS_ANDROID
// Initialize VirtualGearingDevice for Android keypress simulation
VirtualGearingDevice* vgd = new VirtualGearingDevice();
Q_UNUSED(vgd)
#endif
QString mqtt_host = settings.value(QZSettings::mqtt_host, QZSettings::default_mqtt_host).toString();
int mqtt_port = settings.value(QZSettings::mqtt_port, QZSettings::default_mqtt_port).toInt();
QString mqtt_username = settings.value(QZSettings::mqtt_username, QZSettings::default_mqtt_username).toString();
@@ -807,6 +814,7 @@ int main(int argc, char *argv[]) {
#endif
{
AndroidStatusBar::registerQmlType();
VirtualGearingDevice::registerQmlType();
#ifdef Q_OS_ANDROID
FontManager fontManager;

View File

@@ -9,6 +9,7 @@ import org.cagnulein.qdomyoszwift 1.0
import QtQuick.Window 2.12
import Qt.labs.platform 1.1
import AndroidStatusBar 1.0
import VirtualGearingDevice 1.0
ApplicationWindow {
id: window

View File

@@ -869,6 +869,7 @@ DISTFILES += \
$$PWD/android/libs/ciq-companion-app-sdk-2.0.3.aar \
$$PWD/android/libs/zaplibrary-debug.aar \
$$PWD/android/res/xml/device_filter.xml \
$$PWD/android/src/AppConfiguration.java \
$$PWD/android/src/BikeChannelController.java \
$$PWD/android/src/BleAdvertiser.java \
$$PWD/android/src/CSafeRowerUSBHID.java \
@@ -883,6 +884,8 @@ DISTFILES += \
$$PWD/android/src/QLog.java \
$$PWD/android/src/ScreenCaptureService.java \
$$PWD/android/src/Shortcuts.java \
$$PWD/android/src/VirtualGearingBridge.java \
$$PWD/android/src/VirtualGearingService.java \
$$PWD/android/src/WearableController.java \
$$PWD/android/src/WearableMessageListenerService.java \
$$PWD/android/src/ZapClickLayer.java \
@@ -981,12 +984,14 @@ ios {
HEADERS += \
mqttpublisher.h \
androidstatusbar.h \
fontmanager.h
fontmanager.h \
virtualgearingdevice.h
SOURCES += \
mqttpublisher.cpp \
androidstatusbar.cpp \
fontmanager.cpp
fontmanager.cpp \
virtualgearingdevice.cpp
include($$PWD/purchasing/purchasing.pri)
INCLUDEPATH += purchasing/qmltypes

View File

@@ -981,11 +981,17 @@ const QString QZSettings::chart_display_mode = QStringLiteral("chart_display_mod
const QString QZSettings::calories_active_only = QStringLiteral("calories_active_only");
const QString QZSettings::calories_from_hr = QStringLiteral("calories_from_hr");
const QString QZSettings::height = QStringLiteral("height");
const QString QZSettings::virtual_gearing_device = QStringLiteral("virtual_gearing_device");
const QString QZSettings::virtual_gearing_shift_up_x = QStringLiteral("virtual_gearing_shift_up_x");
const QString QZSettings::virtual_gearing_shift_up_y = QStringLiteral("virtual_gearing_shift_up_y");
const QString QZSettings::virtual_gearing_shift_down_x = QStringLiteral("virtual_gearing_shift_down_x");
const QString QZSettings::virtual_gearing_shift_down_y = QStringLiteral("virtual_gearing_shift_down_y");
const QString QZSettings::virtual_gearing_app = QStringLiteral("virtual_gearing_app");
const QString QZSettings::taurua_ic90 = QStringLiteral("taurua_ic90");
const QString QZSettings::proform_csx210 = QStringLiteral("proform_csx210");
const uint32_t allSettingsCount = 807;
const uint32_t allSettingsCount = 813;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1814,6 +1820,12 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::taurua_ic90, QZSettings::default_taurua_ic90},
{QZSettings::proform_csx210, QZSettings::default_proform_csx210},
{QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed},
{QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device},
{QZSettings::virtual_gearing_shift_up_x, QZSettings::default_virtual_gearing_shift_up_x},
{QZSettings::virtual_gearing_shift_up_y, QZSettings::default_virtual_gearing_shift_up_y},
{QZSettings::virtual_gearing_shift_down_x, QZSettings::default_virtual_gearing_shift_down_x},
{QZSettings::virtual_gearing_shift_down_y, QZSettings::default_virtual_gearing_shift_down_y},
{QZSettings::virtual_gearing_app, QZSettings::default_virtual_gearing_app},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -2693,12 +2693,30 @@ class QZSettings {
static const QString height;
static constexpr double default_height = 175.0;
static const QString virtual_gearing_device;
static constexpr bool default_virtual_gearing_device = false;
// Virtual Gearing - Generic coordinate settings (app-agnostic)
static const QString virtual_gearing_shift_up_x;
static constexpr double default_virtual_gearing_shift_up_x = 0.98;
static const QString virtual_gearing_shift_up_y;
static constexpr double default_virtual_gearing_shift_up_y = 0.94;
static const QString virtual_gearing_shift_down_x;
static constexpr double default_virtual_gearing_shift_down_x = 0.80;
static const QString virtual_gearing_shift_down_y;
static constexpr double default_virtual_gearing_shift_down_y = 0.94;
// Virtual Gearing - App selection
static const QString virtual_gearing_app;
static constexpr int default_virtual_gearing_app = 0; // 0=MyWhoosh default
static const QString taurua_ic90;
static constexpr bool default_taurua_ic90 = false;
static const QString proform_csx210;
static constexpr bool default_proform_csx210 = false;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -5,6 +5,7 @@ import QtQuick.Controls.Material 2.0
import Qt.labs.settings 1.0
import QtQuick.Dialogs 1.0
import Qt.labs.platform 1.1
import VirtualGearingDevice 1.0
//Page {
ScrollView {
@@ -1207,6 +1208,12 @@ import Qt.labs.platform 1.1
property bool toorxtreadmill_discovery_completed: false
property bool taurua_ic90: false
property bool proform_csx210: false
property bool virtual_gearing_device: false
property double virtual_gearing_shift_up_x: 0.98
property double virtual_gearing_shift_up_y: 0.94
property double virtual_gearing_shift_down_x: 0.80
property double virtual_gearing_shift_down_y: 0.94
property int virtual_gearing_app: 0
}
@@ -12878,6 +12885,145 @@ import Qt.labs.platform 1.1
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
id: virtualGearingDeviceDelegate
text: qsTr("Virtual Gearing Device")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.virtual_gearing_device
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
visible: Qt.platform.os === "android"
onClicked: {
settings.virtual_gearing_device = checked;
if (checked) {
// Auto-enable Android notification and fake bike when virtual gearing is enabled
settings.android_notification = true;
settings.virtual_device_enabled = true;
}
window.settings_restart_to_apply = true;
}
}
Label {
text: qsTr("Android Only: enables virtual gearing through keypress simulation for third-party apps like MyWhoosh and indieVelo. Uses Zwift Play/Click controls to send shift commands.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
visible: Qt.platform.os === "android"
}
Button {
text: qsTr("Open Accessibility Settings")
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
visible: settings.virtual_gearing_device && Qt.platform.os === "android"
onClicked: {
VirtualGearingDevice.openAccessibilitySettings()
}
}
// App Selection ComboBox
Row {
visible: settings.virtual_gearing_device && Qt.platform.os === "android"
Layout.fillWidth: true
spacing: 10
Label {
text: qsTr("Target App:")
anchors.verticalCenter: parent.verticalCenter
}
ComboBox {
id: virtualGearingAppCombo
model: ["MyWhoosh", "IndieVelo", "Biketerra", "RGT Cycling", "Zwift"]
currentIndex: settings.virtual_gearing_app
onCurrentIndexChanged: {
settings.virtual_gearing_app = currentIndex;
// Auto-populate coordinates based on selected app
if (currentIndex === 0) { // MyWhoosh
settings.virtual_gearing_shift_up_x = 0.98;
settings.virtual_gearing_shift_up_y = 0.94;
settings.virtual_gearing_shift_down_x = 0.80;
settings.virtual_gearing_shift_down_y = 0.94;
} else if (currentIndex === 1) { // IndieVelo
settings.virtual_gearing_shift_up_x = 0.66;
settings.virtual_gearing_shift_up_y = 0.74;
settings.virtual_gearing_shift_down_x = 0.575;
settings.virtual_gearing_shift_down_y = 0.74;
} else if (currentIndex === 2) { // Biketerra
settings.virtual_gearing_shift_up_x = 0.8;
settings.virtual_gearing_shift_up_y = 0.5;
settings.virtual_gearing_shift_down_x = 0.2;
settings.virtual_gearing_shift_down_y = 0.5;
} else { // RGT Cycling, Zwift and others
settings.virtual_gearing_shift_up_x = 0.95;
settings.virtual_gearing_shift_up_y = 0.85;
settings.virtual_gearing_shift_down_x = 0.75;
settings.virtual_gearing_shift_down_y = 0.85;
}
}
}
}
// Coordinate Customization
GridLayout {
visible: settings.virtual_gearing_device && Qt.platform.os === "android"
Layout.fillWidth: true
columns: 4
Label { text: qsTr("Shift Up X:") }
TextField {
text: settings.virtual_gearing_shift_up_x.toFixed(3)
onAccepted: settings.virtual_gearing_shift_up_x = parseFloat(text)
validator: DoubleValidator { bottom: 0.0; top: 1.0; decimals: 3 }
}
Label { text: qsTr("Shift Up Y:") }
TextField {
text: settings.virtual_gearing_shift_up_y.toFixed(3)
onAccepted: settings.virtual_gearing_shift_up_y = parseFloat(text)
validator: DoubleValidator { bottom: 0.0; top: 1.0; decimals: 3 }
}
Label { text: qsTr("Shift Down X:") }
TextField {
text: settings.virtual_gearing_shift_down_x.toFixed(3)
onAccepted: settings.virtual_gearing_shift_down_x = parseFloat(text)
validator: DoubleValidator { bottom: 0.0; top: 1.0; decimals: 3 }
}
Label { text: qsTr("Shift Down Y:") }
TextField {
text: settings.virtual_gearing_shift_down_y.toFixed(3)
onAccepted: settings.virtual_gearing_shift_down_y = parseFloat(text)
validator: DoubleValidator { bottom: 0.0; top: 1.0; decimals: 3 }
}
}
Label {
visible: settings.virtual_gearing_device && Qt.platform.os === "android"
text: qsTr("Coordinates are percentages (0.0-1.0) of screen dimensions. Select an app above to auto-populate with default values, then customize as needed.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
text: qsTr("Android Force Documents/QZ Folder")
spacing: 0

View File

@@ -0,0 +1,202 @@
#include "virtualgearingdevice.h"
#include "qzsettings.h"
#include <QDebug>
#include <QQmlEngine>
#include <QSettings>
#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include <QAndroidJniEnvironment>
#include <QAndroidJniObject>
#include <jni.h>
#endif
VirtualGearingDevice* VirtualGearingDevice::m_instance = nullptr;
VirtualGearingDevice::VirtualGearingDevice(QObject *parent) : QObject(parent)
{
m_instance = this;
}
VirtualGearingDevice* VirtualGearingDevice::instance()
{
return m_instance;
}
void VirtualGearingDevice::registerQmlType()
{
qmlRegisterSingletonType<VirtualGearingDevice>("VirtualGearingDevice", 1, 0, "VirtualGearingDevice",
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return instance();
});
}
bool VirtualGearingDevice::isAccessibilityServiceEnabled()
{
#ifdef Q_OS_ANDROID
QAndroidJniObject activity = QtAndroid::androidActivity();
if (activity.isValid()) {
return QAndroidJniObject::callStaticMethod<jboolean>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"isAccessibilityServiceEnabled",
"(Landroid/content/Context;)Z",
activity.object<jobject>());
}
#endif
return false;
}
void VirtualGearingDevice::openAccessibilitySettings()
{
#ifdef Q_OS_ANDROID
QAndroidJniObject activity = QtAndroid::androidActivity();
if (activity.isValid()) {
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"openAccessibilitySettings",
"(Landroid/content/Context;)V",
activity.object<jobject>());
}
#endif
}
void VirtualGearingDevice::simulateShiftUp()
{
#ifdef Q_OS_ANDROID
qDebug() << "VirtualGearingDevice: Simulating shift up";
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"simulateShiftUp",
"()V");
#endif
}
void VirtualGearingDevice::simulateShiftDown()
{
#ifdef Q_OS_ANDROID
qDebug() << "VirtualGearingDevice: Simulating shift down";
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"simulateShiftDown",
"()V");
#endif
}
void VirtualGearingDevice::simulateTouch(int x, int y)
{
#ifdef Q_OS_ANDROID
qDebug() << "VirtualGearingDevice: Simulating touch at (" << x << ", " << y << ")";
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"simulateTouch",
"(II)V",
x, y);
#endif
}
bool VirtualGearingDevice::isServiceRunning()
{
#ifdef Q_OS_ANDROID
return QAndroidJniObject::callStaticMethod<jboolean>(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"isServiceRunning",
"()Z");
#endif
return false;
}
#ifdef Q_OS_ANDROID
// JNI implementations for settings access
extern "C" {
JNIEXPORT jdouble JNICALL
Java_org_cagnulen_qdomyoszwift_VirtualGearingBridge_getVirtualGearingShiftUpX(JNIEnv *env, jclass clazz)
{
Q_UNUSED(env)
Q_UNUSED(clazz)
QSettings settings;
return settings.value(QZSettings::virtual_gearing_shift_up_x, QZSettings::default_virtual_gearing_shift_up_x).toDouble();
}
JNIEXPORT jdouble JNICALL
Java_org_cagnulen_qdomyoszwift_VirtualGearingBridge_getVirtualGearingShiftUpY(JNIEnv *env, jclass clazz)
{
Q_UNUSED(env)
Q_UNUSED(clazz)
QSettings settings;
return settings.value(QZSettings::virtual_gearing_shift_up_y, QZSettings::default_virtual_gearing_shift_up_y).toDouble();
}
JNIEXPORT jdouble JNICALL
Java_org_cagnulen_qdomyoszwift_VirtualGearingBridge_getVirtualGearingShiftDownX(JNIEnv *env, jclass clazz)
{
Q_UNUSED(env)
Q_UNUSED(clazz)
QSettings settings;
return settings.value(QZSettings::virtual_gearing_shift_down_x, QZSettings::default_virtual_gearing_shift_down_x).toDouble();
}
JNIEXPORT jdouble JNICALL
Java_org_cagnulen_qdomyoszwift_VirtualGearingBridge_getVirtualGearingShiftDownY(JNIEnv *env, jclass clazz)
{
Q_UNUSED(env)
Q_UNUSED(clazz)
QSettings settings;
return settings.value(QZSettings::virtual_gearing_shift_down_y, QZSettings::default_virtual_gearing_shift_down_y).toDouble();
}
JNIEXPORT jint JNICALL
Java_org_cagnulen_qdomyoszwift_VirtualGearingBridge_getVirtualGearingApp(JNIEnv *env, jclass clazz)
{
Q_UNUSED(env)
Q_UNUSED(clazz)
QSettings settings;
return settings.value(QZSettings::virtual_gearing_app, QZSettings::default_virtual_gearing_app).toInt();
}
} // extern "C"
#endif
QString VirtualGearingDevice::getLastTouchCoordinates()
{
#ifdef Q_OS_ANDROID
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"getLastTouchCoordinates",
"()Ljava/lang/String;");
if (result.isValid()) {
return result.toString();
}
#endif
return "0,0";
}
QString VirtualGearingDevice::getShiftUpCoordinates()
{
#ifdef Q_OS_ANDROID
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"getShiftUpCoordinates",
"()Ljava/lang/String;");
if (result.isValid()) {
return result.toString();
}
#endif
return "0,0";
}
QString VirtualGearingDevice::getShiftDownCoordinates()
{
#ifdef Q_OS_ANDROID
QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(
"org/cagnulen/qdomyoszwift/VirtualGearingBridge",
"getShiftDownCoordinates",
"()Ljava/lang/String;");
if (result.isValid()) {
return result.toString();
}
#endif
return "0,0";
}

View File

@@ -0,0 +1,31 @@
#ifndef VIRTUALGEARINGDEVICE_H
#define VIRTUALGEARINGDEVICE_H
#include <QObject>
#include <QQmlEngine>
class VirtualGearingDevice : public QObject
{
Q_OBJECT
public:
explicit VirtualGearingDevice(QObject *parent = nullptr);
static VirtualGearingDevice* instance();
static void registerQmlType();
public slots:
bool isAccessibilityServiceEnabled();
void openAccessibilitySettings();
void simulateShiftUp();
void simulateShiftDown();
void simulateTouch(int x, int y);
bool isServiceRunning();
QString getLastTouchCoordinates();
QString getShiftUpCoordinates();
QString getShiftDownCoordinates();
private:
static VirtualGearingDevice* m_instance;
};
#endif // VIRTUALGEARINGDEVICE_H