mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
10 Commits
Mobi-Rower
...
Google-And
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e346809270 | ||
|
|
fc68dec7ba | ||
|
|
4acb5029b6 | ||
|
|
71b4894e2d | ||
|
|
ac874bd682 | ||
|
|
8143ca110d | ||
|
|
e2d62099c3 | ||
|
|
d028fa4aa4 | ||
|
|
861e01c3f8 | ||
|
|
46d259994b |
871
.github/workflows/main.yml
vendored
871
.github/workflows/main.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
src/qdomyos-zwift.pro.user
|
||||
|
||||
.idea/
|
||||
|
||||
src/Makefile
|
||||
@@ -52,4 +50,3 @@ src/inner_templates/googlemaps/cesium-key.js
|
||||
.vscode/settings.json
|
||||
/tst/Devices/.vs
|
||||
src/inner_templates/googlemaps/cesium-key.js
|
||||
src/qdomyos-zwift.pro.user.49de507
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -5,10 +5,14 @@
|
||||
path = src/smtpclient
|
||||
url = https://github.com/cagnulein/SmtpClient-for-Qt.git
|
||||
branch = cagnulein-patch-2
|
||||
[submodule "src/qmdnsengine"]
|
||||
path = src/qmdnsengine
|
||||
url = https://github.com/cagnulein/qmdnsengine.git
|
||||
branch = zwift
|
||||
[submodule "tst/googletest"]
|
||||
path = tst/googletest
|
||||
url = https://github.com/google/googletest.git
|
||||
tag = release-1.12.1
|
||||
branch = tags/release-1.12.1
|
||||
[submodule "src/qthttpserver"]
|
||||
path = src/qthttpserver
|
||||
url = https://github.com/qt-labs/qthttpserver
|
||||
|
||||
374
CLAUDE.md
374
CLAUDE.md
@@ -1,374 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
QDomyos-Zwift is a Qt-based application that bridges fitness equipment (treadmills, bikes, ellipticals, rowers) with virtual training platforms like Zwift. It acts as a Bluetooth intermediary, connecting physical equipment to fitness apps while providing enhanced features like Peloton integration, power zone training, and workout programs.
|
||||
|
||||
## Build System & Commands
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Build entire project (use subdirs TEMPLATE)
|
||||
qmake
|
||||
make
|
||||
|
||||
# Build specific configurations
|
||||
qmake -r # Recursive build
|
||||
make debug # Debug build
|
||||
make release # Release build
|
||||
|
||||
# Clean build
|
||||
make clean
|
||||
make distclean
|
||||
```
|
||||
|
||||
### Platform-Specific Builds
|
||||
```bash
|
||||
# Android
|
||||
qmake -spec android-clang
|
||||
make
|
||||
|
||||
# iOS
|
||||
qmake -spec macx-ios-clang
|
||||
make
|
||||
|
||||
# Windows (MinGW)
|
||||
qmake -spec win32-g++
|
||||
make
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Build and run tests (requires main app built first)
|
||||
cd tst
|
||||
qmake
|
||||
make
|
||||
./qdomyos-zwift-tests
|
||||
|
||||
# Run with XML output for CI
|
||||
GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests
|
||||
```
|
||||
|
||||
### No-GUI Mode
|
||||
```bash
|
||||
# Run application without GUI
|
||||
sudo ./qdomyos-zwift -no-gui
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Device Architecture
|
||||
The application follows a hierarchical device architecture:
|
||||
|
||||
1. **Base Class**: `bluetoothdevice` - Abstract base for all fitness devices
|
||||
- Manages Bluetooth connectivity via Qt's QLowEnergyController
|
||||
- Defines common metrics (speed, cadence, heart rate, power, distance)
|
||||
- Integrates with virtual devices for app connectivity
|
||||
|
||||
2. **Device Type Classes**: Inherit from `bluetoothdevice`
|
||||
- `bike` - Bike-specific features (resistance, gears, power zones)
|
||||
- `treadmill` - Treadmill features (speed control, inclination, pace)
|
||||
- `elliptical` - Combined bike/treadmill features
|
||||
- `rower` - Rowing metrics (stroke count, 500m pace)
|
||||
- `stairclimber` - Step counting and climbing metrics
|
||||
- `jumprope` - Jump sequence tracking
|
||||
|
||||
3. **Concrete Implementations**: Inherit from device type classes
|
||||
- Located in `src/devices/[devicename]/` folders
|
||||
- Examples: `domyosbike`, `pelotonbike`, `ftmsbike`
|
||||
|
||||
### Virtual Device System
|
||||
- `virtualdevice` - Abstract base for virtual representations
|
||||
- `virtualbike`, `virtualtreadmill`, etc. - Advertise to external apps
|
||||
- Enables bidirectional communication between physical and virtual devices
|
||||
|
||||
### Bluetooth Management
|
||||
- `bluetooth` class acts as device factory and connection manager
|
||||
- `discoveryoptions` configures device discovery process
|
||||
- Supports multiple connection types (Bluetooth LE, TCP, UDP)
|
||||
|
||||
## Key Development Areas
|
||||
|
||||
### Adding New Device Support
|
||||
1. Create device folder in `src/devices/[devicename]/`
|
||||
2. Implement device class inheriting from appropriate base type
|
||||
3. Add device detection logic to `bluetooth.cpp`
|
||||
4. Update `qdomyos-zwift.pri` with new source files
|
||||
5. Add tests in `tst/Devices/` following existing patterns
|
||||
|
||||
### Characteristics & Protocols
|
||||
- Bluetooth characteristics handlers in `src/characteristics/`
|
||||
- FTMS (Fitness Machine Service) protocol support
|
||||
- ANT+ integration for sensors
|
||||
- Custom protocol implementations for specific brands
|
||||
|
||||
### UI & QML
|
||||
- QML-based UI with Qt Quick Controls 2
|
||||
- Main QML files in `src/` (main.qml, settings.qml, etc.)
|
||||
- Platform-specific UI adaptations (iOS, Android, desktop)
|
||||
|
||||
### Integration Features
|
||||
- Peloton workout/resistance integration (`peloton.cpp`)
|
||||
- Zwift workout parsing (`zwiftworkout.cpp`)
|
||||
- GPX file support for route following (`gpx.cpp`)
|
||||
- Training program support (ZWO, XML formats)
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### iOS
|
||||
- Swift bridge files in `src/ios/`
|
||||
- Apple Watch integration via `WatchKitConnection.swift`
|
||||
- HealthKit integration for fitness data
|
||||
- ConnectIQ SDK for Garmin devices
|
||||
|
||||
### Android
|
||||
- Java bridge files in `src/android/src/`
|
||||
- ANT+ integration via Android ANT SDK
|
||||
- Foreground service for background operation
|
||||
- USB serial support for wired connections
|
||||
|
||||
### Windows
|
||||
- ADB integration for Nordic Track iFit devices
|
||||
- PaddleOCR integration for Zwift workout detection
|
||||
- Windows-specific networking features
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
### Device Files
|
||||
```
|
||||
src/devices/[devicename]/
|
||||
├── [devicename].h # Header file
|
||||
├── [devicename].cpp # Implementation
|
||||
└── README.md # Device-specific documentation (optional)
|
||||
```
|
||||
|
||||
### Test Files
|
||||
```
|
||||
tst/Devices/
|
||||
├── DeviceTestData.h # Test data definitions
|
||||
├── Test[DeviceName].h # Device-specific test cases
|
||||
└── TestBluetooth.cpp # Main device detection test suite
|
||||
```
|
||||
|
||||
## Testing Framework
|
||||
|
||||
- Uses Google Test (gtest) with Google Mock
|
||||
- Comprehensive device detection testing
|
||||
- Configuration-based test scenarios
|
||||
- XML output support for CI/CD integration
|
||||
- Tests must be built after main application (links against libqdomyos-zwift.a)
|
||||
|
||||
## Configuration & Settings
|
||||
|
||||
- Settings managed via `qzsettings.cpp` (QSettings wrapper)
|
||||
- Platform-specific configuration paths
|
||||
- Profile system for multiple users/devices
|
||||
- Extensive customization options for device behavior
|
||||
|
||||
## External Dependencies
|
||||
|
||||
- Qt 5.15.2+ (Bluetooth, WebSockets, Charts, Quick, etc.)
|
||||
- Google Test (submodule for testing)
|
||||
- Platform SDKs (Android ANT+, iOS HealthKit, Windows ADB)
|
||||
- Protocol Buffers for Zwift API integration
|
||||
- MQTT client for IoT integration
|
||||
- Various fitness platform APIs (Strava, Garmin Connect, etc.)
|
||||
|
||||
## Adding New ProForm Treadmill Models
|
||||
|
||||
This section provides a complete guide for adding new ProForm treadmill models to the codebase, based on the ProForm 995i implementation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Bluetooth Frame Capture File**: A file containing raw Bluetooth frames from the target treadmill
|
||||
2. **Frame Analysis**: Understanding of which frames are initialization vs. sendPoll frames
|
||||
3. **BLE Header Knowledge**: Each frame has an 11-byte BLE header that must be removed
|
||||
|
||||
### Step-by-Step Implementation Process
|
||||
|
||||
#### 1. Process Bluetooth Frames
|
||||
|
||||
First, process the raw Bluetooth frames by removing the first 11 bytes (BLE header) from each frame:
|
||||
|
||||
```bash
|
||||
# Example: if you have "proform_model.c" with raw frames
|
||||
# Process each frame by removing first 11 bytes
|
||||
# Separate initialization frames from sendPoll frames
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
- Remove exactly 11 bytes from each frame (BLE header)
|
||||
- Identify the boundary between initialization and sendPoll frames
|
||||
- Initialization frames come first, sendPoll frames follow
|
||||
- Document which packet number starts the sendPoll sequence
|
||||
|
||||
#### 2. Add Boolean Flag to Header File
|
||||
|
||||
Add the new model flag to `src/devices/proformtreadmill/proformtreadmill.h`:
|
||||
|
||||
```cpp
|
||||
// Add before #ifdef Q_OS_IOS section
|
||||
bool proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
#### 3. Add Settings Support
|
||||
|
||||
Update the following files for settings integration:
|
||||
|
||||
**In `src/qzsettings.h`:**
|
||||
```cpp
|
||||
static const QString proform_treadmill_newmodel;
|
||||
static constexpr bool default_proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
**In `src/qzsettings.cpp`:**
|
||||
```cpp
|
||||
const QString QZSettings::proform_treadmill_newmodel = QStringLiteral("proform_treadmill_newmodel");
|
||||
```
|
||||
|
||||
* Update the `allSettingsCount` in `qzsettings.cpp`
|
||||
|
||||
#### 4. Update QML Settings UI
|
||||
|
||||
**In `src/settings.qml`:**
|
||||
|
||||
1. Add property at the END of properties list:
|
||||
```qml
|
||||
property bool proform_treadmill_newmodel: false
|
||||
```
|
||||
|
||||
2. Update ComboBox model array:
|
||||
```qml
|
||||
model: ["Disabled", "Proform New Model", ...]
|
||||
```
|
||||
|
||||
3. Add case selection logic (find next available case number):
|
||||
```qml
|
||||
currentIndex: settings.proform_treadmill_newmodel ? XX : 0;
|
||||
```
|
||||
|
||||
4. Add reset logic:
|
||||
```qml
|
||||
settings.proform_treadmill_newmodel = false;
|
||||
```
|
||||
|
||||
5. Add switch case:
|
||||
```qml
|
||||
case XX: settings.proform_treadmill_newmodel = true; break;
|
||||
```
|
||||
|
||||
#### 5. Implement Device Logic
|
||||
|
||||
**In `src/devices/proformtreadmill/proformtreadmill.cpp`:**
|
||||
|
||||
1. **Load Settings** (in constructor):
|
||||
```cpp
|
||||
proform_treadmill_newmodel = settings.value(QZSettings::proform_treadmill_newmodel, QZSettings::default_proform_treadmill_newmodel).toBool();
|
||||
```
|
||||
|
||||
2. **Add Initialization Case** (in `btinit()` method):
|
||||
```cpp
|
||||
} else if (proform_treadmill_newmodel) {
|
||||
// ALL initialization frames go here
|
||||
uint8_t initData1[] = {0x00, 0xfe, 0x02, 0x08, 0x02};
|
||||
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
|
||||
// ... continue with ALL init frames from capture file
|
||||
// Use frames from beginning until sendPoll boundary
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add SendPoll Case** (in `sendPoll()` method):
|
||||
```cpp
|
||||
} else if (proform_treadmill_newmodel) {
|
||||
switch (counterPoll) {
|
||||
case 0:
|
||||
// First sendPoll frame
|
||||
break;
|
||||
case 1:
|
||||
// Second sendPoll frame
|
||||
break;
|
||||
// ... continue with pattern from sendPoll frames
|
||||
default:
|
||||
// Reset counter and cycle
|
||||
counterPoll = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Update Force Functions** - Add flag to conditional checks in `forceIncline()` and `forceSpeed()`:
|
||||
```cpp
|
||||
} else if (proform_treadmill_8_0 || ... || proform_treadmill_newmodel) {
|
||||
write[14] = write[11] + write[12] + 0x12;
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Requirements
|
||||
|
||||
#### Frame Processing Rules
|
||||
- **Exactly 11 bytes** must be removed from each frame (BLE header)
|
||||
- **All initialization frames** must be included in the btinit() case
|
||||
- **All sendPoll frames** must be included in the sendPoll() switch statement
|
||||
- **Frame order** must be preserved exactly as captured
|
||||
|
||||
#### Settings Integration Rules
|
||||
- **Property placement**: Always add new properties at the END of the properties list in settings.qml
|
||||
- **Case numbering**: Find the next available case number in the ComboBox switch statement
|
||||
- **Naming convention**: Use descriptive names following existing patterns
|
||||
|
||||
#### Code Organization Rules
|
||||
- **Initialization**: All init frames go in btinit() method
|
||||
- **Communication**: All sendPoll frames go in sendPoll() method with switch/case structure
|
||||
- **Force functions**: Add new model flag to existing conditional chains
|
||||
|
||||
### Common Pitfalls and Solutions
|
||||
|
||||
#### Incorrect Byte Removal
|
||||
- **Problem**: Removing wrong number of bytes (12 instead of 11)
|
||||
- **Solution**: Always remove exactly 11 bytes (BLE header)
|
||||
|
||||
#### Wrong SendPoll Boundary
|
||||
- **Problem**: Using initialization frames in sendPoll logic
|
||||
- **Solution**: Identify exact packet number where sendPoll starts
|
||||
|
||||
#### Incomplete Initialization
|
||||
- **Problem**: Missing initialization frames
|
||||
- **Solution**: Include ALL frames from start until sendPoll boundary
|
||||
|
||||
#### Settings Placement
|
||||
- **Problem**: Adding property in wrong location in settings.qml
|
||||
- **Solution**: Always add at END of properties list
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
- [ ] All 11 bytes removed from each frame
|
||||
- [ ] Initialization frames correctly identified and included
|
||||
- [ ] SendPoll frames correctly identified and implemented
|
||||
- [ ] Settings properly integrated in all required files
|
||||
- [ ] ComboBox updated with new model option
|
||||
- [ ] Force functions updated with new model flag
|
||||
- [ ] Property added at END of settings.qml properties list
|
||||
|
||||
### Example Reference
|
||||
|
||||
The ProForm 995i implementation serves as the reference example:
|
||||
- 25 initialization frames (pkt4658-pkt4756)
|
||||
- 33 sendPoll frames (pkt4761-pkt4897)
|
||||
- 6-case sendPoll switch statement with cycling logic
|
||||
- Complete settings integration across all required files
|
||||
|
||||
## Development Tips
|
||||
|
||||
- Use Qt Creator for development with proper project file support
|
||||
- The project uses Qt's signal/slot mechanism extensively
|
||||
- Device implementations should follow existing patterns for consistency
|
||||
- Add comprehensive logging using the project's logging framework
|
||||
- Test device detection thoroughly using the existing test infrastructure
|
||||
- Consider platform differences when adding new features
|
||||
|
||||
## Additional Memories
|
||||
|
||||
- When adding a new setting in QML (setting-tiles.qml), you must:
|
||||
* Add the property at the END of the properties list
|
||||
@@ -1,84 +0,0 @@
|
||||
//
|
||||
// QZWidget.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct QZWidgetEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QZWidget: Widget {
|
||||
let kind: String = "QZWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
QZWidget()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// QZWidgetBundle.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct QZWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
QZWidget()
|
||||
QZWidgetControl()
|
||||
QZWidgetLiveActivity()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// QZWidgetControl.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct QZWidgetControl: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "org.cagnulein.qdomyoszwift.QZWidget",
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value,
|
||||
action: StartTimerIntent()
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension QZWidgetControl {
|
||||
struct Provider: ControlValueProvider {
|
||||
var previewValue: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func currentValue() async throws -> Bool {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start / stop the timer based on `value`.
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
//
|
||||
// QZWidgetLiveActivity.swift
|
||||
// QDomyos-Zwift Live Activity Widget
|
||||
//
|
||||
// Displays workout metrics on Dynamic Island and Lock Screen
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// QZWorkoutAttributes is defined in QZWorkoutAttributes.swift (shared file)
|
||||
|
||||
// MARK: - Live Activity Widget
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: QZWorkoutAttributes.self) { context in
|
||||
// Lock screen/banner UI
|
||||
LockScreenLiveActivityView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded UI
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
Label("\(Int(speed)) \(speedUnit)", systemImage: "speedometer")
|
||||
.font(.caption)
|
||||
Label("\(context.state.heartRate) bpm", systemImage: "heart.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Label("\(Int(context.state.power)) W", systemImage: "bolt.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow)
|
||||
Label("\(Int(context.state.cadence)) rpm", systemImage: "arrow.clockwise")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
// Empty or can add more info
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
HStack {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
Label(String(format: "%.2f \(distanceUnit)", distance), systemImage: "map")
|
||||
Spacer()
|
||||
Label("\(Int(context.state.kcal)) kcal", systemImage: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
} compactLeading: {
|
||||
// Compact leading (left side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("\(context.state.heartRate)")
|
||||
.font(.caption2)
|
||||
}
|
||||
} compactTrailing: {
|
||||
// Compact trailing (right side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundColor(.yellow)
|
||||
Text("\(Int(context.state.power))")
|
||||
.font(.caption2)
|
||||
}
|
||||
} minimal: {
|
||||
// Minimal view (when multiple activities)
|
||||
Image(systemName: "figure.run")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lock Screen View
|
||||
@available(iOS 16.1, *)
|
||||
struct LockScreenLiveActivityView: View {
|
||||
let context: ActivityViewContext<QZWorkoutAttributes>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "figure.indoor.cycle")
|
||||
.foregroundColor(.blue)
|
||||
Text(context.attributes.deviceName)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
MetricView(icon: "speedometer", value: String(format: "%.1f", speed), unit: speedUnit)
|
||||
MetricView(icon: "heart.fill", value: "\(context.state.heartRate)", unit: "bpm", color: .red)
|
||||
MetricView(icon: "bolt.fill", value: "\(Int(context.state.power))", unit: "W", color: .yellow)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
MetricView(icon: "arrow.clockwise", value: "\(Int(context.state.cadence))", unit: "rpm")
|
||||
MetricView(icon: "map", value: String(format: "%.2f", distance), unit: distanceUnit)
|
||||
MetricView(icon: "flame.fill", value: "\(Int(context.state.kcal))", unit: "kcal", color: .orange)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metric View Component
|
||||
struct MetricView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let unit: String
|
||||
var color: Color = .primary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
.font(.caption)
|
||||
Text(value)
|
||||
.font(.system(.body, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
Text(unit)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = QZWorkoutAttributes(deviceName: "QZ Bike", useMiles: false)
|
||||
static let contentState = QZWorkoutAttributes.ContentState(
|
||||
speed: 25.5,
|
||||
cadence: 85,
|
||||
power: 200,
|
||||
heartRate: 145,
|
||||
distance: 12500, // meters (will be displayed as 12.50 km or 7.77 mi)
|
||||
kcal: 320,
|
||||
useMiles: false
|
||||
)
|
||||
|
||||
static var previews: some View {
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Island Compact")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Island Expanded")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Minimal")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .content)
|
||||
.previewDisplayName("Lock Screen")
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// QZWorkoutAttributes.swift
|
||||
// QDomyos-Zwift
|
||||
//
|
||||
// Shared attributes for Live Activities
|
||||
// MUST be included in both qdomyoszwift and QZWidget targets
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
public struct QZWorkoutAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
public var speed: Double
|
||||
public var cadence: Double
|
||||
public var power: Double
|
||||
public var heartRate: Int
|
||||
public var distance: Double
|
||||
public var kcal: Double
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(speed: Double, cadence: Double, power: Double, heartRate: Int, distance: Double, kcal: Double, useMiles: Bool) {
|
||||
self.speed = speed
|
||||
self.cadence = cadence
|
||||
self.power = power
|
||||
self.heartRate = heartRate
|
||||
self.distance = distance
|
||||
self.kcal = kcal
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
|
||||
public var deviceName: String
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(deviceName: String, useMiles: Bool) {
|
||||
self.deviceName = deviceName
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,13 +23,11 @@ class WatchKitConnection: NSObject {
|
||||
static let shared = WatchKitConnection()
|
||||
public static var distance = 0.0
|
||||
public static var kcal = 0.0
|
||||
public static var totalKcal = 0.0
|
||||
public static var stepCadence = 0
|
||||
public static var speed = 0.0
|
||||
public static var cadence = 0.0
|
||||
public static var power = 0.0
|
||||
public static var steps = 0
|
||||
public static var elevationGain = 0.0
|
||||
weak var delegate: WatchKitConnectionDelegate?
|
||||
|
||||
private override init() {
|
||||
@@ -72,9 +70,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
WatchKitConnection.distance = dDistance
|
||||
let dKcal = Double(result["kcal"] as! Double)
|
||||
WatchKitConnection.kcal = dKcal
|
||||
if let totalKcalDouble = result["totalKcal"] as? Double {
|
||||
WatchKitConnection.totalKcal = totalKcalDouble
|
||||
}
|
||||
|
||||
let dSpeed = Double(result["speed"] as! Double)
|
||||
WatchKitConnection.speed = dSpeed
|
||||
@@ -86,13 +81,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
|
||||
let iSteps = Int(stepsDouble)
|
||||
WatchKitConnection.steps = iSteps
|
||||
}
|
||||
if let elevationGainDouble = result["elevationGain"] as? Double {
|
||||
WatchKitConnection.elevationGain = elevationGainDouble
|
||||
// Calculate flights climbed and update WorkoutTracking
|
||||
let flightsClimbed = elevationGainDouble / 3.048 // One flight = 10 feet = 3.048 meters
|
||||
WorkoutTracking.flightsClimbed = flightsClimbed
|
||||
print("WatchKitConnection: Received elevation gain: \(elevationGainDouble)m, flights: \(flightsClimbed)")
|
||||
}
|
||||
}, errorHandler: { (error) in
|
||||
print(error)
|
||||
})
|
||||
|
||||
@@ -28,7 +28,6 @@ class WorkoutTracking: NSObject {
|
||||
static let shared = WorkoutTracking()
|
||||
public static var distance = Double()
|
||||
public static var kcal = Double()
|
||||
public static var totalKcal = Double()
|
||||
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
|
||||
public static var cadenceLastSteps = Double()
|
||||
public static var cadenceSteps = 0
|
||||
@@ -37,18 +36,17 @@ class WorkoutTracking: NSObject {
|
||||
public static var steps = Int()
|
||||
public static var cadence = Double()
|
||||
public static var lastDateMetric = Date()
|
||||
public static var flightsClimbed = Double()
|
||||
var sport: Int = 0
|
||||
let healthStore = HKHealthStore()
|
||||
let configuration = HKWorkoutConfiguration()
|
||||
var workoutSession: HKWorkoutSession!
|
||||
var workoutBuilder: HKLiveWorkoutBuilder!
|
||||
|
||||
|
||||
weak var delegate: WorkoutTrackingDelegate?
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutTracking {
|
||||
@@ -56,26 +54,20 @@ extension WorkoutTracking {
|
||||
switch statistics.quantityType {
|
||||
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
|
||||
let distanceUnit = HKUnit.mile()
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
|
||||
let energyUnit = HKUnit.kilocalorie()
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .heartRate):
|
||||
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
|
||||
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) else {
|
||||
return
|
||||
}
|
||||
let roundedValue = Double( round( 1 * value ) / 1 )
|
||||
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
|
||||
let roundedValue = Double( round( 1 * value! ) / 1 )
|
||||
delegate?.didReceiveHealthKitHeartRate(roundedValue)
|
||||
|
||||
case HKQuantityType.quantityType(forIdentifier: .stepCount):
|
||||
@@ -168,7 +160,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
|
||||
@@ -178,7 +169,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .runningVerticalOscillation)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingStepLength)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
} else {
|
||||
@@ -189,8 +179,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
|
||||
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
|
||||
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
}
|
||||
@@ -209,8 +197,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
func startWorkOut() {
|
||||
WorkoutTracking.lastDateMetric = Date()
|
||||
// Reset flights climbed for new workout
|
||||
WorkoutTracking.flightsClimbed = 0
|
||||
print("Start workout")
|
||||
configWorkout()
|
||||
workoutSession.startActivity(with: Date())
|
||||
@@ -231,30 +217,23 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
workoutSession.stopActivity(with: Date())
|
||||
workoutSession.end()
|
||||
|
||||
// Write active calories
|
||||
guard let activeQuantityType = HKQuantityType.quantityType(
|
||||
guard let quantityType = HKQuantityType.quantityType(
|
||||
forIdentifier: .activeEnergyBurned) else {
|
||||
return
|
||||
}
|
||||
|
||||
let unit = HKUnit.kilocalorie()
|
||||
let activeEnergyBurned = WorkoutTracking.kcal
|
||||
let activeQuantity = HKQuantity(unit: unit,
|
||||
doubleValue: activeEnergyBurned)
|
||||
let totalEnergyBurned = WorkoutTracking.kcal
|
||||
let quantity = HKQuantity(unit: unit,
|
||||
doubleValue: totalEnergyBurned)
|
||||
|
||||
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
|
||||
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
|
||||
quantity: quantity,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
|
||||
quantity: activeQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([activeSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
workoutBuilder.add([sample]) {(success, error) in}
|
||||
|
||||
let unitDistance = HKUnit.mile()
|
||||
let miles = WorkoutTracking.distance
|
||||
let quantityMiles = HKQuantity(unit: unitDistance,
|
||||
@@ -270,7 +249,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
@@ -286,80 +265,11 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(sport == 4) { // Rowing
|
||||
// Guard to check if steps quantity type is available
|
||||
guard let quantityTypeSteps = HKQuantityType.quantityType(
|
||||
forIdentifier: .stepCount) else {
|
||||
return
|
||||
}
|
||||
|
||||
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
|
||||
|
||||
// Create a sample for total steps
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
quantity: stepsQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Add the steps sample to workout builder
|
||||
workoutBuilder.add([sampleSteps]) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Per il rowing, HealthKit utilizza un tipo specifico di distanza
|
||||
// Se non esiste un tipo specifico per il rowing, possiamo usare un tipo generico di distanza
|
||||
var quantityTypeDistance: HKQuantityType?
|
||||
|
||||
// In watchOS 10 e versioni successive, possiamo usare un tipo specifico se disponibile
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
// Verifica se esiste un tipo specifico per il rowing, altrimenti utilizza un tipo generico
|
||||
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)
|
||||
} else {
|
||||
// Nelle versioni precedenti, usa il tipo generico
|
||||
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)
|
||||
}
|
||||
|
||||
guard let typeDistance = quantityTypeDistance else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
// Guard to check if steps quantity type is available
|
||||
guard let quantityTypeSteps = HKQuantityType.quantityType(
|
||||
forIdentifier: .stepCount) else {
|
||||
@@ -367,67 +277,59 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
|
||||
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
|
||||
|
||||
|
||||
// Create a sample for total steps
|
||||
let sampleSteps = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeSteps,
|
||||
quantity: stepsQuantity, // Use your steps quantity here
|
||||
start: startDate,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
// Guard to check if distance quantity type is available
|
||||
guard let quantityTypeDistance = HKQuantityType.quantityType(
|
||||
forIdentifier: .distanceWalkingRunning) else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
|
||||
// Create flights climbed sample if available
|
||||
var samplesToAdd: [HKCumulativeQuantitySeriesSample] = [sampleSteps, sampleDistance]
|
||||
|
||||
if WorkoutTracking.flightsClimbed > 0 {
|
||||
if let quantityTypeFlights = HKQuantityType.quantityType(forIdentifier: .flightsClimbed) {
|
||||
let flightsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: WorkoutTracking.flightsClimbed)
|
||||
let sampleFlights = HKCumulativeQuantitySeriesSample(
|
||||
type: quantityTypeFlights,
|
||||
quantity: flightsQuantity,
|
||||
start: startDate,
|
||||
end: Date())
|
||||
samplesToAdd.append(sampleFlights)
|
||||
print("WatchWorkoutTracking: Adding flights climbed to workout: \(WorkoutTracking.flightsClimbed)")
|
||||
}
|
||||
}
|
||||
|
||||
// Add all samples to the workout builder
|
||||
workoutBuilder.add(samplesToAdd) { (success, error) in
|
||||
// Add the steps sample to workout builder
|
||||
workoutBuilder.add([sampleSteps]) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
|
||||
|
||||
// End the data collection
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
|
||||
// Finish the workout and save metrics
|
||||
|
||||
// Finish the workout and save total steps
|
||||
self.workoutBuilder.finishWorkout { (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(stepsQuantity, forKey: "totalSteps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let quantityTypeDistance = HKQuantityType.quantityType(
|
||||
forIdentifier: .distanceWalkingRunning) else {
|
||||
return
|
||||
}
|
||||
|
||||
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
|
||||
quantity: quantityMiles,
|
||||
start: workoutSession.startDate!,
|
||||
end: Date())
|
||||
|
||||
workoutBuilder.add([sampleDistance]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
self.workoutBuilder.finishWorkout{ (workout, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
workout?.setValue(quantityMiles, forKey: "totalDistance")
|
||||
// Set total energy burned on the workout
|
||||
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
|
||||
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
|
||||
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
|
||||
|
||||
// Reset flights climbed for next workout
|
||||
WorkoutTracking.flightsClimbed = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,7 +344,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
}
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
||||
|
||||
|
||||
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
|
||||
guard let weakSelf = self else {
|
||||
return
|
||||
@@ -452,7 +354,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
|
||||
print("Failed to fetch steps rate")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let sum = result.sumQuantity() {
|
||||
resultCount = sum.doubleValue(for: HKUnit.count())
|
||||
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
|
||||
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
|
||||
QTPLUGIN += qavfmediaplayer
|
||||
QT+= charts
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ These instructions build the app itself, not the test project.
|
||||
## On a Linux System (from source)
|
||||
|
||||
```buildoutcfg
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on Raspberry Pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
|
||||
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make
|
||||
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
$ cd qdomyos-zwift
|
||||
$ git submodule update --init src/smtpclient/
|
||||
@@ -34,15 +34,16 @@ Download and install https://download.qt.io/archive/qt/5.12/5.12.12/qt-opensourc
|
||||
|
||||

|
||||
|
||||
This guide will walk you through steps to setup an autonomous, headless Raspberry Pi bridge.
|
||||
This guide will walk you through steps to setup an autonomous, headless raspberry bridge.
|
||||
|
||||
|
||||
### Initial System Preparation
|
||||
|
||||
You can install a lightweight version of embedded OS to speed up your Raspberry booting time.
|
||||
You can install a lightweight version of embedded OS to speed up your raspberry booting time.
|
||||
|
||||
#### Prepare your SD Card
|
||||
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, [`Raspberry Pi OS Lite 64bit`](https://www.raspberrypi.com/software/operating-systems/). Boot up the Raspberry Pi (default credentials are pi/raspberry)
|
||||
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, the Raspberry lite OS version.
|
||||
Boot on the raspberry (default credentials are pi/raspberry)
|
||||
|
||||
#### Change default credentials
|
||||
|
||||
@@ -55,7 +56,7 @@ Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and
|
||||
`System Options` > `Wireless LAN`
|
||||
Enter an SSID and your wifi password.
|
||||
|
||||
Your Raspberry will fetch a DHCP address at boot time, which can be painful :
|
||||
Your raspberry will fetch a DHCP address at boot time, which can be painful :
|
||||
- The IP address might change at every boot
|
||||
- This process takes approximately 10 seconds at boot time.
|
||||
|
||||
@@ -76,7 +77,7 @@ Apply the changes `sudo systemctl restart dhcpcd.service` and ensure you have in
|
||||
|
||||
#### Enable SSH access
|
||||
|
||||
You might want to access your Raspberry remotely while it is attached to your fitness equipment.
|
||||
You might want to access your raspberry remotely while it is attached to your fitness equipment.
|
||||
|
||||
`sudo raspi-config` > `Interface Options` > `SSH`
|
||||
|
||||
@@ -85,17 +86,15 @@ You might want to access your Raspberry remotely while it is attached to your fi
|
||||
This option allows a faster boot. `sudo raspi-config` > `System Options` > `Network at boot` > `No`
|
||||
|
||||
#### Reboot and test connectivity
|
||||
Reboot your Raspberry `sudo reboot now`
|
||||
Reboot your raspberry `sudo reboot now`
|
||||
|
||||
Congratulations !
|
||||
Your Raspberry should be reachable from your local network via SSH.
|
||||
Your raspberry should be reachable from your local network via SSH.
|
||||
|
||||
|
||||
### QDOMYOS-ZWIFT installation
|
||||
|
||||
Qdomyos-zwift can be compiled from source (hard), or using a binary (easy). **Only one is required**.
|
||||
|
||||
#### Update your Raspberry (mandatory !)
|
||||
#### Update your raspberry (mandatory !)
|
||||
|
||||
Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
|
||||
|
||||
@@ -104,10 +103,10 @@ Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
|
||||
|
||||
This operation takes a moment to complete.
|
||||
|
||||
#### Option 1. Install qdomyos-zwift from sources
|
||||
#### Install qdomyos-zwift from sources
|
||||
|
||||
```bash
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
|
||||
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make
|
||||
git clone https://github.com/cagnulein/qdomyos-zwift.git
|
||||
cd qdomyos-zwift
|
||||
git submodule update --init src/smtpclient/
|
||||
@@ -127,117 +126,20 @@ Please note :
|
||||
- Don't build the application with `-j4` option (this will fail)
|
||||
- Build operation is circa 45 minutes (subsequent builds are faster)
|
||||
|
||||
#### Option 2. Install qdomyos-zwift from binary
|
||||
|
||||
Ensure you're logged in to GitHub and download `https://github.com/cagnulein/qdomyos-zwift/actions/runs/19521021942/artifacts/4622513957`. Extract the zip file and copy the QZ binary to the Raspberry Pi Zero 2 W. If you get a 404 Not Found you might have to login to GitHub first.
|
||||
|
||||
Make it executable:
|
||||
```
|
||||
chmod +x qdomyos-zwift-64bit
|
||||
```
|
||||
|
||||
Install required libraries and dependencies for headless mode:
|
||||
```
|
||||
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64
|
||||
```
|
||||
|
||||
If you are running Raspberry Pi Desktop OS, and you want to run the QZ UI, additonally add the qml libraries.
|
||||
```
|
||||
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64 *qml*
|
||||
```
|
||||
|
||||
|
||||
#### Unblock Bluetooth (if using Bluetooth)
|
||||
|
||||
Unblock Bluetooth:
|
||||
```
|
||||
sudo rfkill unblock bluetooth
|
||||
```
|
||||
|
||||
Troubleshooting Bluetooth not working:
|
||||
Errors:
|
||||
```
|
||||
Fri Nov 21 18:05:07 2025 1763708707500 Debug: Bluez 5 detected.
|
||||
qt.bluetooth.bluez: Aborting device discovery due to offline Bluetooth Adapter
|
||||
Fri Nov 21 18:05:07 2025 1763708707540 Debug: Aborting device discovery due to offline Bluetooth Adapter
|
||||
^C"SIGINT"
|
||||
Fri Nov 21 18:05:21 2025 1763708721033 Debug: devices/bluetooth.cpp virtual bool bluetooth::handleSignal(int) "SIGINT"
|
||||
```
|
||||
|
||||
Check if Bluetooth is blocked/down:
|
||||
```
|
||||
$ rfkill list
|
||||
0: hci0: Bluetooth
|
||||
Soft blocked: yes
|
||||
Hard blocked: no
|
||||
1: phy0: Wireless LAN
|
||||
Soft blocked: no
|
||||
Hard blocked: no
|
||||
```
|
||||
```
|
||||
$ hciconfig -a
|
||||
hci0: Type: Primary Bus: UART
|
||||
BD Address: B8:27:EB:A2:85:70 ACL MTU: 1021:8 SCO MTU: 64:1
|
||||
DOWN
|
||||
RX bytes:3629 acl:0 sco:0 events:280 errors:0
|
||||
TX bytes:48392 acl:0 sco:0 commands:280 errors:0
|
||||
Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87
|
||||
Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
|
||||
Link policy: RSWITCH SNIFF
|
||||
Link mode: PERIPHERAL ACCEPT
|
||||
```
|
||||
|
||||
Unblock Bluetooth:
|
||||
|
||||
```
|
||||
sudo rfkill unblock bluetooth
|
||||
```
|
||||
|
||||
|
||||
#### Test your installation
|
||||
It is now time to check everything's fine
|
||||
|
||||
`sudo ./qdomyos-zwift-64bit -no-gui -heart-service`
|
||||
`./qdomyos-zwift -no-gui -heart-service`
|
||||
|
||||

|
||||
|
||||
Test your access from your fitness device.
|
||||
|
||||
Check logs to see if it's running:
|
||||
```
|
||||
journalctl -u qz.service -f
|
||||
```
|
||||
|
||||
#### Update QZ config file
|
||||
|
||||
Running headless you need to update `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` with specific settings for your set up. If you already have it working on an iPhone/Android, follow this guide to deploy QZ with the UI, replicate the settings in the UI, check everything works, then take a copy of `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` to use with the headless deployment.
|
||||
|
||||
For my set up, I add:
|
||||
|
||||
Nordictrack C1650:
|
||||
```
|
||||
norditrack_s25_treadmill=true
|
||||
proformtreadmillip=172.31.2.36
|
||||
```
|
||||
Zwift specific options (auto inclination not there yet in the Raspberry Pi version):
|
||||
```
|
||||
zwift_api_autoinclination=true
|
||||
zwift_inclination_gain=1
|
||||
zwift_inclination_offset=0
|
||||
zwift_username=user@myemail.com
|
||||
zwift_password=Password1
|
||||
```
|
||||
|
||||
Check it works:
|
||||
```
|
||||
sudo ./qdomyos-zwift-64bit -no-gui -no-console -no-log
|
||||
```
|
||||
|
||||
#### Automate QDOMYOS-ZWIFT at startup
|
||||
|
||||
You might want to have QDOMYOS-ZWIFT to start automatically at boot time.
|
||||
|
||||
Let's create a systemd service that we'll enable at boot sequence. **Update ExecStart with the path and full name with commandline options for your qz binary. Update ExecStop with the full name of the binary.**
|
||||
Let's create a systemd service that we'll enable at boot sequence.
|
||||
|
||||
`sudo vi /lib/systemd/system/qz.service`
|
||||
|
||||
@@ -423,7 +325,7 @@ sudo tail -f /var/log/qz-treadmill-monitor.log
|
||||
|
||||
### (optional) Enable overlay FS
|
||||
|
||||
Once that everything is working as expected, and if you dedicate your Raspberry Pi to this usage, you might want to enable the read-only overlay FS.
|
||||
Once that everything is working as expected, and if you dedicate your Raspberry pi to this usage, you might want to enable the read-only overlay FS.
|
||||
|
||||
By enabling the overlay read-only system, your SD card will be read-only only and every file written will be to RAM.
|
||||
Then at each reboot the RAM is erased and you'll revert to the initial status of the overlay file-system.
|
||||
@@ -448,19 +350,7 @@ Reboot immediately.
|
||||
|
||||
## Other tricks
|
||||
|
||||
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my Raspberry to my bike.
|
||||
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the Raspberry.
|
||||
|
||||
You can easily remove the Raspberry Pi from the bike if required.
|
||||
|
||||
|
||||
## Trouobleshooting QZ on RPI
|
||||
|
||||
Run qz as root
|
||||
|
||||
For Zwift, check Zwift detects QZ. Check bluetooth
|
||||
|
||||
If Zwift isn't detecting speed from your exercise device, double check your .conf is correct. If you're not sure, Check the setup works using iPhone/Android phone, then replicate the settings by using Raspberry Pi Desktop OS and qz -qml to view the QZ UI. Change settings to match working iPhone/Android.
|
||||
|
||||
|
||||
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my raspberry to my bike.
|
||||
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the raspberry.
|
||||
|
||||
You can easily remove the raspberry pi from the bike if required.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Workout Editor
|
||||
|
||||
The Workout Editor lets you create multi-device training sessions without leaving QZ.
|
||||
|
||||
## Open the Editor
|
||||
- Drawer → Workout Editor
|
||||
- Select the target device profile (treadmill, bike, elliptical, rower).
|
||||
|
||||
## Build Intervals
|
||||
- Every interval exposes the parameters supported by the selected device.
|
||||
- Use **Add Interval**, **Copy**, **Up/Down**, or **Del** to manage the timeline.
|
||||
- Select a block of consecutive intervals and hit **Repeat Selection** to clone it quickly (perfect for repeat sets like work/rest pairs).
|
||||
- Toggle **Show advanced parameters** to edit cadence targets, Peloton levels, heart-rate limits, GPS metadata, etc.
|
||||
- The Chart.js preview updates automatically while you edit.
|
||||
|
||||
## Load or Save Programs
|
||||
- **Load** imports any `.xml` plan from `training/`.
|
||||
- **Save** writes the XML back into the same folder (name is sanitised automatically).
|
||||
- **Save & Start** persists the file and immediately queues it for playback.
|
||||
- Existing files trigger an overwrite confirmation.
|
||||
|
||||
## Tips
|
||||
- Durations must follow `hh:mm:ss` format.
|
||||
- Speed/incline units follow the global miles setting.
|
||||
- Saved workouts appear inside the regular “Open Train Program” list.
|
||||
@@ -1,188 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple
|
||||
import re
|
||||
|
||||
@dataclass
|
||||
class HubRidingData:
|
||||
power: Optional[int] = None
|
||||
cadence: Optional[int] = None
|
||||
speed_x100: Optional[int] = None
|
||||
hr: Optional[int] = None
|
||||
unknown1: Optional[int] = None
|
||||
unknown2: Optional[int] = None
|
||||
|
||||
def __str__(self):
|
||||
return (f"Power={self.power}W Cadence={self.cadence}rpm "
|
||||
f"Speed={self.speed_x100/100 if self.speed_x100 else 0:.1f}km/h "
|
||||
f"HR={self.hr}bpm Unknown1={self.unknown1} Unknown2={self.unknown2}")
|
||||
|
||||
def parse_protobuf_varint(data: bytes, offset: int = 0) -> Tuple[int, int]:
|
||||
result = 0
|
||||
shift = 0
|
||||
while offset < len(data):
|
||||
byte = data[offset]
|
||||
result |= (byte & 0x7F) << shift
|
||||
offset += 1
|
||||
if not (byte & 0x80):
|
||||
break
|
||||
shift += 7
|
||||
return result, offset
|
||||
|
||||
def parse_hub_riding_data(data: bytes) -> Optional[HubRidingData]:
|
||||
try:
|
||||
riding_data = HubRidingData()
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
key, new_offset = parse_protobuf_varint(data, offset)
|
||||
wire_type = key & 0x07
|
||||
field_number = key >> 3
|
||||
offset = new_offset
|
||||
|
||||
if wire_type == 0:
|
||||
value, offset = parse_protobuf_varint(data, offset)
|
||||
if field_number == 1:
|
||||
riding_data.power = value
|
||||
elif field_number == 2:
|
||||
riding_data.cadence = value
|
||||
elif field_number == 3:
|
||||
riding_data.speed_x100 = value
|
||||
elif field_number == 4:
|
||||
riding_data.hr = value
|
||||
elif field_number == 5:
|
||||
riding_data.unknown1 = value
|
||||
elif field_number == 6:
|
||||
riding_data.unknown2 = value
|
||||
return riding_data
|
||||
except Exception as e:
|
||||
print(f"Error parsing protobuf: {e}")
|
||||
return None
|
||||
|
||||
@dataclass
|
||||
class DirconPacket:
|
||||
message_version: int = 1
|
||||
identifier: int = 0xFF
|
||||
sequence_number: int = 0
|
||||
response_code: int = 0
|
||||
length: int = 0
|
||||
uuid: int = 0
|
||||
uuids: List[int] = None
|
||||
additional_data: bytes = b''
|
||||
is_request: bool = False
|
||||
riding_data: Optional[HubRidingData] = None
|
||||
|
||||
def __str__(self):
|
||||
uuids_str = ','.join(f'{u:04x}' for u in (self.uuids or []))
|
||||
base_str = (f"vers={self.message_version} Id={self.identifier} sn={self.sequence_number} "
|
||||
f"resp={self.response_code} len={self.length} req?={self.is_request} "
|
||||
f"uuid={self.uuid:04x} dat={self.additional_data.hex()} uuids=[{uuids_str}]")
|
||||
if self.riding_data:
|
||||
base_str += f"\nRiding Data: {self.riding_data}"
|
||||
return base_str
|
||||
|
||||
def parse_dircon_packet(data: bytes, offset: int = 0) -> Tuple[Optional[DirconPacket], int]:
|
||||
if len(data) - offset < 6:
|
||||
return None, 0
|
||||
|
||||
packet = DirconPacket()
|
||||
packet.message_version = data[offset]
|
||||
packet.identifier = data[offset + 1]
|
||||
packet.sequence_number = data[offset + 2]
|
||||
packet.response_code = data[offset + 3]
|
||||
packet.length = (data[offset + 4] << 8) | data[offset + 5]
|
||||
|
||||
total_length = 6 + packet.length
|
||||
if len(data) - offset < total_length:
|
||||
return None, 0
|
||||
|
||||
curr_offset = offset + 6
|
||||
|
||||
if packet.identifier == 0x01: # DPKT_MSGID_DISCOVER_SERVICES
|
||||
if packet.length == 0:
|
||||
packet.is_request = True
|
||||
elif packet.length % 16 == 0:
|
||||
packet.uuids = []
|
||||
while curr_offset + 16 <= offset + total_length:
|
||||
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
packet.uuids.append(uuid)
|
||||
curr_offset += 16
|
||||
|
||||
elif packet.identifier == 0x02: # DPKT_MSGID_DISCOVER_CHARACTERISTICS
|
||||
if packet.length >= 16:
|
||||
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
if packet.length == 16:
|
||||
packet.is_request = True
|
||||
elif (packet.length - 16) % 17 == 0:
|
||||
curr_offset += 16
|
||||
packet.uuids = []
|
||||
packet.additional_data = b''
|
||||
while curr_offset + 17 <= offset + total_length:
|
||||
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
packet.uuids.append(uuid)
|
||||
packet.additional_data += bytes([data[curr_offset + 16]])
|
||||
curr_offset += 17
|
||||
|
||||
elif packet.identifier in [0x03, 0x04, 0x05, 0x06]: # READ/WRITE/NOTIFY characteristics
|
||||
if packet.length >= 16:
|
||||
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
|
||||
if packet.length > 16:
|
||||
packet.additional_data = data[curr_offset + 16:offset + total_length]
|
||||
if packet.uuid == 0x0002:
|
||||
packet.riding_data = parse_hub_riding_data(packet.additional_data)
|
||||
if packet.identifier != 0x06:
|
||||
packet.is_request = True
|
||||
|
||||
return packet, total_length
|
||||
|
||||
def extract_bytes_from_c_array(content: str) -> List[Tuple[str, bytes]]:
|
||||
packets = []
|
||||
pattern = r'static const unsigned char (\w+)\[\d+\] = \{([^}]+)\};'
|
||||
matches = re.finditer(pattern, content)
|
||||
|
||||
for match in matches:
|
||||
name = match.group(1)
|
||||
hex_str = match.group(2)
|
||||
|
||||
hex_values = []
|
||||
for line in hex_str.split('\n'):
|
||||
line = line.split('//')[0]
|
||||
values = re.findall(r'0x[0-9a-fA-F]{2}', line)
|
||||
hex_values.extend(values)
|
||||
|
||||
byte_data = bytes([int(x, 16) for x in hex_values])
|
||||
packets.append((name, byte_data))
|
||||
|
||||
return packets
|
||||
|
||||
def get_tcp_payload(data: bytes) -> bytes:
|
||||
ip_header_start = 14 # Skip Ethernet header
|
||||
ip_header_len = (data[ip_header_start] & 0x0F) * 4
|
||||
tcp_header_start = ip_header_start + ip_header_len
|
||||
tcp_header_len = ((data[tcp_header_start + 12] >> 4) & 0x0F) * 4
|
||||
payload_start = tcp_header_start + tcp_header_len
|
||||
return data[payload_start:]
|
||||
|
||||
def parse_file(filename: str):
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read()
|
||||
packets = extract_bytes_from_c_array(content)
|
||||
|
||||
for name, data in packets:
|
||||
print(f"\nPacket {name}:")
|
||||
payload = get_tcp_payload(data)
|
||||
print(f"Dircon payload: {payload.hex()}")
|
||||
|
||||
offset = 0
|
||||
while offset < len(payload):
|
||||
packet, consumed = parse_dircon_packet(payload, offset)
|
||||
if packet is None or consumed == 0:
|
||||
break
|
||||
print(f"Frame: {packet}")
|
||||
offset += consumed
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python script.py <input_file>")
|
||||
sys.exit(1)
|
||||
|
||||
parse_file(sys.argv[1])
|
||||
@@ -1,331 +0,0 @@
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
import random
|
||||
import struct
|
||||
import binascii
|
||||
import time
|
||||
from typing import Any, Union
|
||||
|
||||
# Verificare che siamo su macOS
|
||||
if sys.platform != 'darwin':
|
||||
print("Questo script è progettato specificamente per macOS!")
|
||||
sys.exit(1)
|
||||
|
||||
# Importare bless
|
||||
try:
|
||||
from bless import (
|
||||
BlessServer,
|
||||
BlessGATTCharacteristic,
|
||||
GATTCharacteristicProperties,
|
||||
GATTAttributePermissions,
|
||||
)
|
||||
except ImportError:
|
||||
print("Errore: impossibile importare bless. Installarlo con: pip install bless")
|
||||
sys.exit(1)
|
||||
|
||||
# Configurazione logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Trigger per eventi
|
||||
trigger = threading.Event()
|
||||
|
||||
# Informazioni sul dispositivo
|
||||
DEVICE_NAME = "Wahoo KICKR 51A6"
|
||||
|
||||
# UUID dei servizi standard
|
||||
CYCLING_POWER_SERVICE = "00001818-0000-1000-8000-00805f9b34fb"
|
||||
USER_DATA_SERVICE = "0000181c-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_SERVICE = "00001826-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
# UUID dei servizi Wahoo personalizzati
|
||||
WAHOO_SERVICE_1 = "a026ee01-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_3 = "a026ee03-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_6 = "a026ee06-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_SERVICE_B = "a026ee0b-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
|
||||
# UUID delle caratteristiche standard
|
||||
CYCLING_POWER_MEASUREMENT = "00002a63-0000-1000-8000-00805f9b34fb"
|
||||
CYCLING_POWER_FEATURE = "00002a65-0000-1000-8000-00805f9b34fb"
|
||||
SENSOR_LOCATION = "00002a5d-0000-1000-8000-00805f9b34fb"
|
||||
CYCLING_POWER_CONTROL_POINT = "00002a66-0000-1000-8000-00805f9b34fb"
|
||||
WEIGHT = "00002a98-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_FEATURE = "00002acc-0000-1000-8000-00805f9b34fb"
|
||||
TRAINING_STATUS = "00002ad3-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_CONTROL_POINT = "00002ad9-0000-1000-8000-00805f9b34fb"
|
||||
FITNESS_MACHINE_STATUS = "00002ada-0000-1000-8000-00805f9b34fb"
|
||||
INDOOR_BIKE_DATA = "00002ad2-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
# UUID delle caratteristiche Wahoo personalizzate
|
||||
WAHOO_CUSTOM_CP_CHAR = "a026e005-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_1 = "a026e002-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_2 = "a026e004-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_3 = "a026e00a-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_4 = "a026e023-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
WAHOO_CHAR_5 = "a026e037-0a7d-4ab3-97fa-f1500f9feb8b"
|
||||
|
||||
# Stato dispositivo - variabili globali
|
||||
current_power = 120
|
||||
current_cadence = 85
|
||||
current_speed = 25.0
|
||||
current_resistance = 5
|
||||
|
||||
# Funzioni di callback
|
||||
def read_request(characteristic, **kwargs):
|
||||
logger.debug(f"Lettura: {characteristic.value}")
|
||||
return characteristic.value
|
||||
|
||||
def write_request(characteristic, value, **kwargs):
|
||||
uuid_str = str(characteristic.uuid).lower()
|
||||
logger.info(f"Scrittura su caratteristica: {uuid_str}, valore: {binascii.hexlify(value)}")
|
||||
|
||||
# Gestione delle richieste di scrittura
|
||||
if uuid_str == FITNESS_MACHINE_CONTROL_POINT.lower():
|
||||
handle_ftms_control_point(value)
|
||||
elif uuid_str == CYCLING_POWER_CONTROL_POINT.lower():
|
||||
handle_cp_control_point(value)
|
||||
elif uuid_str in [WAHOO_CHAR_1.lower(), WAHOO_CHAR_3.lower(), WAHOO_CHAR_4.lower(), WAHOO_CHAR_5.lower()]:
|
||||
handle_wahoo_char_write(uuid_str, value)
|
||||
|
||||
characteristic.value = value
|
||||
|
||||
# Gestori di richieste di scrittura
|
||||
def handle_ftms_control_point(data):
|
||||
global current_power, current_resistance
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
op_code = data[0]
|
||||
logger.info(f"Comando FTMS Control Point: {op_code:#x}")
|
||||
|
||||
if op_code == 0x05: # Set Target Power (ERG mode)
|
||||
if len(data) >= 3:
|
||||
target_power = int.from_bytes(data[1:3], byteorder='little')
|
||||
logger.info(f"Target power impostato: {target_power}W")
|
||||
current_power = target_power
|
||||
|
||||
def handle_cp_control_point(data):
|
||||
if not data:
|
||||
return
|
||||
|
||||
op_code = data[0]
|
||||
logger.info(f"Comando CP Control Point: {op_code:#x}")
|
||||
|
||||
def handle_wahoo_char_write(uuid_str, data):
|
||||
logger.info(f"Scrittura su caratteristica Wahoo {uuid_str}: {binascii.hexlify(data)}")
|
||||
|
||||
# Funzioni per generare dati
|
||||
def generate_cycling_power_data():
|
||||
global current_power, current_cadence
|
||||
|
||||
# Varia leggermente i valori
|
||||
current_power += random.randint(-3, 3)
|
||||
current_power = max(0, min(2000, current_power))
|
||||
|
||||
current_cadence += random.randint(-1, 1)
|
||||
current_cadence = max(0, min(200, current_cadence))
|
||||
|
||||
# Crea Cycling Power Measurement
|
||||
power_data = bytearray(16)
|
||||
power_data[0:2] = (0x0034).to_bytes(2, byteorder='little')
|
||||
power_data[2:4] = (current_power).to_bytes(2, byteorder='little')
|
||||
power_data[4:8] = (int(current_power * 10)).to_bytes(4, byteorder='little')
|
||||
power_data[8:12] = (0).to_bytes(4, byteorder='little')
|
||||
power_data[12:14] = (current_cadence).to_bytes(2, byteorder='little')
|
||||
power_data[14:16] = (0xBAD8).to_bytes(2, byteorder='little')
|
||||
|
||||
return bytes(power_data)
|
||||
|
||||
def generate_indoor_bike_data():
|
||||
global current_speed, current_cadence
|
||||
|
||||
# Varia leggermente i valori
|
||||
current_speed += random.uniform(-0.2, 0.2)
|
||||
current_speed = max(0, min(60.0, current_speed))
|
||||
|
||||
# Crea Indoor Bike Data
|
||||
bike_data = bytearray(8)
|
||||
bike_data[0:2] = (0x0044).to_bytes(2, byteorder='little')
|
||||
bike_data[2:4] = (int(current_speed * 100)).to_bytes(2, byteorder='little')
|
||||
bike_data[4:6] = (current_cadence).to_bytes(2, byteorder='little')
|
||||
bike_data[6:8] = (0).to_bytes(2, byteorder='little')
|
||||
|
||||
return bytes(bike_data)
|
||||
|
||||
async def run():
|
||||
# Crea server con minimo di parametri
|
||||
server = BlessServer(name=DEVICE_NAME)
|
||||
server.read_request_func = read_request
|
||||
server.write_request_func = write_request
|
||||
|
||||
logger.info(f"Configurazione del simulatore {DEVICE_NAME}...")
|
||||
|
||||
# 1. Servizi standard
|
||||
# Aggiungi Cycling Power Service
|
||||
await server.add_new_service(CYCLING_POWER_SERVICE)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_MEASUREMENT,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_FEATURE,
|
||||
GATTCharacteristicProperties.read,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
CYCLING_POWER_CONTROL_POINT,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
CYCLING_POWER_SERVICE,
|
||||
WAHOO_CUSTOM_CP_CHAR,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Aggiungi Fitness Machine Service
|
||||
await server.add_new_service(FITNESS_MACHINE_SERVICE)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
INDOOR_BIKE_DATA,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
FITNESS_MACHINE_CONTROL_POINT,
|
||||
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
FITNESS_MACHINE_SERVICE,
|
||||
FITNESS_MACHINE_FEATURE,
|
||||
GATTCharacteristicProperties.read,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
|
||||
# 2. Servizi Wahoo personalizzati
|
||||
# Wahoo Service 1
|
||||
await server.add_new_service(WAHOO_SERVICE_1)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_1,
|
||||
WAHOO_CHAR_1,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_1,
|
||||
WAHOO_CHAR_2,
|
||||
GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable
|
||||
)
|
||||
|
||||
# Wahoo Service 3
|
||||
await server.add_new_service(WAHOO_SERVICE_3)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_3,
|
||||
WAHOO_CHAR_3,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Wahoo Service 6
|
||||
await server.add_new_service(WAHOO_SERVICE_6)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_6,
|
||||
WAHOO_CHAR_4,
|
||||
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
# Wahoo Service B
|
||||
await server.add_new_service(WAHOO_SERVICE_B)
|
||||
await server.add_new_characteristic(
|
||||
WAHOO_SERVICE_B,
|
||||
WAHOO_CHAR_5,
|
||||
GATTCharacteristicProperties.read | GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
|
||||
None,
|
||||
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
|
||||
)
|
||||
|
||||
logger.info("Configurazione dei servizi completata")
|
||||
|
||||
# Avvia il server
|
||||
await server.start()
|
||||
logger.info(f"{DEVICE_NAME} è ora in fase di advertising")
|
||||
|
||||
# Imposta i valori iniziali DOPO l'avvio del server
|
||||
# Valori per servizi standard
|
||||
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
|
||||
server.get_characteristic(CYCLING_POWER_FEATURE).value = (0x08).to_bytes(4, byteorder='little')
|
||||
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
|
||||
server.get_characteristic(FITNESS_MACHINE_FEATURE).value = (0x02C6).to_bytes(4, byteorder='little')
|
||||
|
||||
# Valori per caratteristiche Wahoo
|
||||
server.get_characteristic(WAHOO_CHAR_1).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_2).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_3).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_4).value = bytearray(1)
|
||||
server.get_characteristic(WAHOO_CHAR_5).value = bytearray(1)
|
||||
|
||||
# Loop di aggiornamento
|
||||
try:
|
||||
counter = 0
|
||||
while True:
|
||||
# Aggiorna i dati principali
|
||||
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
|
||||
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
|
||||
|
||||
# Invia notifiche
|
||||
server.update_value(FITNESS_MACHINE_SERVICE, INDOOR_BIKE_DATA)
|
||||
server.update_value(CYCLING_POWER_SERVICE, CYCLING_POWER_MEASUREMENT)
|
||||
|
||||
if counter % 10 == 0: # Log ogni 10 cicli
|
||||
logger.info(f"Potenza: {current_power}W, Cadenza: {current_cadence}rpm, Velocità: {current_speed:.1f}km/h")
|
||||
|
||||
counter += 1
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Arresto richiesto dall'utente")
|
||||
except Exception as e:
|
||||
logger.error(f"Errore durante l'esecuzione: {e}")
|
||||
finally:
|
||||
await server.stop()
|
||||
logger.info("Server arrestato")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 80)
|
||||
print(f"Wahoo KICKR 51A6 BLE Simulator per macOS (Versione completa)")
|
||||
print("=" * 80)
|
||||
print(f"Avvio della simulazione di {DEVICE_NAME}")
|
||||
print("Premi Ctrl+C per terminare il server")
|
||||
print("=" * 80)
|
||||
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
print("\nSimulazione fermata dall'utente")
|
||||
except Exception as e:
|
||||
print(f"Errore: {e}")
|
||||
print("Potrebbe essere necessario eseguire questo script con sudo")
|
||||
sys.exit(1)
|
||||
@@ -25,11 +25,6 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
height: 48
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: title
|
||||
Accessible.description: expanded ? "Expanded" : "Collapsed"
|
||||
Accessible.onPressAction: toggle()
|
||||
|
||||
Rectangle {
|
||||
id: indicatRect
|
||||
x: 16; y: 20
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
when you add a setting remember:
|
||||
- you have to add always as the last settings declared in the settings.qml
|
||||
- if you have to add a setting also on another qml file, you need also to declare it there always putting as the last one
|
||||
- in the qzsettings.cpp there is a allsettingscount that must be updated if you add a setting
|
||||
@@ -9,7 +9,6 @@ ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
property int chart_display_mode: 0
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
@@ -20,9 +19,6 @@ ColumnLayout {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
// Send chart display mode to the web view
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
@@ -32,22 +28,4 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes in chart display mode setting
|
||||
Connections {
|
||||
target: settings
|
||||
function onChart_display_modeChanged() {
|
||||
sendDisplayModeToWebView();
|
||||
}
|
||||
}
|
||||
|
||||
function sendDisplayModeToWebView() {
|
||||
if (webView.loading === false) {
|
||||
webView.runJavaScript("
|
||||
if (window.setChartDisplayMode) {
|
||||
window.setChartDisplayMode(" + settings.chart_display_mode + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Executable file → Normal file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Executable file → Normal file
Binary file not shown.
@@ -6,10 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -50,22 +49,9 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -102,11 +88,8 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -116,60 +99,6 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -295,21 +224,6 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,9 +42,6 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -54,17 +51,6 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Executable file → Normal file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Executable file → Normal file
Binary file not shown.
@@ -6,10 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -50,22 +49,9 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -102,11 +88,8 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -116,60 +99,6 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -295,21 +224,6 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,9 +42,6 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -54,17 +51,6 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
@@ -6,11 +6,11 @@
|
||||
<dict>
|
||||
<key>Headers/ConnectIQ.h</key>
|
||||
<data>
|
||||
oktDCwqbdQQg6rdcptAN5TGhUZs=
|
||||
yih4e2KjbC/GqavxdCZ3xQ4mHmA=
|
||||
</data>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<data>
|
||||
CMQ9wDp2PKaw9dRd8NBYpX9xkzE=
|
||||
NDlj8k5C84UPFmD+qEMz2WcZloY=
|
||||
</data>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
<data>
|
||||
@@ -22,11 +22,11 @@
|
||||
</data>
|
||||
<key>Headers/IQDevice.h</key>
|
||||
<data>
|
||||
a4hkgIut7ETtkOJXPkn/nGElEYg=
|
||||
bl545C/cu0mw2KlRmzojKmHPom0=
|
||||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
LeO8CbXcC4FrKgyl2zDm7R7nOj0=
|
||||
YUOCJU/YBLc4CRWV1z8JHDjCx8M=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
@@ -300,14 +300,14 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
E2QDme6rWC+CJc/kKtxIVSpPzbE4ArUwNagnLG6Nxis=
|
||||
kAenemss8n98vVLi54JqBUtGwaL1/i+HSejFBZgawHA=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
KhyZorkoK2Qipuzee5aE5ENCarHR+Ni21GdxCV3FQ0s=
|
||||
bSRRooQ0FKFr3BgrFolAnkU402889YFHrH+6EEca3cg=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
@@ -328,7 +328,7 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
Xx+4dhu0JD6w2pd9UMvLXukYVQfKzaLJhU0paDUQyls=
|
||||
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/module.modulemap</key>
|
||||
|
||||
@@ -92,7 +92,7 @@ class BluetoothHandler : public QObject
|
||||
void onKeyPressed(int keyCode)
|
||||
{
|
||||
qDebug() << "Key pressed:" << keyCode;
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == BIKE) {
|
||||
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) {
|
||||
if (keyCode == 115) // up
|
||||
((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1);
|
||||
else if (keyCode == 114) // down
|
||||
|
||||
@@ -13,32 +13,22 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +236,7 @@ ColumnLayout {
|
||||
elevationGain = elevationGain + (pathController.geopath.coordinateAt(i).altitude - pathController.geopath.coordinateAt(i-1).altitude)
|
||||
lines[i] = pathController.geopath.coordinateAt(i)
|
||||
}
|
||||
distance.text = "Distance " + pathController.distance.toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
distance.text = "Distance " + (pathController.geopath.length() / 1000.0).toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -273,8 +263,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
fileDialogTrainProgram.visible = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
68
src/Home.qml
68
src/Home.qml
@@ -14,10 +14,6 @@ HomeForm {
|
||||
width: parent.fill
|
||||
height: parent.fill
|
||||
color: settings.theme_background_color
|
||||
|
||||
// VoiceOver accessibility - ignore decorative background
|
||||
Accessible.role: Accessible.Pane
|
||||
Accessible.ignored: true
|
||||
}
|
||||
signal start_clicked;
|
||||
signal stop_clicked;
|
||||
@@ -76,19 +72,7 @@ HomeForm {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("New lap started!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: stopConfirmationDialog
|
||||
text: qsTr("Stop Workout")
|
||||
informativeText: qsTr("Do you really want to stop the current workout?")
|
||||
buttons: (MessageDialog.Yes | MessageDialog.No)
|
||||
onYesClicked: {
|
||||
close();
|
||||
inner_stop();
|
||||
}
|
||||
onNoClicked: close()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -157,11 +141,7 @@ HomeForm {
|
||||
|
||||
start.onClicked: { start_clicked(); }
|
||||
stop.onClicked: {
|
||||
if (rootItem.confirmStopEnabled()) {
|
||||
stopConfirmationDialog.open();
|
||||
} else {
|
||||
inner_stop();
|
||||
}
|
||||
inner_stop();
|
||||
}
|
||||
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
|
||||
|
||||
@@ -189,8 +169,6 @@ HomeForm {
|
||||
gridView.leftMargin = (parent.width % cellWidth) / 2;
|
||||
}
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
delegate: Item {
|
||||
id: id1
|
||||
width: 170 * settings.ui_zoom / 100
|
||||
@@ -199,12 +177,6 @@ HomeForm {
|
||||
visible: visibleItem
|
||||
Component.onCompleted: console.log("completed " + objectName)
|
||||
|
||||
// VoiceOver accessibility support
|
||||
Accessible.role: largeButton ? Accessible.Button : (writable ? Accessible.Pane : Accessible.StaticText)
|
||||
Accessible.name: name + (largeButton ? "" : (": " + value))
|
||||
Accessible.description: largeButton ? largeButtonLabel : (secondLine !== "" ? secondLine : (writable ? qsTr("Adjustable. Current value: ") + value : qsTr("Current value: ") + value))
|
||||
Accessible.focusable: true
|
||||
|
||||
Behavior on x {
|
||||
enabled: id1.state != "active"
|
||||
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
|
||||
@@ -238,9 +210,6 @@ HomeForm {
|
||||
border.color: (settings.theme_tile_shadow_enabled ? settings.theme_tile_shadow_color : settings.theme_tile_background_color)
|
||||
color: settings.theme_tile_background_color
|
||||
id: rect
|
||||
|
||||
// Ignore for VoiceOver - decorative background only
|
||||
Accessible.ignored: true
|
||||
}
|
||||
|
||||
DropShadow {
|
||||
@@ -271,9 +240,6 @@ HomeForm {
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
source: icon
|
||||
visible: settings.theme_tile_icon_enabled && !largeButton
|
||||
|
||||
// Ignore for VoiceOver - decorative only
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
objectName: "value"
|
||||
@@ -288,9 +254,6 @@ HomeForm {
|
||||
font.pointSize: valueFontSize * settings.ui_zoom / 100
|
||||
font.bold: true
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
objectName: "secondLine"
|
||||
@@ -306,9 +269,6 @@ HomeForm {
|
||||
font.pointSize: settings.theme_tile_secondline_textsize * settings.ui_zoom / 100
|
||||
font.bold: false
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
Text {
|
||||
id: myText
|
||||
@@ -323,9 +283,6 @@ HomeForm {
|
||||
anchors.leftMargin: 55 * settings.ui_zoom / 100
|
||||
anchors.topMargin: 20 * settings.ui_zoom / 100
|
||||
visible: !largeButton
|
||||
|
||||
// Ignore for VoiceOver - parent Item handles accessibility
|
||||
Accessible.ignored: true
|
||||
}
|
||||
RoundButton {
|
||||
objectName: minusName
|
||||
@@ -338,13 +295,6 @@ HomeForm {
|
||||
anchors.leftMargin: 2
|
||||
width: 48 * settings.ui_zoom / 100
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Decrease ") + name
|
||||
Accessible.description: qsTr("Decrease the value of ") + name
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { minus_clicked(objectName) }
|
||||
}
|
||||
RoundButton {
|
||||
autoRepeat: true
|
||||
@@ -357,13 +307,6 @@ HomeForm {
|
||||
anchors.rightMargin: 2
|
||||
width: 48 * settings.ui_zoom / 100
|
||||
height: 48 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Increase ") + name
|
||||
Accessible.description: qsTr("Increase the value of ") + name
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { plus_clicked(objectName) }
|
||||
}
|
||||
RoundButton {
|
||||
autoRepeat: true
|
||||
@@ -377,13 +320,6 @@ HomeForm {
|
||||
radius: 20
|
||||
}
|
||||
font.pointSize: 20 * settings.ui_zoom / 100
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: largeButtonLabel
|
||||
Accessible.description: name + ": " + largeButtonLabel
|
||||
Accessible.focusable: true
|
||||
Accessible.onPressAction: { largeButton_clicked(objectName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ Page {
|
||||
title: qsTr("QZ Fitness")
|
||||
id: page
|
||||
|
||||
// VoiceOver accessibility - ignore Page itself, only children are accessible
|
||||
Accessible.ignored: true
|
||||
|
||||
property alias start: start
|
||||
property alias stop: stop
|
||||
property alias lap: lap
|
||||
@@ -42,8 +39,6 @@ Page {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -52,13 +47,10 @@ Page {
|
||||
height: row.height
|
||||
spacing: 0
|
||||
padding: 0
|
||||
Accessible.ignored: true
|
||||
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
Image {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -68,12 +60,6 @@ Page {
|
||||
source: "icons/icons/bluetooth-icon.png"
|
||||
enabled: rootItem.device
|
||||
smooth: true
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Indicator
|
||||
Accessible.name: qsTr("Bluetooth connection")
|
||||
Accessible.description: rootItem.device ? qsTr("Device connected") : qsTr("Device not connected")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: treadmill_connection
|
||||
@@ -88,7 +74,6 @@ Page {
|
||||
height: row.height - 76
|
||||
source: rootItem.signal
|
||||
smooth: true
|
||||
Accessible.ignored: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +82,6 @@ Page {
|
||||
width: 120
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
icon.source: rootItem.startIcon
|
||||
icon.height: row.height - 54
|
||||
@@ -108,12 +91,6 @@ Page {
|
||||
id: start
|
||||
width: 120
|
||||
height: row.height - 4
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: rootItem.startText
|
||||
Accessible.description: qsTr("Start workout")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: start
|
||||
@@ -127,7 +104,6 @@ Page {
|
||||
width: 120
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
icon.source: rootItem.stopIcon
|
||||
@@ -138,12 +114,6 @@ Page {
|
||||
id: stop
|
||||
width: 120
|
||||
height: row.height - 4
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: rootItem.stopText
|
||||
Accessible.description: qsTr("Stop workout")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: stop
|
||||
@@ -158,8 +128,6 @@ Page {
|
||||
width: 50
|
||||
height: row.height
|
||||
color: settings.theme_background_color
|
||||
Accessible.ignored: true
|
||||
|
||||
RoundButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
id: lap
|
||||
@@ -170,12 +138,6 @@ Page {
|
||||
icon.height: 48
|
||||
enabled: rootItem.lap
|
||||
smooth: true
|
||||
|
||||
// VoiceOver accessibility
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Lap")
|
||||
Accessible.description: qsTr("Record a new lap")
|
||||
Accessible.focusable: true
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: lap
|
||||
@@ -203,7 +165,7 @@ Page {
|
||||
width: parent.width
|
||||
anchors.top: row1.bottom
|
||||
anchors.topMargin: 30
|
||||
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) both Bluetooth and Bluetooth permissions MUST be enabled<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
|
||||
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) bluetooth and bluetooth permission MUST be on<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
|
||||
wrapMode: Label.WordWrap
|
||||
visible: rootItem.labelHelp
|
||||
}
|
||||
|
||||
@@ -22,11 +22,6 @@ ColumnLayout {
|
||||
Layout.fillWidth: true;
|
||||
height: 48
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: title
|
||||
Accessible.description: expanded ? "Expanded" : "Collapsed"
|
||||
Accessible.onPressAction: toggle()
|
||||
|
||||
Rectangle{
|
||||
id:indicatRect
|
||||
x: 16; y: 20
|
||||
|
||||
26
src/OAuth2.h
26
src/OAuth2.h
@@ -1,26 +0,0 @@
|
||||
#ifndef OAUTH2_H
|
||||
#define OAUTH2_H
|
||||
|
||||
#include <QString>
|
||||
#include <QTextStream>
|
||||
|
||||
struct OAuth2Parameter {
|
||||
QString responseType = QStringLiteral("code");
|
||||
QString approval_prompt = QStringLiteral("force");
|
||||
|
||||
inline bool isEmpty() const { return responseType.isEmpty() && approval_prompt.isEmpty(); }
|
||||
|
||||
QString toString() const {
|
||||
QString msg;
|
||||
QTextStream out(&msg);
|
||||
out << QStringLiteral("OAuth2Parameter{\n") << QStringLiteral("responseType: ") << this->responseType
|
||||
<< QStringLiteral("\n") << QStringLiteral("approval_prompt: ") << this->approval_prompt
|
||||
<< QStringLiteral("\n");
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
#define _STR(x) #x
|
||||
#define STRINGIFY(x) _STR(x)
|
||||
|
||||
#endif // OAUTH2_H
|
||||
@@ -42,27 +42,11 @@ class PathController : public QObject {
|
||||
|
||||
void centerChanged() W_SIGNAL(centerChanged)
|
||||
|
||||
double distance() const {
|
||||
return mDistance;
|
||||
}
|
||||
|
||||
void setDistance(double distance) {
|
||||
if (qFuzzyCompare(distance, mDistance)) {
|
||||
return;
|
||||
}
|
||||
mDistance = distance;
|
||||
emit distanceChanged();
|
||||
}
|
||||
|
||||
void distanceChanged() W_SIGNAL(distanceChanged)
|
||||
|
||||
private : QGeoPath mGeoPath;
|
||||
QGeoCoordinate mCenter;
|
||||
double mDistance = 0.0;
|
||||
|
||||
W_PROPERTY(QGeoPath, geopath READ geoPath WRITE setGeoPath NOTIFY geopathChanged)
|
||||
W_PROPERTY(QGeoCoordinate, center READ center WRITE setCenter NOTIFY centerChanged)
|
||||
W_PROPERTY(double, distance READ distance WRITE setDistance NOTIFY distanceChanged)
|
||||
};
|
||||
|
||||
#endif // APPLICATION_PATHCONTROLLER_H
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal popupclose()
|
||||
id: column1
|
||||
spacing: 10
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
|
||||
visible: true
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.errorString) {
|
||||
console.error(loadRequest.errorString);
|
||||
console.error("port " + settings.value("template_inner_QZWS_port"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: chartJscheckStartFromWeb
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: closeButton
|
||||
height: 50
|
||||
width: parent.width
|
||||
text: "Close"
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
popupclose();
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
headerToolbar.visible = true;
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,18 @@ import QtQuick.Dialogs 1.0
|
||||
|
||||
ColumnLayout {
|
||||
signal loadSettings(url name)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
loadSettings(fileUrl)
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
FileDialog {
|
||||
id: fileDialogSettings
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogSettings.fileUrl)
|
||||
loadSettings(fileDialogSettings.fileUrl)
|
||||
fileDialogSettings.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogSettings.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +106,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
fileDialogSettings.visible = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import QtQuick 2.0
|
||||
import AndroidStatusBar 1.0
|
||||
import QtQuick.Window 2.12
|
||||
|
||||
/**
|
||||
* adapted from StackOverflow:
|
||||
@@ -31,9 +29,7 @@ ListView {
|
||||
z: Infinity
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ?
|
||||
((Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight + 10 : 10) : 10
|
||||
anchors.bottomMargin: 10
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
interactive: false
|
||||
|
||||
@@ -11,32 +11,22 @@ ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileUrl)
|
||||
}
|
||||
close()
|
||||
// Destroy and recreate the dialog for next use
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
// Destroy the dialog
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
FileDialog {
|
||||
id: fileDialogTrainProgram
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
|
||||
} else {
|
||||
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
|
||||
}
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
fileDialogTrainProgram.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +64,12 @@ ColumnLayout {
|
||||
id: filterField
|
||||
onTextChanged: updateFilter()
|
||||
}
|
||||
Button {
|
||||
anchors.left: mainRect.right
|
||||
anchors.leftMargin: 5
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
Button {
|
||||
anchors.left: mainRect.right
|
||||
anchors.leftMargin: 5
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -95,10 +85,10 @@ ColumnLayout {
|
||||
id: folderModel
|
||||
nameFilters: ["*.xml", "*.zwo"]
|
||||
folder: "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
showDotAndDotDot: false
|
||||
showDotAndDotDot: false
|
||||
showDirs: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
}
|
||||
model: folderModel
|
||||
delegate: Component {
|
||||
@@ -106,7 +96,7 @@ ColumnLayout {
|
||||
property alias textColor: fileTextBox.color
|
||||
width: parent.width
|
||||
height: 40
|
||||
color: Material.backgroundColor
|
||||
color: Material.backgroundColor
|
||||
z: 1
|
||||
Item {
|
||||
id: root
|
||||
@@ -145,12 +135,12 @@ ColumnLayout {
|
||||
console.log('onclicked ' + index+ " count "+list.count);
|
||||
if (index == list.currentIndex) {
|
||||
let fileUrl = folderModel.get(list.currentIndex, 'fileUrl') || folderModel.get(list.currentIndex, 'fileURL');
|
||||
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
|
||||
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
|
||||
trainprogram_open_clicked(fileUrl);
|
||||
popup.open()
|
||||
} else {
|
||||
folderModel.folder = fileURL
|
||||
}
|
||||
} else {
|
||||
folderModel.folder = fileURL
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (list.currentItem)
|
||||
@@ -306,8 +296,7 @@ ColumnLayout {
|
||||
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
|
||||
onClicked: {
|
||||
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
|
||||
// Create a fresh FileDialog instance
|
||||
fileDialogLoader.active = true
|
||||
fileDialogTrainProgram.visible = true
|
||||
}
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
import QtQuick 2.7
|
||||
import Qt.labs.folderlistmodel 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import QtQuick.Dialogs 1.0
|
||||
import Qt.labs.settings 1.0
|
||||
import Qt.labs.platform 1.1
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
signal trainprogram_open_clicked(url name)
|
||||
signal trainprogram_open_other_folder(url name)
|
||||
signal trainprogram_preview(url name)
|
||||
signal trainprogram_autostart_requested()
|
||||
|
||||
property url pendingWorkoutUrl: ""
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
property real ftp: 200.0
|
||||
}
|
||||
|
||||
property var selectedFileUrl: ""
|
||||
|
||||
Loader {
|
||||
id: fileDialogLoader
|
||||
active: false
|
||||
sourceComponent: Component {
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: "Please choose a file"
|
||||
folder: shortcuts.home
|
||||
visible: true
|
||||
onAccepted: {
|
||||
var chosenFile = fileDialog.fileUrl || fileDialog.file || (fileDialog.fileUrls && fileDialog.fileUrls.length > 0 ? fileDialog.fileUrls[0] : "")
|
||||
console.log("You chose: " + chosenFile)
|
||||
selectedFileUrl = chosenFile
|
||||
if(OS_VERSION === "Android") {
|
||||
trainprogram_open_other_folder(chosenFile)
|
||||
} else {
|
||||
trainprogram_open_clicked(chosenFile)
|
||||
}
|
||||
close()
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
onRejected: {
|
||||
console.log("Canceled")
|
||||
close()
|
||||
fileDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
initialItem: masterView
|
||||
|
||||
// MASTER VIEW - Lista Workout
|
||||
Component {
|
||||
id: masterView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: "Filter"
|
||||
color: "white"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: filterField
|
||||
Layout.fillWidth: true
|
||||
|
||||
function updateFilter() {
|
||||
var text = filterField.text
|
||||
var filter = "*"
|
||||
for(var i = 0; i<text.length; i++)
|
||||
filter+= "[%1%2]".arg(text[i].toUpperCase()).arg(text[i].toLowerCase())
|
||||
filter+="*"
|
||||
folderModel.nameFilters = [filter + ".zwo", filter + ".xml"]
|
||||
}
|
||||
|
||||
onTextChanged: updateFilter()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "←"
|
||||
onClicked: folderModel.folder = folderModel.parentFolder
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
id: list
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
nameFilters: ["*.xml", "*.zwo"]
|
||||
folder: "file://" + rootItem.getWritableAppDir() + 'training'
|
||||
showDotAndDotDot: false
|
||||
showDirs: true
|
||||
sortField: "Name"
|
||||
showDirsFirst: true
|
||||
}
|
||||
|
||||
model: folderModel
|
||||
|
||||
delegate: Component {
|
||||
Rectangle {
|
||||
width: ListView.view.width
|
||||
height: 50
|
||||
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
id: fileIcon
|
||||
text: folderModel.isFolder(index) ? "📁" : "📄"
|
||||
font.pixelSize: 24
|
||||
}
|
||||
|
||||
Text {
|
||||
id: fileName
|
||||
Layout.fillWidth: true
|
||||
text: !folderModel.isFolder(index) ?
|
||||
folderModel.get(index, "fileName").substring(0, folderModel.get(index, "fileName").length-4) :
|
||||
folderModel.get(index, "fileName")
|
||||
color: folderModel.isFolder(index) ? Material.color(Material.Orange) : "white"
|
||||
font.pixelSize: 16
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"
|
||||
font.pixelSize: 24
|
||||
color: Material.color(Material.Grey)
|
||||
visible: !ListView.isCurrentItem
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = index
|
||||
let fileUrl = folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL');
|
||||
|
||||
if (folderModel.isFolder(index)) {
|
||||
// Navigate to folder
|
||||
folderModel.folder = fileUrl
|
||||
} else if (fileUrl) {
|
||||
// Load preview and show detail view
|
||||
console.log('Loading preview for: ' + fileUrl);
|
||||
trainprogram_preview(fileUrl)
|
||||
pendingWorkoutUrl = fileUrl
|
||||
|
||||
// Wait for preview to load then push detail view
|
||||
detailViewTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
text: "Other folders"
|
||||
onClicked: {
|
||||
fileDialogLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to push detail view after preview loads
|
||||
Timer {
|
||||
id: detailViewTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
stackView.push(detailView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DETAIL VIEW - Anteprima Workout
|
||||
Component {
|
||||
id: detailView
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 10
|
||||
|
||||
// Header con pulsanti
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 5
|
||||
spacing: 10
|
||||
|
||||
Button {
|
||||
text: "← Back"
|
||||
onClicked: stackView.pop()
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: "Start Workout"
|
||||
highlighted: true
|
||||
Material.background: Material.Green
|
||||
onClicked: {
|
||||
trainprogram_open_clicked(pendingWorkoutUrl)
|
||||
trainprogram_autostart_requested()
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Descrizione workout
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 10
|
||||
text: rootItem.previewWorkoutDescription
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
color: "white"
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
text: rootItem.previewWorkoutTags
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
color: Material.color(Material.Grey, Material.Shade400)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// WebView con grafico
|
||||
WebView {
|
||||
id: previewWebView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/workoutpreview/preview.html"
|
||||
|
||||
Component.onCompleted: {
|
||||
// Update workout after a short delay to ensure data is loaded
|
||||
updateTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: previewWebView.updateWorkout()
|
||||
}
|
||||
|
||||
function updateWorkout() {
|
||||
if (!rootItem.preview_workout_points) return;
|
||||
|
||||
// Build arrays for the workout data
|
||||
var watts = [];
|
||||
var speed = [];
|
||||
var inclination = [];
|
||||
var resistance = [];
|
||||
var cadence = [];
|
||||
|
||||
var hasWatts = false;
|
||||
var hasSpeed = false;
|
||||
var hasInclination = false;
|
||||
var hasResistance = false;
|
||||
var hasCadence = false;
|
||||
|
||||
for (var i = 0; i < rootItem.preview_workout_points; i++) {
|
||||
if (rootItem.preview_workout_watt && rootItem.preview_workout_watt[i] !== undefined && rootItem.preview_workout_watt[i] > 0) {
|
||||
watts.push({ x: i, y: rootItem.preview_workout_watt[i] });
|
||||
hasWatts = true;
|
||||
}
|
||||
if (rootItem.preview_workout_speed && rootItem.preview_workout_speed[i] !== undefined && rootItem.preview_workout_speed[i] > 0) {
|
||||
speed.push({ x: i, y: rootItem.preview_workout_speed[i] });
|
||||
hasSpeed = true;
|
||||
}
|
||||
if (rootItem.preview_workout_inclination && rootItem.preview_workout_inclination[i] !== undefined && rootItem.preview_workout_inclination[i] > -200) {
|
||||
inclination.push({ x: i, y: rootItem.preview_workout_inclination[i] });
|
||||
hasInclination = true;
|
||||
}
|
||||
if (rootItem.preview_workout_resistance && rootItem.preview_workout_resistance[i] !== undefined && rootItem.preview_workout_resistance[i] >= 0) {
|
||||
resistance.push({ x: i, y: rootItem.preview_workout_resistance[i] });
|
||||
hasResistance = true;
|
||||
}
|
||||
if (rootItem.preview_workout_cadence && rootItem.preview_workout_cadence[i] !== undefined && rootItem.preview_workout_cadence[i] > 0) {
|
||||
cadence.push({ x: i, y: rootItem.preview_workout_cadence[i] });
|
||||
hasCadence = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device type based on available data
|
||||
var deviceType = 'bike'; // default
|
||||
|
||||
// Priority 1: If has resistance, it's a bike (regardless of inclination)
|
||||
if (hasResistance) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
// Priority 2: If has speed or inclination (without resistance), it's a treadmill
|
||||
else if (hasSpeed || hasInclination) {
|
||||
deviceType = 'treadmill';
|
||||
}
|
||||
// Priority 3: If has power or cadence (bike metrics), it's a bike
|
||||
else if (hasWatts || hasCadence) {
|
||||
deviceType = 'bike';
|
||||
}
|
||||
|
||||
// Call JavaScript function in the WebView
|
||||
var data = {
|
||||
points: rootItem.preview_workout_points,
|
||||
watts: watts,
|
||||
speed: speed,
|
||||
inclination: inclination,
|
||||
resistance: resistance,
|
||||
cadence: cadence,
|
||||
deviceType: deviceType,
|
||||
miles_unit: settings.value("miles_unit", false)
|
||||
};
|
||||
|
||||
runJavaScript("if(window.setWorkoutData) window.setWorkoutData(" + JSON.stringify(data) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtQuick.Dialogs 1.0
|
||||
import QtGraphicalEffects 1.12
|
||||
import Qt.labs.settings 1.0
|
||||
import QtMultimedia 5.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: true
|
||||
|
||||
WebView {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: !rootItem.generalPopupVisible
|
||||
url: rootItem.getIntervalsICUAuthUrl
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupIntervalsICUConnectedWeb
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.generalPopupVisible
|
||||
onEnabledChanged: { if(rootItem.generalPopupVisible) popupIntervalsICUConnectedWeb.open() }
|
||||
onClosed: { rootItem.generalPopupVisible = false; }
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
width: 380
|
||||
height: 120
|
||||
modal: true
|
||||
focus: true
|
||||
palette.text: "white"
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enter: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 370
|
||||
height: 120
|
||||
text: qsTr("Your Intervals.icu account is now connected!<br><br>When you will press STOP on QZ a file<br>will be automatically uploaded to Intervals.icu!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import QtQuick.Controls.Material 2.12
|
||||
import QtQuick.Dialogs 1.0
|
||||
import QtGraphicalEffects 1.12
|
||||
import Qt.labs.settings 1.0
|
||||
import QtMultimedia 5.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
id: pelotonAuthPage
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: true
|
||||
|
||||
// Signal to notify the parent stack when we want to go back
|
||||
signal goBack()
|
||||
|
||||
WebView {
|
||||
anchors.fill: parent
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
visible: !rootItem.pelotonPopupVisible
|
||||
url: rootItem.getPelotonAuthUrl
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupPelotonConnectedWeb
|
||||
parent: Overlay.overlay
|
||||
enabled: rootItem.pelotonPopupVisible
|
||||
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
|
||||
onClosed: {
|
||||
rootItem.pelotonPopupVisible = false;
|
||||
// Attempt to go back to the previous view after the popup is closed
|
||||
goBack();
|
||||
}
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
width: 380
|
||||
height: 120
|
||||
modal: true
|
||||
focus: true
|
||||
palette.text: "white"
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enter: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 370
|
||||
height: 120
|
||||
text: qsTr("Your Peloton account is now connected!")
|
||||
}
|
||||
}
|
||||
|
||||
// Add a MouseArea to capture clicks anywhere on the popup
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
popupPelotonConnectedWeb.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Component is being completed
|
||||
Component.onCompleted: {
|
||||
console.log("WebPelotonAuth loaded")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import Qt.labs.settings 1.0
|
||||
|
||||
Page {
|
||||
id: wizardPage
|
||||
objectName: "wizardPage"
|
||||
|
||||
property int currentStep: 0
|
||||
property var selectedOptions: ({})
|
||||
@@ -336,7 +335,7 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Connect to Peloton")
|
||||
text: qsTr("Peloton Login")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: "white"
|
||||
@@ -344,38 +343,56 @@ Page {
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Click the button below to connect your Peloton account")
|
||||
text: qsTr("Username")
|
||||
font.pixelSize: 20
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
width: stackViewLocal.width * 0.8
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.bold: true
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Image {
|
||||
TextField {
|
||||
id: pelotonUsernameTextField
|
||||
text: settings.peloton_username
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: parent.width * 0.8
|
||||
Layout.fillHeight: false
|
||||
onAccepted: settings.peloton_username = text
|
||||
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
stackViewLocal.push("WebPelotonAuth.qml")
|
||||
stackViewLocal.currentItem.goBack.connect(function() {
|
||||
stackViewLocal.pop();
|
||||
stackViewLocal.push(pelotonDifficultyComponent)
|
||||
})
|
||||
peloton_connect_clicked()
|
||||
}
|
||||
}
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Password")
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
color: "white"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: pelotonPasswordTextField
|
||||
text: settings.peloton_password
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
inputMethodHints: Qt.ImhHiddenText
|
||||
echoMode: TextInput.PasswordEchoOnEdit
|
||||
onAccepted: settings.peloton_password = text
|
||||
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 50
|
||||
}
|
||||
|
||||
WizardButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Next")
|
||||
onClicked: {
|
||||
settings.peloton_username = pelotonUsernameTextField.text;
|
||||
settings.peloton_password = pelotonPasswordTextField.text;
|
||||
stackViewLocal.push(pelotonDifficultyComponent)
|
||||
}
|
||||
}
|
||||
|
||||
WizardButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Back")
|
||||
@@ -845,6 +862,7 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 0.5;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
@@ -903,6 +921,7 @@ Page {
|
||||
text: qsTr("Finish")
|
||||
onClicked: {
|
||||
settings.tile_gears_enabled = true;
|
||||
settings.gears_gain = 1;
|
||||
stackViewLocal.push(finalStepComponent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.5
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string title: qsTr("Workout Editor")
|
||||
property bool pageLoaded: false
|
||||
|
||||
signal closeRequested()
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: portPoller
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: !root.pageLoaded
|
||||
onTriggered: {
|
||||
var port = settings.value("template_inner_QZWS_port", 0)
|
||||
if (!port) {
|
||||
return
|
||||
}
|
||||
var targetUrl = "http://localhost:" + port + "/workouteditor/index.html"
|
||||
if (webView.url !== targetUrl) {
|
||||
webView.url = targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
visible: root.pageLoaded
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.status === WebView.LoadSucceededStatus) {
|
||||
root.pageLoaded = true
|
||||
busy.visible = false
|
||||
busy.running = false
|
||||
portPoller.stop()
|
||||
} else if (loadRequest.status === WebView.LoadFailedStatus) {
|
||||
root.pageLoaded = false
|
||||
busy.visible = true
|
||||
busy.running = true
|
||||
portPoller.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: busy
|
||||
anchors.centerIn: parent
|
||||
visible: !root.pageLoaded
|
||||
running: !root.pageLoaded
|
||||
}
|
||||
|
||||
Component.onCompleted: portPoller.start()
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string workoutSource: "QZ"
|
||||
property alias text: tagText.text
|
||||
|
||||
// Auto-size based on text
|
||||
width: tagText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
|
||||
// Color scheme based on workout source
|
||||
color: {
|
||||
switch(workoutSource.toUpperCase()) {
|
||||
case "PELOTON": return "#ff6b35"
|
||||
case "ZWIFT": return "#ff6900"
|
||||
case "ERG": return "#8bc34a"
|
||||
case "QZ": return "#2196f3"
|
||||
case "MANUAL": return "#757575"
|
||||
default: return "#9e9e9e"
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle border for better definition
|
||||
border.color: Qt.darker(color, 1.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: tagText
|
||||
anchors.centerIn: parent
|
||||
text: workoutSource.toUpperCase()
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Arial"
|
||||
}
|
||||
|
||||
// Subtle shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 1
|
||||
anchors.leftMargin: 1
|
||||
radius: parent.radius
|
||||
color: "#20000000"
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Hover effect for interactivity feedback
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,910 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtCharts 2.15
|
||||
import Qt.labs.calendar 1.0
|
||||
|
||||
Page {
|
||||
id: workoutHistoryPage
|
||||
|
||||
|
||||
// Signal for chart preview
|
||||
signal fitfile_preview_clicked(var url)
|
||||
|
||||
// Helper function to wrap text with emoji font only on Android
|
||||
function wrapEmoji(emoji) {
|
||||
return Qt.platform.os === "android" ?
|
||||
'<font face="' + fontManager.emojiFontFamily + '">' + emoji + '</font>' :
|
||||
emoji;
|
||||
}
|
||||
|
||||
// Sport type to icon mapping (using FIT_SPORT values)
|
||||
function getSportIcon(sport) {
|
||||
switch(parseInt(sport)) {
|
||||
case 1: // FIT_SPORT_RUNNING
|
||||
case 11: // FIT_SPORT_WALKING
|
||||
return "🏃"; // Running/Walking
|
||||
case 2: // FIT_SPORT_CYCLING
|
||||
return "🚴"; // Cycling
|
||||
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
|
||||
return "⭕"; // Elliptical
|
||||
case 15: // FIT_SPORT_ROWING
|
||||
return "🚣"; // Rowing
|
||||
case 84: // FIT_SPORT_JUMPROPE
|
||||
return "🪢"; // Jump Rope
|
||||
default:
|
||||
return "💪"; // Generic workout
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 60
|
||||
color: "#f5f5f5"
|
||||
|
||||
// Calendar Icon Button - positioned absolutely on the left
|
||||
Button {
|
||||
id: calendarButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: calendarButton.pressed ? "#e0e0e0" : "#f0f0f0"
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📅") :
|
||||
"📅"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 20
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
calendarPopup.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Title with filter status - centered
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Workout History"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: workoutModel && workoutModel.isDateFiltered ?
|
||||
"Filtered: " + workoutModel.filteredDate.toLocaleDateString() : ""
|
||||
font.pixelSize: 12
|
||||
color: "#666666"
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Filter Button - positioned absolutely on the right
|
||||
Button {
|
||||
id: clearFilterButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 12
|
||||
width: 100
|
||||
height: 36
|
||||
visible: workoutModel && workoutModel.isDateFiltered
|
||||
|
||||
background: Rectangle {
|
||||
radius: 6
|
||||
color: clearFilterButton.pressed ? "#ff6666" : "#ff8888"
|
||||
border.color: "#ff4444"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "Clear Filter"
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.clearDateFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
BusyIndicator {
|
||||
id: loadingIndicator
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
// Database processing message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
|
||||
text: "Processing workout files...\nThis may take a few moments on first startup."
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#666666"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
// Workout List
|
||||
ListView {
|
||||
id: workoutListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: streakBanner.visible ? streakBanner.height + 10 : 10
|
||||
model: workoutModel
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
onContentYChanged: {
|
||||
// Hide banner when scrolling down, show when at top
|
||||
streakBanner.visible = contentY <= 20
|
||||
}
|
||||
|
||||
delegate: SwipeDelegate {
|
||||
id: swipeDelegate
|
||||
width: parent.width
|
||||
height: 135
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Delegate data:", JSON.stringify({
|
||||
sport: sport,
|
||||
title: title,
|
||||
date: date,
|
||||
duration: duration,
|
||||
distance: distance,
|
||||
calories: calories,
|
||||
id: id
|
||||
}))
|
||||
}
|
||||
|
||||
swipe.right: Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: "#FF4444"
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 20
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🗑️") + " Delete" :
|
||||
"🗑️ Delete"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
color: "white"
|
||||
font.pixelSize: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipe.onCompleted: {
|
||||
// Show confirmation dialog
|
||||
confirmDialog.workoutId = model.id
|
||||
confirmDialog.workoutTitle = model.title
|
||||
confirmDialog.open()
|
||||
}
|
||||
|
||||
// Card-like container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
radius: 10
|
||||
color: "white"
|
||||
border.color: "#e0e0e0"
|
||||
|
||||
// Workout Type Tag - positioned absolutely in top-right
|
||||
WorkoutTypeTag {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
workoutSource: workoutModel ? workoutModel.getWorkoutSource(model.id) : "QZ"
|
||||
}
|
||||
|
||||
// Action buttons - positioned absolutely in bottom-right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
// Peloton URL button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.getWorkoutSource(model.id) === "PELOTON" &&
|
||||
workoutModel.getPelotonUrl(model.id) !== ""
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#ff8855" : "#ff6b35"
|
||||
radius: 6
|
||||
border.color: "#cc5529"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🌐") :
|
||||
"🌐"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
workoutModel.openPelotonUrl(model.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program button
|
||||
Button {
|
||||
width: 40
|
||||
height: 45
|
||||
visible: workoutModel && workoutModel.hasTrainingProgram(model.id)
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? "#1976d2" : "#2196f3"
|
||||
radius: 6
|
||||
border.color: "#1565c0"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("📋") :
|
||||
"📋"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var success = workoutModel.loadTrainingProgram(model.id)
|
||||
if (success) {
|
||||
trainingProgramDialog.title = "Success"
|
||||
trainingProgramDialog.message = "Training program loaded successfully!"
|
||||
trainingProgramDialog.isSuccess = true
|
||||
} else {
|
||||
trainingProgramDialog.title = "Error"
|
||||
trainingProgramDialog.message = "Failed to load training program. Please check if the file exists."
|
||||
trainingProgramDialog.isSuccess = false
|
||||
}
|
||||
trainingProgramDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 16
|
||||
|
||||
// Sport icon
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji(getSportIcon(sport)) :
|
||||
getSportIcon(sport)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: 32
|
||||
}
|
||||
}
|
||||
|
||||
// Workout info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
// Title row (without tag) with auto-scrolling
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 80 // Reserve space for tag
|
||||
Layout.preferredHeight: 24
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
text: title
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Auto-scroll animation for long titles
|
||||
SequentialAnimation on x {
|
||||
running: titleText.contentWidth > titleText.parent.width
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0
|
||||
to: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 1500 }
|
||||
NumberAnimation {
|
||||
from: -(titleText.contentWidth - titleText.parent.width + 20)
|
||||
to: 0
|
||||
duration: Math.max(3000, titleText.contentWidth * 30)
|
||||
}
|
||||
PauseAnimation { duration: 2000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: date
|
||||
color: "#666666"
|
||||
}
|
||||
|
||||
// Stats row
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "⏱ " + duration
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "📏 " + distance.toFixed(2) + " km"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " + Math.round(calories) + " kcal" :
|
||||
"🔥 " + Math.round(calories) + " kcal"
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
console.log("Workout clicked, ID:", model.id)
|
||||
|
||||
// Get workout details from the model
|
||||
var details = workoutModel.getWorkoutDetails(model.id)
|
||||
console.log("Workout details:", JSON.stringify(details))
|
||||
|
||||
// Emit signal with file URL for chart preview - same pattern as profiles.qml
|
||||
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
|
||||
// Convert to URL like profiles.qml does with FolderListModel
|
||||
var fileUrl = "file://" + details.filePath
|
||||
console.log("Converted to URL:", fileUrl)
|
||||
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
|
||||
|
||||
// Push the ChartJsTest view
|
||||
stackView.push("PreviewChart.qml")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation Dialog
|
||||
Dialog {
|
||||
id: confirmDialog
|
||||
|
||||
property int workoutId
|
||||
property string workoutTitle
|
||||
|
||||
title: "Delete Workout"
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
Text {
|
||||
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
workoutModel.deleteWorkout(confirmDialog.workoutId)
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
onRejected: {
|
||||
swipeDelegate.swipe.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Training Program Loading Dialog
|
||||
Dialog {
|
||||
id: trainingProgramDialog
|
||||
|
||||
property string message: ""
|
||||
property bool isSuccess: true
|
||||
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 8
|
||||
border.color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
border.width: 2
|
||||
}
|
||||
|
||||
header: Rectangle {
|
||||
height: 50
|
||||
color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
|
||||
radius: 8
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: trainingProgramDialog.title
|
||||
color: "white"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
Layout.margins: 20
|
||||
Layout.preferredWidth: 300
|
||||
Layout.preferredHeight: 120
|
||||
text: Qt.platform.os === "android" ?
|
||||
wrapEmoji("🔥") + " " +
|
||||
wrapEmoji(trainingProgramDialog.isSuccess ? '✅' : '❌') +
|
||||
" " + trainingProgramDialog.message :
|
||||
"🔥 " + (trainingProgramDialog.isSuccess ? '✅ ' : '❌ ') + trainingProgramDialog.message
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streak Banner at the bottom
|
||||
Rectangle {
|
||||
id: streakBanner
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 80
|
||||
visible: workoutModel
|
||||
|
||||
Behavior on visible {
|
||||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Special pulsing effect for major milestones
|
||||
SequentialAnimation on opacity {
|
||||
running: workoutModel && workoutModel.currentStreak >= 30
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0.9; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { from: 1.0; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFD700" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#9932CC" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#FF1493" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF4500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF6347" : "#FF6B35"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0;
|
||||
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFA500" :
|
||||
workoutModel && (workoutModel.currentStreak >= 180) ? "#8A2BE2" :
|
||||
workoutModel && (workoutModel.currentStreak >= 90) ? "#DC143C" :
|
||||
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF6B35" :
|
||||
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF4500" : "#F7931E"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40FFFFFF" }
|
||||
GradientStop { position: 1.0; color: "#00FFFFFF" }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
// Current streak with count
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 15
|
||||
|
||||
// Fire emoji with animation
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("👑🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🎖️🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🦁🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🎊🔥") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🏆🔥") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "👑🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🎖️🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🦁🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🎊🔥" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🏆🔥" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 7 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 365 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 600 : 800;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Special sparkle effect for year achievement
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: 360; duration: 3000; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
|
||||
// Current streak count
|
||||
Text {
|
||||
text: workoutModel ? workoutModel.currentStreak + " day" + (workoutModel.currentStreak !== 1 ? "s" : "") + " streak" : ""
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: "white"
|
||||
visible: workoutModel
|
||||
}
|
||||
|
||||
// Another fire emoji
|
||||
Text {
|
||||
text: Qt.platform.os === "android" ? (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("🔥👑") :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🔥🎖️") :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🔥🦁") :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🔥🎊") :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🔥🏆") : wrapEmoji("🔥")
|
||||
) : (
|
||||
workoutModel && workoutModel.currentStreak >= 365 ? "🔥👑" :
|
||||
workoutModel && workoutModel.currentStreak >= 180 ? "🔥🎖️" :
|
||||
workoutModel && workoutModel.currentStreak >= 90 ? "🔥🦁" :
|
||||
workoutModel && workoutModel.currentStreak >= 30 ? "🔥🎊" :
|
||||
workoutModel && workoutModel.currentStreak >= 7 ? "🔥🏆" : "🔥"
|
||||
)
|
||||
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
|
||||
font.pixelSize: workoutModel && workoutModel.currentStreak >= 365 ? 28 : 24
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: workoutModel && workoutModel.currentStreak > 0
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1.0;
|
||||
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
|
||||
to: 1.0;
|
||||
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
// Counter-rotation for variety
|
||||
SequentialAnimation on rotation {
|
||||
running: workoutModel && workoutModel.currentStreak >= 7
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 0; to: -360; duration: 3500; easing.type: Easing.Linear }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Motivational message
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? workoutModel.streakMessage : ""
|
||||
font.pixelSize: 14
|
||||
font.italic: true
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.streakMessage !== ""
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
// Best streak (smaller text)
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: workoutModel ? "Personal best: " + workoutModel.longestStreak + " day" + (workoutModel.longestStreak !== 1 ? "s" : "") : ""
|
||||
font.pixelSize: 12
|
||||
color: "white"
|
||||
visible: workoutModel && workoutModel.longestStreak > workoutModel.currentStreak && workoutModel.longestStreak > 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow effect at the top
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "#40000000" }
|
||||
GradientStop { position: 1.0; color: "#00000000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Popup
|
||||
Popup {
|
||||
id: calendarPopup
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.min(parent.width * 0.9, 400)
|
||||
height: Math.min(parent.height * 0.8, 500)
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: {
|
||||
// Refresh workout dates when calendar opens
|
||||
if (workoutModel) {
|
||||
calendar.workoutDates = workoutModel.getWorkoutDates()
|
||||
console.log("Calendar opened, refreshed workout dates:", JSON.stringify(calendar.workoutDates))
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "white"
|
||||
radius: 12
|
||||
border.color: "#d0d0d0"
|
||||
border.width: 1
|
||||
|
||||
// Shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 2
|
||||
anchors.leftMargin: 2
|
||||
radius: parent.radius
|
||||
color: "#40000000"
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Calendar Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
text: "<"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() - 1, 1)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: calendar.selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: ">"
|
||||
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar Grid
|
||||
GridLayout {
|
||||
id: calendar
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
columns: 7
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var workoutDates: workoutModel ? workoutModel.getWorkoutDates() : []
|
||||
|
||||
// Debug: print workout dates when they change
|
||||
onWorkoutDatesChanged: {
|
||||
console.log("Calendar workout dates updated:", JSON.stringify(workoutDates))
|
||||
}
|
||||
|
||||
// Day headers
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
text: modelData
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: "#666666"
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar days
|
||||
Repeater {
|
||||
model: getCalendarDays()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 40
|
||||
|
||||
property date dayDate: modelData.date
|
||||
property bool isCurrentMonth: modelData.currentMonth
|
||||
property bool hasWorkout: modelData.hasWorkout
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
|
||||
color: {
|
||||
if (mouseArea.pressed) return "#e3f2fd"
|
||||
if (isToday) return "#bbdefb"
|
||||
if (!isCurrentMonth) return "#f5f5f5"
|
||||
return "white"
|
||||
}
|
||||
|
||||
border.color: isToday ? "#2196f3" : "#e0e0e0"
|
||||
border.width: isToday ? 2 : 1
|
||||
radius: 4
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dayDate.getDate()
|
||||
color: isCurrentMonth ? "black" : "#cccccc"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
// Workout indicator dot
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: "#ff6b35"
|
||||
visible: hasWorkout
|
||||
border.width: 1
|
||||
border.color: "#cc5529"
|
||||
|
||||
// Debug: log when a dot should be visible
|
||||
Component.onCompleted: {
|
||||
if (hasWorkout) {
|
||||
console.log("Workout dot visible for date:", dayDate.toDateString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
if (isCurrentMonth) {
|
||||
var year = dayDate.getFullYear();
|
||||
var month = dayDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = dayDate.getDate();
|
||||
var dateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
workoutModel.setDateFilter(dateString);
|
||||
calendarPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Close"
|
||||
onClicked: calendarPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript functions for calendar
|
||||
function getCalendarDays() {
|
||||
var days = []
|
||||
var firstDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth(), 1)
|
||||
var lastDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 0)
|
||||
var startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay()) // Go back to start of week
|
||||
|
||||
var workoutDates = calendar.workoutDates || []
|
||||
console.log("getCalendarDays: workoutDates received:", JSON.stringify(workoutDates))
|
||||
|
||||
// workoutDates is now a QStringList (array of strings in format "yyyy-MM-dd")
|
||||
var workoutDateStrings = workoutDates || []
|
||||
console.log("Final workout date strings:", JSON.stringify(workoutDateStrings))
|
||||
|
||||
for (var i = 0; i < 42; i++) { // 6 rows x 7 days
|
||||
var currentDate = new Date(startDate)
|
||||
currentDate.setDate(startDate.getDate() + i)
|
||||
|
||||
// Costruisci la stringa YYYY-MM-DD dai componenti della data locale per evitare problemi di fuso orario
|
||||
var year = currentDate.getFullYear();
|
||||
var month = currentDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
|
||||
var day = currentDate.getDate();
|
||||
var localDateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
|
||||
|
||||
var hasWorkout = workoutDateStrings.indexOf(localDateString) !== -1;
|
||||
if (hasWorkout) {
|
||||
// Questo console.log ora utilizza la stringa della data locale corretta per la corrispondenza
|
||||
console.log("Found workout match for:", localDateString);
|
||||
}
|
||||
|
||||
var isCurrentMonth = currentDate.getMonth() === calendar.selectedDate.getMonth()
|
||||
|
||||
days.push({
|
||||
date: currentDate,
|
||||
currentMonth: isCurrentMonth,
|
||||
hasWorkout: hasWorkout
|
||||
})
|
||||
}
|
||||
|
||||
console.log("getCalendarDays: returning", days.length, "days")
|
||||
return days
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.23" android:versionCode="1264" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.18.18" android:versionCode="1014" android:installLocation="auto">
|
||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default permissions. -->
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.cagnulen.qdomyoszwift.CustomQtActivity" android:label="QZ" android:launchMode="singleTop">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="QZ" android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -112,15 +112,52 @@
|
||||
|
||||
<activity android:name="org.cagnulen.qdomyoszwift.MyActivity" />
|
||||
|
||||
<!-- =============================================
|
||||
Health Connect
|
||||
=============================================
|
||||
|
||||
For supported versions through Android 13, create an activity to show the rationale
|
||||
of Health Connect permissions once users click the privacy policy link. -->
|
||||
<activity
|
||||
android:name=".PermissionsRationaleActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- For versions starting Android 14, create an activity alias to show the rationale
|
||||
of Health Connect permissions once users click the privacy policy link. -->
|
||||
<activity-alias
|
||||
android:name="ViewPermissionUsageActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".PermissionsRationaleActivity"
|
||||
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
||||
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<!-- =============================================
|
||||
End Health Connect
|
||||
============================================= -->
|
||||
|
||||
|
||||
<receiver android:name=".MediaButtonReceiver" android:exported="true" android:permission="android.permission.MODIFY_AUDIO_SETTINGS">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.VOLUME_CHANGED_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="36" />
|
||||
<!-- Check if Health Connect is installed -->
|
||||
<queries>
|
||||
<package android:name="com.google.android.apps.healthdata" />
|
||||
</queries>
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
|
||||
@@ -140,4 +177,21 @@
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
|
||||
<uses-permission android:name="android.permission.health.READ_STEPS"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_STEPS"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_DISTANCE"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED"/>
|
||||
<uses-permission android:name="android.permission.health.READ_HEIGHT"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_POWER"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_SPEED"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED"/>
|
||||
<uses-permission android:name="android.permission.health.READ_WEIGHT"/>
|
||||
<uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>
|
||||
<uses-permission android:name="android.permission.health.READ_VO2_MAX"/>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
|
||||
def appcompat_version = "1.3.1"
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "com.android.billingclient:billing:8.0.0"
|
||||
implementation "com.android.billingclient:billing:6.0.1"
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
@@ -52,6 +52,11 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation files('libs/usb-serial-for-android-3.8.1.aar')
|
||||
androidTestImplementation "com.android.support:support-annotations:28.0.0"
|
||||
|
||||
implementation "androidx.health.connect:connect-client:1.1.0-alpha03"
|
||||
implementation "androidx.activity:activity:1.2.0"
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect'
|
||||
|
||||
implementation 'com.google.android.gms:play-services-wearable:+'
|
||||
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
@@ -128,8 +133,9 @@ android {
|
||||
defaultConfig {
|
||||
resConfig "en"
|
||||
compileSdkVersion 33
|
||||
minSdkVersion = 21
|
||||
targetSdkVersion = 36
|
||||
|
||||
minSdkVersion = 26
|
||||
targetSdkVersion = 34
|
||||
}
|
||||
|
||||
tasks.all { task ->
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
@@ -22,151 +23,110 @@ import android.widget.Button;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.content.Intent;
|
||||
|
||||
public class Ant {
|
||||
|
||||
private ChannelService.ChannelServiceComm mChannelService = null;
|
||||
private boolean mChannelServiceBound = false;
|
||||
private final String TAG = "Ant";
|
||||
public static Activity activity = null;
|
||||
static boolean speedRequest = false;
|
||||
static boolean heartRequest = false;
|
||||
static boolean bikeRequest = false; // Added bike request flag
|
||||
static boolean garminKey = false;
|
||||
static boolean treadmill = false;
|
||||
static boolean technoGymGroupCycle = false;
|
||||
static int antBikeDeviceNumber = 0;
|
||||
static int antHeartDeviceNumber = 0;
|
||||
|
||||
// Updated antStart method with BikeRequest parameter at the end
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
|
||||
QLog.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
bikeRequest = BikeRequest; // Set bike request flag
|
||||
technoGymGroupCycle = TechnoGymGroupCycle;
|
||||
antBikeDeviceNumber = AntBikeDeviceNumber;
|
||||
antHeartDeviceNumber = AntHeartDeviceNumber;
|
||||
activity = a;
|
||||
if(a != null)
|
||||
QLog.v(TAG, "antStart activity is valid");
|
||||
else
|
||||
{
|
||||
QLog.v(TAG, "antStart activity is invalid");
|
||||
return;
|
||||
}
|
||||
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) {
|
||||
Log.v(TAG, "antStart");
|
||||
speedRequest = SpeedRequest;
|
||||
heartRequest = HeartRequest;
|
||||
treadmill = Treadmill;
|
||||
garminKey = GarminKey;
|
||||
|
||||
activity = a;
|
||||
if(a != null)
|
||||
Log.v(TAG, "antStart activity is valid");
|
||||
else
|
||||
{
|
||||
Log.v(TAG, "antStart activity is invalid");
|
||||
return;
|
||||
}
|
||||
if(!mChannelServiceBound) doBindChannelService();
|
||||
}
|
||||
|
||||
private ServiceConnection mChannelServiceConnection = new ServiceConnection()
|
||||
{
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
|
||||
{
|
||||
QLog.v(TAG, "mChannelServiceConnection.onServiceConnected...");
|
||||
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
|
||||
QLog.v(TAG, "...mChannelServiceConnection.onServiceConnected");
|
||||
}
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
|
||||
{
|
||||
Log.v(TAG, "mChannelServiceConnection.onServiceConnected...");
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0)
|
||||
{
|
||||
QLog.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
|
||||
// Clearing and disabling when disconnecting from ChannelService
|
||||
mChannelService = null;
|
||||
QLog.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
|
||||
}
|
||||
};
|
||||
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
|
||||
|
||||
|
||||
Log.v(TAG, "...mChannelServiceConnection.onServiceConnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0)
|
||||
{
|
||||
Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
|
||||
|
||||
// Clearing and disabling when disconnecting from ChannelService
|
||||
mChannelService = null;
|
||||
|
||||
Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
|
||||
}
|
||||
};
|
||||
|
||||
private void doBindChannelService()
|
||||
{
|
||||
QLog.v(TAG, "doBindChannelService...");
|
||||
// Binds to ChannelService. ChannelService binds and manages connection between the
|
||||
// app and the ANT Radio Service
|
||||
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
|
||||
doUnbindChannelService();
|
||||
QLog.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
|
||||
QLog.v(TAG, "...doBindChannelService");
|
||||
}
|
||||
Log.v(TAG, "doBindChannelService...");
|
||||
|
||||
// Binds to ChannelService. ChannelService binds and manages connection between the
|
||||
// app and the ANT Radio Service
|
||||
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection , Context.BIND_AUTO_CREATE);
|
||||
|
||||
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
|
||||
doUnbindChannelService();
|
||||
|
||||
Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
|
||||
|
||||
Log.v(TAG, "...doBindChannelService");
|
||||
}
|
||||
|
||||
public void doUnbindChannelService()
|
||||
{
|
||||
QLog.v(TAG, "doUnbindChannelService...");
|
||||
if(mChannelServiceBound)
|
||||
{
|
||||
activity.unbindService(mChannelServiceConnection);
|
||||
mChannelServiceBound = false;
|
||||
}
|
||||
QLog.v(TAG, "...doUnbindChannelService");
|
||||
}
|
||||
Log.v(TAG, "doUnbindChannelService...");
|
||||
|
||||
if(mChannelServiceBound)
|
||||
{
|
||||
activity.unbindService(mChannelServiceConnection);
|
||||
|
||||
mChannelServiceBound = false;
|
||||
}
|
||||
|
||||
Log.v(TAG, "...doUnbindChannelService");
|
||||
}
|
||||
|
||||
public void setCadenceSpeedPower(float speed, int power, int cadence)
|
||||
{
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
QLog.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
|
||||
mChannelService.setSpeed(speed);
|
||||
mChannelService.setPower(power);
|
||||
mChannelService.setCadence(cadence);
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
|
||||
Log.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
|
||||
mChannelService.setSpeed(speed);
|
||||
mChannelService.setPower(power);
|
||||
mChannelService.setCadence(cadence);
|
||||
}
|
||||
|
||||
public int getHeart()
|
||||
{
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getHeart");
|
||||
return mChannelService.getHeart();
|
||||
}
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
|
||||
// Added bike-related getter methods
|
||||
public int getBikeCadence() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikeCadence");
|
||||
return mChannelService.getBikeCadence();
|
||||
}
|
||||
|
||||
public int getBikePower() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikePower");
|
||||
return mChannelService.getBikePower();
|
||||
}
|
||||
|
||||
public double getBikeSpeed() {
|
||||
if(mChannelService == null)
|
||||
return 0.0;
|
||||
QLog.v(TAG, "getBikeSpeed");
|
||||
return mChannelService.getBikeSpeed();
|
||||
}
|
||||
|
||||
public long getBikeDistance() {
|
||||
if(mChannelService == null)
|
||||
return 0;
|
||||
QLog.v(TAG, "getBikeDistance");
|
||||
return mChannelService.getBikeDistance();
|
||||
}
|
||||
|
||||
public boolean isBikeConnected() {
|
||||
if(mChannelService == null)
|
||||
return false;
|
||||
QLog.v(TAG, "isBikeConnected");
|
||||
return mChannelService.isBikeConnected();
|
||||
}
|
||||
|
||||
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
|
||||
double elapsedTimeSeconds, int resistance,
|
||||
double inclination) {
|
||||
if(mChannelService == null)
|
||||
return;
|
||||
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
|
||||
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
|
||||
elapsedTimeSeconds, resistance,
|
||||
inclination);
|
||||
Log.v(TAG, "getHeart");
|
||||
return mChannelService.getHeart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,562 +0,0 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.app.Activity;
|
||||
|
||||
// ANT+ Plugin imports
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IFitnessEquipmentStateReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IBikeDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnessEquipmentDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentType;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.HeartRateDataSource;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.IRawPowerOnlyDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.ICalculatedPowerReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedAccumulatedDistanceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc;
|
||||
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc.ICalculatedCadenceReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
|
||||
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IDeviceStateChangeReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IPluginAccessResultReceiver;
|
||||
import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
|
||||
|
||||
// Java imports
|
||||
import java.math.BigDecimal;
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class BikeChannelController {
|
||||
private static final String TAG = BikeChannelController.class.getSimpleName();
|
||||
|
||||
private Context context;
|
||||
private AntPlusFitnessEquipmentPcc fePcc = null;
|
||||
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
|
||||
private AntPlusBikePowerPcc powerPcc = null;
|
||||
private PccReleaseHandle<AntPlusBikePowerPcc> powerReleaseHandle = null;
|
||||
private AntPlusBikeSpeedDistancePcc speedCadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> speedCadenceReleaseHandle = null;
|
||||
private AntPlusBikeCadencePcc cadencePcc = null;
|
||||
private PccReleaseHandle<AntPlusBikeCadencePcc> cadenceReleaseHandle = null;
|
||||
private boolean isConnected = false;
|
||||
private boolean isPowerConnected = false;
|
||||
private boolean isSpeedCadenceConnected = false;
|
||||
|
||||
// Bike data fields - from fitness equipment
|
||||
public int cadence = 0; // Current cadence in RPM
|
||||
public int power = 0; // Current power in watts
|
||||
public BigDecimal speed = new BigDecimal(0); // Current speed in m/s
|
||||
public long distance = 0; // Total distance in meters
|
||||
public long calories = 0; // Total calories burned
|
||||
public EquipmentType equipmentType = EquipmentType.UNKNOWN;
|
||||
public EquipmentState equipmentState = EquipmentState.ASLEEP_OFF;
|
||||
public int heartRate = 0; // Heart rate from equipment
|
||||
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
|
||||
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
|
||||
|
||||
// Bike data fields - from dedicated sensors
|
||||
public int powerSensorPower = 0; // Power from dedicated power sensor
|
||||
public int speedSensorCadence = 0; // Cadence from speed/cadence sensor
|
||||
public BigDecimal speedSensorSpeed = new BigDecimal(0); // Speed from speed/cadence sensor
|
||||
public long speedSensorDistance = 0; // Distance from speed/cadence sensor
|
||||
|
||||
// Fitness equipment state receiver
|
||||
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
|
||||
new IFitnessEquipmentStateReceiver() {
|
||||
@Override
|
||||
public void onNewFitnessEquipmentState(long estTimestamp,
|
||||
EnumSet<EventFlag> eventFlags,
|
||||
EquipmentType type,
|
||||
EquipmentState state) {
|
||||
equipmentType = type;
|
||||
equipmentState = state;
|
||||
QLog.d(TAG, "Equipment type: " + type + ", State: " + state);
|
||||
|
||||
// Only subscribe to bike specific data if this is actually a bike
|
||||
if (type == EquipmentType.BIKE && !isSubscribedToBikeData) {
|
||||
subscribeToBikeSpecificData();
|
||||
isSubscribedToBikeData = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public BikeChannelController(boolean technoGymGroupCycle, int antBikeDeviceNumber) {
|
||||
this.context = Ant.activity;
|
||||
|
||||
if (technoGymGroupCycle) {
|
||||
// For Technogym Group Cycle: disable openChannel, enable openPowerSensorChannel
|
||||
openPowerSensorChannel(antBikeDeviceNumber);
|
||||
} else {
|
||||
// Standard behavior: enable openChannel, disable openPowerSensorChannel
|
||||
openChannel();
|
||||
}
|
||||
|
||||
//openSpeedCadenceSensorChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel() {
|
||||
// Request access to first available fitness equipment device
|
||||
// Using requestNewOpenAccess from the sample code
|
||||
releaseHandle = AntPlusFitnessEquipmentPcc.requestNewOpenAccess(
|
||||
(Activity)context,
|
||||
context,
|
||||
new IPluginAccessResultReceiver<AntPlusFitnessEquipmentPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusFitnessEquipmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
fePcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to fitness equipment: " + result.getDeviceName());
|
||||
subscribeToBikeEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isConnected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mFitnessEquipmentStateReceiver
|
||||
);
|
||||
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public boolean openPowerSensorChannel(int deviceNumber) {
|
||||
// Request access to power sensor device (deviceNumber = 0 means first available)
|
||||
powerReleaseHandle = AntPlusBikePowerPcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
new IPluginAccessResultReceiver<AntPlusBikePowerPcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikePowerPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
powerPcc = result;
|
||||
isPowerConnected = true;
|
||||
QLog.d(TAG, "Connected to power sensor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
subscribeToPowerSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Power Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Power Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Power Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Power Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Power Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Power Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized power sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Power Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isPowerConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean openSpeedCadenceSensorChannel() {
|
||||
// Request access to first available speed/cadence sensor device
|
||||
speedCadenceReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess((Activity)context, context,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
switch(resultCode) {
|
||||
case SUCCESS:
|
||||
speedCadencePcc = result;
|
||||
isSpeedCadenceConnected = true;
|
||||
QLog.d(TAG, "Connected to speed/cadence sensor: " + result.getDeviceName());
|
||||
subscribeToSpeedCadenceSensorEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available for Speed/Cadence Sensor");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters for Speed/Cadence Sensor");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "Speed/Cadence Sensor RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed for Speed/Cadence Sensor");
|
||||
break;
|
||||
case USER_CANCELLED:
|
||||
QLog.e(TAG, "User cancelled Speed/Cadence Sensor");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized speed/cadence sensor result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Speed/Cadence Sensor State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isSpeedCadenceConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
private void subscribeToBikeEvents() {
|
||||
if (fePcc != null) {
|
||||
// General fitness equipment data
|
||||
fePcc.subscribeGeneralFitnessEquipmentDataEvent(new IGeneralFitnessEquipmentDataReceiver() {
|
||||
@Override
|
||||
public void onNewGeneralFitnessEquipmentData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal elapsedTime, long cumulativeDistance,
|
||||
BigDecimal instantaneousSpeed, boolean virtualInstantaneousSpeed,
|
||||
int instantaneousHeartRate, HeartRateDataSource source) {
|
||||
|
||||
if (elapsedTime != null && elapsedTime.intValue() != -1) {
|
||||
BikeChannelController.this.elapsedTime = elapsedTime;
|
||||
}
|
||||
|
||||
if (cumulativeDistance != -1) {
|
||||
distance = cumulativeDistance;
|
||||
}
|
||||
|
||||
if (instantaneousSpeed != null && instantaneousSpeed.intValue() != -1) {
|
||||
speed = instantaneousSpeed;
|
||||
}
|
||||
|
||||
if (instantaneousHeartRate != -1) {
|
||||
heartRate = instantaneousHeartRate;
|
||||
heartRateSource = source;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "General Data - Time: " + elapsedTime + "s, Distance: " +
|
||||
distance + "m, Speed: " + speed + "m/s, HR: " + heartRate + "bpm");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSubscribedToBikeData = false;
|
||||
|
||||
private void subscribeToBikeSpecificData() {
|
||||
if (fePcc != null) {
|
||||
// Subscribe to bike specific data
|
||||
fePcc.getBikeMethods().subscribeBikeDataEvent(new IBikeDataReceiver() {
|
||||
@Override
|
||||
public void onNewBikeData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
int instantaneousCadence, int instantaneousPower) {
|
||||
|
||||
if (instantaneousCadence != -1) {
|
||||
cadence = instantaneousCadence;
|
||||
}
|
||||
|
||||
if (instantaneousPower != -1) {
|
||||
power = instantaneousPower;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "Bike Data - Cadence: " + cadence + "rpm, Power: " + power + "W");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToPowerSensorEvents() {
|
||||
if (powerPcc != null) {
|
||||
// Subscribe to raw power-only data events
|
||||
powerPcc.subscribeRawPowerOnlyDataEvent(new IRawPowerOnlyDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawPowerOnlyData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
long powerOnlyUpdateEventCount, int instantaneousPower,
|
||||
long accumulatedPower) {
|
||||
if (instantaneousPower != -1) {
|
||||
powerSensorPower = instantaneousPower;
|
||||
QLog.d(TAG, "Power Sensor Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also subscribe to calculated power events
|
||||
powerPcc.subscribeCalculatedPowerEvent(new ICalculatedPowerReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedPower(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
AntPlusBikePowerPcc.DataSource dataSource,
|
||||
BigDecimal calculatedPower) {
|
||||
if (calculatedPower != null && calculatedPower.intValue() != -1) {
|
||||
powerSensorPower = calculatedPower.intValue();
|
||||
QLog.d(TAG, "Power Sensor Calculated Data - Power: " + powerSensorPower + "W");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribeToSpeedCadenceSensorEvents() {
|
||||
if (speedCadencePcc != null) {
|
||||
// 2.095m circumference = average 700cx23mm road tire
|
||||
BigDecimal wheelCircumference = new BigDecimal("2.095");
|
||||
|
||||
// Subscribe to calculated speed events
|
||||
speedCadencePcc.subscribeCalculatedSpeedEvent(new CalculatedSpeedReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedSpeed) {
|
||||
if (calculatedSpeed != null && calculatedSpeed.doubleValue() > 0) {
|
||||
speedSensorSpeed = calculatedSpeed;
|
||||
QLog.d(TAG, "Speed Sensor Data - Speed: " + speedSensorSpeed + "m/s");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to calculated distance events
|
||||
speedCadencePcc.subscribeCalculatedAccumulatedDistanceEvent(new CalculatedAccumulatedDistanceReceiver(wheelCircumference) {
|
||||
@Override
|
||||
public void onNewCalculatedAccumulatedDistance(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedAccumulatedDistance) {
|
||||
if (calculatedAccumulatedDistance != null && calculatedAccumulatedDistance.longValue() > 0) {
|
||||
speedSensorDistance = calculatedAccumulatedDistance.longValue();
|
||||
QLog.d(TAG, "Speed Sensor Data - Distance: " + speedSensorDistance + "m");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to raw speed and distance data
|
||||
speedCadencePcc.subscribeRawSpeedAndDistanceDataEvent(new IRawSpeedAndDistanceDataReceiver() {
|
||||
@Override
|
||||
public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal timestampOfLastEvent, long cumulativeRevolutions) {
|
||||
QLog.d(TAG, "Speed/Distance Raw Data - Revs: " + cumulativeRevolutions + ", Time: " + timestampOfLastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this is a combined speed/cadence sensor
|
||||
if (speedCadencePcc.isSpeedAndCadenceCombinedSensor()) {
|
||||
// Connect to cadence functionality
|
||||
cadenceReleaseHandle = AntPlusBikeCadencePcc.requestAccess(
|
||||
(Activity)context, speedCadencePcc.getAntDeviceNumber(), 0, true,
|
||||
new IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
if (resultCode == RequestAccessResult.SUCCESS) {
|
||||
cadencePcc = result;
|
||||
cadencePcc.subscribeCalculatedCadenceEvent(new ICalculatedCadenceReceiver() {
|
||||
@Override
|
||||
public void onNewCalculatedCadence(long estTimestamp, EnumSet<EventFlag> eventFlags,
|
||||
BigDecimal calculatedCadence) {
|
||||
if (calculatedCadence != null && calculatedCadence.intValue() > 0) {
|
||||
speedSensorCadence = calculatedCadence.intValue();
|
||||
QLog.d(TAG, "Cadence Sensor Data - Cadence: " + speedSensorCadence + "rpm");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Cadence Sensor State Changed to: " + newDeviceState);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (releaseHandle != null) {
|
||||
releaseHandle.close();
|
||||
releaseHandle = null;
|
||||
}
|
||||
if (powerReleaseHandle != null) {
|
||||
powerReleaseHandle.close();
|
||||
powerReleaseHandle = null;
|
||||
}
|
||||
if (speedCadenceReleaseHandle != null) {
|
||||
speedCadenceReleaseHandle.close();
|
||||
speedCadenceReleaseHandle = null;
|
||||
}
|
||||
if (cadenceReleaseHandle != null) {
|
||||
cadenceReleaseHandle.close();
|
||||
cadenceReleaseHandle = null;
|
||||
}
|
||||
fePcc = null;
|
||||
powerPcc = null;
|
||||
speedCadencePcc = null;
|
||||
cadencePcc = null;
|
||||
isConnected = false;
|
||||
isPowerConnected = false;
|
||||
isSpeedCadenceConnected = false;
|
||||
QLog.d(TAG, "All Channels Closed");
|
||||
}
|
||||
|
||||
// Getter methods for bike data with sensor reconciliation
|
||||
public int getCadence() {
|
||||
// Priority: 1) Fitness Equipment, 2) Speed/Cadence Sensor, 3) Power Sensor
|
||||
if (isConnected && cadence > 0) {
|
||||
return cadence; // From fitness equipment
|
||||
} else if (isSpeedCadenceConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From dedicated speed/cadence sensor
|
||||
} else if (isPowerConnected && speedSensorCadence > 0) {
|
||||
return speedSensorCadence; // From power sensor (if it provides cadence)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
// Priority: 1) Dedicated Power Sensor, 2) Fitness Equipment
|
||||
if (isPowerConnected && powerSensorPower > 0) {
|
||||
return powerSensorPower; // From dedicated power sensor (most accurate)
|
||||
} else if (isConnected && power > 0) {
|
||||
return power; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getSpeedKph() {
|
||||
// Convert from m/s to km/h
|
||||
return getSpeedMps() * 3.6;
|
||||
}
|
||||
|
||||
public double getSpeedMps() {
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorSpeed.doubleValue() > 0) {
|
||||
return speedSensorSpeed.doubleValue(); // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && speed.doubleValue() > 0) {
|
||||
return speed.doubleValue(); // From fitness equipment
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public long getDistance() {
|
||||
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
|
||||
if (isSpeedCadenceConnected && speedSensorDistance > 0) {
|
||||
return speedSensorDistance; // From dedicated speed sensor (most accurate)
|
||||
} else if (isConnected && distance > 0) {
|
||||
return distance; // From fitness equipment
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long getCalories() {
|
||||
return calories;
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public BigDecimal getElapsedTime() {
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
public EquipmentState getEquipmentState() {
|
||||
return equipmentState;
|
||||
}
|
||||
|
||||
public EquipmentType getEquipmentType() {
|
||||
return equipmentType;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
// Additional connection status methods
|
||||
public boolean isPowerSensorConnected() {
|
||||
return isPowerConnected;
|
||||
}
|
||||
|
||||
public boolean isSpeedCadenceSensorConnected() {
|
||||
return isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
public boolean isAnyDeviceConnected() {
|
||||
return isConnected || isPowerConnected || isSpeedCadenceConnected;
|
||||
}
|
||||
|
||||
// Raw sensor data getters (for debugging/advanced use)
|
||||
public int getRawFitnessEquipmentPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public int getRawPowerSensorPower() {
|
||||
return powerSensorPower;
|
||||
}
|
||||
|
||||
public int getRawFitnessEquipmentCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public int getRawSpeedSensorCadence() {
|
||||
return speedSensorCadence;
|
||||
}
|
||||
|
||||
public double getRawFitnessEquipmentSpeed() {
|
||||
return speed.doubleValue();
|
||||
}
|
||||
|
||||
public double getRawSpeedSensorSpeed() {
|
||||
return speedSensorSpeed.doubleValue();
|
||||
}
|
||||
|
||||
public long getRawFitnessEquipmentDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public long getRawSpeedSensorDistance() {
|
||||
return speedSensorDistance;
|
||||
}
|
||||
}
|
||||
@@ -1,651 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Dynastream Innovations Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
import com.dsi.ant.channel.IAntChannelEventHandler;
|
||||
import com.dsi.ant.message.ChannelId;
|
||||
import com.dsi.ant.message.ChannelType;
|
||||
import com.dsi.ant.message.EventCode;
|
||||
import com.dsi.ant.message.fromant.AcknowledgedDataMessage;
|
||||
import com.dsi.ant.message.fromant.ChannelEventMessage;
|
||||
import com.dsi.ant.message.fromant.MessageFromAntType;
|
||||
import com.dsi.ant.message.ipc.AntMessageParcel;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* ANT+ Bike Transmitter Controller
|
||||
* Follows exactly the same pattern as PowerChannelController but for Fitness Equipment
|
||||
*/
|
||||
public class BikeTransmitterController {
|
||||
public static final int FITNESS_EQUIPMENT_SENSOR_ID = 0x9e3d4b67; // Different from power sensor
|
||||
// The device type and transmission type to be part of the channel ID message
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE = 17; // Fitness Equipment
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE = 5;
|
||||
// The period and frequency values the channel will be configured to
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_PERIOD = 8192; // 4 Hz for FE
|
||||
private static final int CHANNEL_FITNESS_EQUIPMENT_FREQUENCY = 57;
|
||||
private static final String TAG = BikeTransmitterController.class.getSimpleName();
|
||||
|
||||
// ANT+ Data Page IDs for Fitness Equipment
|
||||
private static final byte DATA_PAGE_GENERAL_FE = 0x10;
|
||||
private static final byte DATA_PAGE_BIKE_DATA = 0x19;
|
||||
private static final byte DATA_PAGE_TRAINER_DATA = 0x1A;
|
||||
private static final byte DATA_PAGE_GENERAL_SETTINGS = 0x11;
|
||||
|
||||
private static Random randGen = new Random();
|
||||
|
||||
// Current bike metrics to transmit
|
||||
int currentCadence = 0; // Current cadence in RPM
|
||||
int currentPower = 0; // Current power in watts
|
||||
double currentSpeedKph = 0.0; // Current speed in km/h
|
||||
long totalDistance = 0; // Total distance in meters
|
||||
int currentHeartRate = 0; // Heart rate in BPM
|
||||
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
|
||||
int currentResistance = 0; // Current resistance level (0-100)
|
||||
double currentInclination = 0.0; // Current inclination in percentage
|
||||
|
||||
// Control commands received from ANT+ devices
|
||||
private int requestedResistance = -1; // Requested resistance from controller
|
||||
private int requestedPower = -1; // Requested power from controller
|
||||
private double requestedInclination = -100; // Requested inclination from controller
|
||||
|
||||
private AntChannel mAntChannel;
|
||||
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
|
||||
private boolean mIsOpen;
|
||||
|
||||
// Callbacks for control commands
|
||||
public interface ControlCommandListener {
|
||||
void onResistanceChangeRequested(int resistance);
|
||||
void onPowerChangeRequested(int power);
|
||||
void onInclinationChangeRequested(double inclination);
|
||||
}
|
||||
|
||||
private ControlCommandListener controlListener = null;
|
||||
|
||||
public BikeTransmitterController(AntChannel antChannel) {
|
||||
mAntChannel = antChannel;
|
||||
openChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener for control commands received from ANT+ devices
|
||||
*/
|
||||
public void setControlCommandListener(ControlCommandListener listener) {
|
||||
this.controlListener = listener;
|
||||
}
|
||||
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type
|
||||
ChannelId channelId = new ChannelId(FITNESS_EQUIPMENT_SENSOR_ID & 0xFFFF,
|
||||
CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE, CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE);
|
||||
|
||||
try {
|
||||
// Setting the channel event handler so that we can receive messages from ANT
|
||||
mAntChannel.setChannelEventHandler(mChannelEventCallback);
|
||||
|
||||
// Performs channel assignment by assigning the type to the channel
|
||||
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
|
||||
|
||||
// Configures the channel ID, messaging period and rf frequency after assigning,
|
||||
// then opening the channel.
|
||||
mAntChannel.setChannelId(channelId);
|
||||
mAntChannel.setPeriod(CHANNEL_FITNESS_EQUIPMENT_PERIOD);
|
||||
mAntChannel.setRfFrequency(CHANNEL_FITNESS_EQUIPMENT_FREQUENCY);
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
QLog.d(TAG, "Opened fitness equipment channel with device number: " + FITNESS_EQUIPMENT_SENSOR_ID);
|
||||
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
// This will release, and therefore unassign if required
|
||||
channelError("Open failed", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
}
|
||||
|
||||
public boolean startTransmission() {
|
||||
return openChannel();
|
||||
}
|
||||
|
||||
public void stopTransmission() {
|
||||
close();
|
||||
}
|
||||
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
QLog.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
StringBuilder logString;
|
||||
|
||||
if (e.getResponseMessage() != null) {
|
||||
String initiatingMessageId = "0x" + Integer.toHexString(
|
||||
e.getResponseMessage().getInitiatingMessageId());
|
||||
String rawResponseCode = "0x" + Integer.toHexString(
|
||||
e.getResponseMessage().getRawResponseCode());
|
||||
|
||||
logString = new StringBuilder(error)
|
||||
.append(". Command ")
|
||||
.append(initiatingMessageId)
|
||||
.append(" failed with code ")
|
||||
.append(rawResponseCode);
|
||||
} else {
|
||||
String attemptedMessageId = "0x" + Integer.toHexString(
|
||||
e.getAttemptedMessageType().getMessageId());
|
||||
String failureReason = e.getFailureReason().toString();
|
||||
|
||||
logString = new StringBuilder(error)
|
||||
.append(". Command ")
|
||||
.append(attemptedMessageId)
|
||||
.append(" failed with reason ")
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
QLog.e(TAG, logString.toString());
|
||||
mAntChannel.release();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (null != mAntChannel) {
|
||||
mIsOpen = false;
|
||||
// Releasing the channel to make it available for others.
|
||||
// After releasing, the AntChannel instance cannot be reused.
|
||||
mAntChannel.release();
|
||||
mAntChannel = null;
|
||||
}
|
||||
QLog.e(TAG, "Fitness Equipment Channel Closed");
|
||||
}
|
||||
|
||||
// Setter methods for updating bike metrics from the main application
|
||||
public void setCadence(int cadence) {
|
||||
this.currentCadence = Math.max(0, cadence);
|
||||
}
|
||||
|
||||
public void setPower(int power) {
|
||||
this.currentPower = Math.max(0, power);
|
||||
}
|
||||
|
||||
public void setSpeedKph(double speedKph) {
|
||||
this.currentSpeedKph = Math.max(0, speedKph);
|
||||
}
|
||||
|
||||
public void setDistance(long distance) {
|
||||
this.totalDistance = Math.max(0, distance);
|
||||
}
|
||||
|
||||
public void setHeartRate(int heartRate) {
|
||||
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
|
||||
}
|
||||
|
||||
public void setElapsedTime(double timeSeconds) {
|
||||
this.elapsedTimeSeconds = Math.max(0, timeSeconds);
|
||||
}
|
||||
|
||||
public void setResistance(int resistance) {
|
||||
this.currentResistance = Math.max(0, Math.min(100, resistance));
|
||||
}
|
||||
|
||||
public void setInclination(double inclination) {
|
||||
this.currentInclination = Math.max(-100, Math.min(100, inclination));
|
||||
}
|
||||
|
||||
// Getter methods for the last requested control values
|
||||
public int getRequestedResistance() {
|
||||
return requestedResistance;
|
||||
}
|
||||
|
||||
public int getRequestedPower() {
|
||||
return requestedPower;
|
||||
}
|
||||
|
||||
public double getRequestedInclination() {
|
||||
return requestedInclination;
|
||||
}
|
||||
|
||||
public void clearControlRequests() {
|
||||
requestedResistance = -1;
|
||||
requestedPower = -1;
|
||||
requestedInclination = -100;
|
||||
}
|
||||
|
||||
public boolean isTransmitting() {
|
||||
return mIsOpen;
|
||||
}
|
||||
|
||||
public String getTransmissionInfo() {
|
||||
if (!mIsOpen) {
|
||||
return "Transmission: STOPPED";
|
||||
}
|
||||
|
||||
return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
|
||||
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
|
||||
currentCadence, currentPower, currentSpeedKph,
|
||||
currentResistance, currentInclination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert byte array to hex string for debugging
|
||||
*/
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
return hex.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Channel Event Handler Interface following PowerChannelController pattern
|
||||
*/
|
||||
public class ChannelEventCallback implements IAntChannelEventHandler {
|
||||
|
||||
int cnt = 0;
|
||||
int eventCount = 0;
|
||||
int eventPowerCount = 0;
|
||||
int cumulativeDistance = 0;
|
||||
int cumulativeWatt = 0;
|
||||
int accumulatedTorque32 = 0;
|
||||
Timer carousalTimer = null;
|
||||
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
QLog.e(TAG, "Fitness Equipment Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
// Start unsolicited transmission timer like PowerChannelController
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d(TAG, "Tx Unsolicited Fitness Equipment Data");
|
||||
byte[] payload = new byte[8];
|
||||
String debugString = "";
|
||||
eventCount = (eventCount + 1) & 0xFF;
|
||||
cumulativeDistance = (cumulativeDistance + (int)(currentSpeedKph / 3.6)) & 0xFFFF; // rough distance calc
|
||||
|
||||
cnt += 1;
|
||||
|
||||
// Cycle through different data pages like PowerChannelController
|
||||
if (cnt % 5 == 0) {
|
||||
// General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
} else if (cnt % 5 == 1) {
|
||||
// Bike Data Page (0x19)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 5 == 2) {
|
||||
// Trainer Data Page (0x1A)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 5 == 3) {
|
||||
// General Settings Page (0x11)
|
||||
debugString = buildGeneralSettingsPage(payload);
|
||||
} else {
|
||||
// Default General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
}
|
||||
|
||||
// Log the hex data and parsed values
|
||||
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
|
||||
QLog.d(TAG, debugString);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
// Setting the data to be broadcast on the next channel period
|
||||
mAntChannel.setBroadcastData(payload);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, 250); // Every 250ms for 4Hz
|
||||
}
|
||||
|
||||
// Switching on message type to handle different types of messages
|
||||
switch (messageType) {
|
||||
case BROADCAST_DATA:
|
||||
// Rx Data
|
||||
break;
|
||||
case ACKNOWLEDGED_DATA:
|
||||
// Handle control commands
|
||||
payload = new AcknowledgedDataMessage(antParcel).getPayload();
|
||||
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
handleControlCommand(payload);
|
||||
break;
|
||||
case CHANNEL_EVENT:
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
case TX:
|
||||
cnt += 1;
|
||||
String debugString = "";
|
||||
|
||||
// Cycle through different data pages like PowerChannelController
|
||||
if (cnt % 16 == 1) {
|
||||
// General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
} else if (cnt % 16 == 5) {
|
||||
// Bike Data Page (0x19)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 16 == 9) {
|
||||
// Trainer Data Page (0x1A)
|
||||
debugString = buildBikeDataPage(payload);
|
||||
} else if (cnt % 16 == 13) {
|
||||
// General Settings Page (0x11)
|
||||
debugString = buildGeneralSettingsPage(payload);
|
||||
} else {
|
||||
// Default General FE Data Page (0x10)
|
||||
debugString = buildGeneralFEDataPage(payload);
|
||||
}
|
||||
|
||||
// Log the hex data and parsed values
|
||||
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
|
||||
QLog.d(TAG, debugString);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
// Setting the data to be broadcast on the next channel period
|
||||
mAntChannel.setBroadcastData(payload);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COLLISION:
|
||||
cnt += 1;
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
QLog.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
case RX_FAIL_GO_TO_SEARCH:
|
||||
case TRANSFER_RX_FAILED:
|
||||
case TRANSFER_TX_COMPLETED:
|
||||
case TRANSFER_TX_FAILED:
|
||||
case TRANSFER_TX_START:
|
||||
case UNKNOWN:
|
||||
// TODO More complex communication will need to handle these events
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ANT_VERSION:
|
||||
case BURST_TRANSFER_DATA:
|
||||
case CAPABILITIES:
|
||||
case CHANNEL_ID:
|
||||
case CHANNEL_RESPONSE:
|
||||
case CHANNEL_STATUS:
|
||||
case SERIAL_NUMBER:
|
||||
case OTHER:
|
||||
// TODO More complex communication will need to handle these message types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build General Fitness Equipment Data Page (0x10) - Page 16
|
||||
* Following Table 8-7 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildGeneralFEDataPage(byte[] payload) {
|
||||
payload[0] = 0x10; // Data Page Number = 0x10 (Page 16)
|
||||
|
||||
// Byte 1: Equipment Type Bit Field (Refer to Table 8-8)
|
||||
payload[1] = 0x19; // Equipment type: Bike (stationary bike = 0x19)
|
||||
|
||||
// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
|
||||
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
|
||||
payload[2] = (byte) elapsedTime025s;
|
||||
|
||||
// Byte 3: Distance Traveled (1 meter resolution, rollover at 256m)
|
||||
int distanceMeters = (int) (totalDistance) & 0xFF;
|
||||
payload[3] = (byte) distanceMeters;
|
||||
|
||||
// Bytes 4-5: Speed (0.001 m/s resolution, 0xFFFF = invalid)
|
||||
int speedMms = (int) (currentSpeedKph / 3.6 * 1000);
|
||||
if (speedMms > 65534) speedMms = 65534; // Max valid value
|
||||
payload[4] = (byte) (speedMms & 0xFF); // Speed LSB
|
||||
payload[5] = (byte) ((speedMms >> 8) & 0xFF); // Speed MSB
|
||||
|
||||
// Byte 6: Heart Rate (0xFF = invalid)
|
||||
payload[6] = (byte) (currentHeartRate == 0 ? 0xFF : currentHeartRate);
|
||||
|
||||
// Byte 7: Capabilities Bit Field (4 bits 0:3) + FE State Bit Field (4 bits 4:7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now (refer to Tables 8-9 and 8-10)
|
||||
|
||||
// Create debug string
|
||||
return String.format(Locale.US,
|
||||
"General FE Data Page (0x10): " +
|
||||
"Page=0x%02X, Equipment=0x%02X(Bike), " +
|
||||
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
|
||||
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
|
||||
"Capabilities=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF,
|
||||
payload[2] & 0xFF, elapsedTimeSeconds,
|
||||
payload[3] & 0xFF, distanceMeters,
|
||||
payload[5] & 0xFF, payload[4] & 0xFF, currentSpeedKph,
|
||||
payload[6] & 0xFF, currentHeartRate == 0 ? "Invalid" : currentHeartRate + "bpm",
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Specific Trainer/Stationary Bike Data Page (0x19) - Page 25
|
||||
* Following Table 8-25 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildBikeDataPage(byte[] payload) {
|
||||
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)
|
||||
|
||||
// Byte 1: Update Event Count (increments with each information update)
|
||||
eventPowerCount = (eventPowerCount + 1) & 0xFF;
|
||||
payload[1] = (byte) eventPowerCount;
|
||||
|
||||
// Byte 2: Instantaneous Cadence (RPM, 0xFF = invalid)
|
||||
payload[2] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);
|
||||
|
||||
// Bytes 3-4: Accumulated Power (1 watt resolution, rollover at 65536W)
|
||||
// This is cumulative power, not instantaneous
|
||||
cumulativeWatt = (cumulativeWatt + currentPower);
|
||||
payload[3] = (byte) (cumulativeWatt & 0xFF); // Accumulated Power LSB
|
||||
payload[4] = (byte) ((cumulativeWatt >> 8) & 0xFF); // Accumulated Power MSB
|
||||
|
||||
// Bytes 5-6: Instantaneous Power (1.5 bytes, 0xFFF = invalid for both fields)
|
||||
if (currentPower > 4094) {
|
||||
// 0xFFF indicates BOTH instantaneous and accumulated power fields are invalid
|
||||
payload[5] = (byte) 0xFF; // Instantaneous Power LSB
|
||||
payload[6] = (byte) 0xFF; // Instantaneous Power MSB (bits 0-3) + Trainer Status (bits 4-7)
|
||||
} else {
|
||||
payload[5] = (byte) (currentPower & 0xFF); // Instantaneous Power LSB
|
||||
payload[6] = (byte) ((currentPower >> 8) & 0x0F); // Instantaneous Power MSN (bits 0-3)
|
||||
// Bits 4-7 of byte 6: Trainer Status Bit Field (refer to Table 8-27)
|
||||
payload[6] |= 0x00; // Trainer status = 0 for now
|
||||
}
|
||||
|
||||
// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now
|
||||
|
||||
// Create debug string
|
||||
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
|
||||
String powerStr = currentPower > 4094 ? "Invalid" : currentPower + "W";
|
||||
|
||||
return String.format(Locale.US,
|
||||
"Bike Data Page (0x19): " +
|
||||
"Page=0x%02X, EventCount=0x%02X(%d), " +
|
||||
"Cadence=0x%02X(%s), AccumPower=0x%02X%02X(%dW), " +
|
||||
"InstPower=0x%X%02X(%s), Flags=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF, eventCount,
|
||||
payload[2] & 0xFF, cadenceStr,
|
||||
payload[4] & 0xFF, payload[3] & 0xFF, cumulativeWatt,
|
||||
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build General Settings Page (0x11) - Page 17
|
||||
* Following Table 8-11 format exactly
|
||||
* @param payload byte array to populate
|
||||
* @return debug string with hex and parsed values
|
||||
*/
|
||||
private String buildGeneralSettingsPage(byte[] payload) {
|
||||
payload[0] = 0x11; // Data Page Number = 0x11 (Page 17)
|
||||
|
||||
// Byte 1: Reserved (0xFF - Do not interpret)
|
||||
payload[1] = (byte) 0xFF;
|
||||
|
||||
// Byte 2: Reserved (0xFF - Do not interpret)
|
||||
payload[2] = (byte) 0xFF;
|
||||
|
||||
// Byte 3: Cycle length (0.01 meters resolution, 0xFF = invalid)
|
||||
// Length of one 'cycle' - for bike this could be wheel circumference
|
||||
int cycleLengthCm = 210; // 2.1m wheel circumference = 210cm
|
||||
payload[3] = (byte) (cycleLengthCm & 0xFF);
|
||||
|
||||
// Bytes 4-5: Incline Percentage (signed integer, 0.01% resolution, 0x7FFF = invalid)
|
||||
int inclinePercent001 = (int) (currentInclination * 100); // Convert to 0.01% units
|
||||
if (inclinePercent001 < -10000) inclinePercent001 = -10000; // Min -100.00%
|
||||
if (inclinePercent001 > 10000) inclinePercent001 = 10000; // Max +100.00%
|
||||
payload[4] = (byte) (inclinePercent001 & 0xFF); // Incline LSB
|
||||
payload[5] = (byte) ((inclinePercent001 >> 8) & 0xFF); // Incline MSB
|
||||
|
||||
// Byte 6: Resistance Level (0.5% resolution, percentage of maximum applicable resistance)
|
||||
int resistanceLevel05 = (int) (currentResistance * 2); // Convert to 0.5% units
|
||||
if (resistanceLevel05 > 200) resistanceLevel05 = 200; // Max 100% = 200 in 0.5% units
|
||||
payload[6] = (byte) (resistanceLevel05 & 0xFF);
|
||||
|
||||
// Byte 7: Capabilities Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
|
||||
payload[7] = 0x00; // Set to 0x00 for now
|
||||
|
||||
// Create debug string
|
||||
return String.format(Locale.US,
|
||||
"General Settings Page (0x11): " +
|
||||
"Page=0x%02X, Reserved1=0x%02X, Reserved2=0x%02X, " +
|
||||
"CycleLength=0x%02X(%.2fm), Incline=0x%02X%02X(%.2f%%), " +
|
||||
"Resistance=0x%02X(%d%%), Capabilities=0x%02X",
|
||||
payload[0] & 0xFF, payload[1] & 0xFF, payload[2] & 0xFF,
|
||||
payload[3] & 0xFF, cycleLengthCm / 100.0,
|
||||
payload[5] & 0xFF, payload[4] & 0xFF, currentInclination,
|
||||
payload[6] & 0xFF, currentResistance,
|
||||
payload[7] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming control commands
|
||||
*/
|
||||
private void handleControlCommand(byte[] data) {
|
||||
if (data.length < 8) return;
|
||||
|
||||
byte pageNumber = data[0];
|
||||
QLog.d(TAG, "Received control command page: 0x" + String.format("%02X", pageNumber));
|
||||
QLog.d(TAG, "Control Command HEX: " + bytesToHex(data));
|
||||
|
||||
// Handle control command pages
|
||||
switch (pageNumber) {
|
||||
case 0x30: // Basic Resistance
|
||||
handleBasicResistanceCommand(data);
|
||||
break;
|
||||
case 0x31: // Target Power
|
||||
handleTargetPowerCommand(data);
|
||||
break;
|
||||
case 0x33: // Track Resistance
|
||||
handleTrackResistanceCommand(data);
|
||||
break;
|
||||
default:
|
||||
QLog.d(TAG, "Unknown control page: 0x" + String.format("%02X", pageNumber));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBasicResistanceCommand(byte[] data) {
|
||||
int resistance = data[7] & 0xFF; // Resistance in 0.5% increments
|
||||
double resistancePercent = resistance * 0.5;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Basic Resistance Command (0x30): Resistance=0x%02X(%.1f%%)",
|
||||
resistance, resistancePercent));
|
||||
|
||||
if (resistancePercent != requestedResistance && controlListener != null) {
|
||||
requestedResistance = (int) resistancePercent;
|
||||
controlListener.onResistanceChangeRequested(requestedResistance);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTargetPowerCommand(byte[] data) {
|
||||
int targetPower = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
|
||||
targetPower = targetPower / 4;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Target Power Command (0x31): Power=0x%02X%02X(%dW)",
|
||||
data[7] & 0xFF, data[6] & 0xFF, targetPower));
|
||||
|
||||
if (targetPower != requestedPower && controlListener != null) {
|
||||
requestedPower = targetPower;
|
||||
controlListener.onPowerChangeRequested(targetPower);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTrackResistanceCommand(byte[] data) {
|
||||
// Grade is in 0.01% increments, signed 16-bit
|
||||
int gradeRaw = ((data[6] & 0xFF) << 8) | (data[5] & 0xFF);
|
||||
if (gradeRaw > 32767) gradeRaw -= 65536; // Convert to signed
|
||||
double grade = (gradeRaw - 0x4E20) * 0.01;
|
||||
|
||||
QLog.d(TAG, String.format(Locale.US,
|
||||
"Track Resistance Command (0x33): Grade=0x%02X%02X(%.2f%%)",
|
||||
data[6] & 0xFF, data[5] & 0xFF, grade));
|
||||
|
||||
if (Math.abs(grade - requestedInclination) > 0.1 && controlListener != null) {
|
||||
requestedInclination = grade;
|
||||
controlListener.onInclinationChangeRequested(grade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IntentFilter;
|
||||
@@ -89,12 +89,12 @@ public class BleAdvertiser {
|
||||
private static AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
|
||||
@Override
|
||||
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
|
||||
QLog.d("BleAdvertiser", "Advertising started successfully");
|
||||
Log.d("BleAdvertiser", "Advertising started successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartFailure(int errorCode) {
|
||||
QLog.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
|
||||
Log.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class CSafeRowerUSBHID {
|
||||
|
||||
@@ -34,21 +34,21 @@ public class CSafeRowerUSBHID {
|
||||
static int lastReadLen = 0;
|
||||
|
||||
public static void open(Context context) {
|
||||
QLog.d("QZ","CSafeRowerUSBHID open");
|
||||
Log.d("QZ","CSafeRowerUSBHID open");
|
||||
hidBridge = new HidBridge(context, 0x0002, 0x17A4);
|
||||
boolean ret = hidBridge.OpenDevice();
|
||||
QLog.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
Log.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
if(ret == false) {
|
||||
hidBridge = new HidBridge(context, 0x0001, 0x17A4);
|
||||
ret = hidBridge.OpenDevice();
|
||||
QLog.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
Log.d("QZ","hidBridge.OpenDevice " + ret);
|
||||
}
|
||||
hidBridge.StartReadingThread();
|
||||
QLog.d("QZ","hidBridge.StartReadingThread");
|
||||
Log.d("QZ","hidBridge.StartReadingThread");
|
||||
}
|
||||
|
||||
public static void write (byte[] bytes) {
|
||||
QLog.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
|
||||
Log.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
|
||||
hidBridge.WriteData(bytes);
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ public class CSafeRowerUSBHID {
|
||||
if(hidBridge.IsThereAnyReceivedData()) {
|
||||
receiveData = hidBridge.GetReceivedDataFromQueue();
|
||||
lastReadLen = receiveData.length;
|
||||
QLog.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
|
||||
Log.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
|
||||
return receiveData;
|
||||
} else {
|
||||
QLog.d("QZ","CSafeRowerUSBHID empty data");
|
||||
Log.d("QZ","CSafeRowerUSBHID empty data");
|
||||
lastReadLen = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -49,21 +49,15 @@ public class ChannelService extends Service {
|
||||
private AntChannelProvider mAntChannelProvider = null;
|
||||
private boolean mAllowAddChannel = false;
|
||||
|
||||
public static native void nativeSetResistance(int resistance);
|
||||
public static native void nativeSetPower(int power);
|
||||
public static native void nativeSetInclination(double inclination);
|
||||
|
||||
HeartChannelController heartChannelController = null;
|
||||
PowerChannelController powerChannelController = null;
|
||||
SpeedChannelController speedChannelController = null;
|
||||
SDMChannelController sdmChannelController = null;
|
||||
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
|
||||
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
|
||||
|
||||
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
QLog.v(TAG, "onServiceConnected");
|
||||
Log.v(TAG, "onServiceConnected");
|
||||
// Must pass in the received IBinder object to correctly construct an AntService object
|
||||
mAntRadioService = new AntService(service);
|
||||
|
||||
@@ -78,7 +72,7 @@ public class ChannelService extends Service {
|
||||
// radio by attempting to acquire a channel.
|
||||
boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse();
|
||||
|
||||
QLog.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
|
||||
Log.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
|
||||
|
||||
// If there are channels OR legacy interface in use, allow adding channels
|
||||
if (mChannelAvailable || legacyInterfaceInUse) {
|
||||
@@ -91,7 +85,7 @@ public class ChannelService extends Service {
|
||||
try {
|
||||
openAllChannels();
|
||||
} catch (ChannelNotAvailableException exception) {
|
||||
QLog.e(TAG, "Channel not available!!");
|
||||
Log.e(TAG, "Channel not available!!");
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// TODO Auto-generated catch block
|
||||
@@ -123,20 +117,12 @@ public class ChannelService extends Service {
|
||||
if (null != sdmChannelController) {
|
||||
sdmChannelController.speed = speed;
|
||||
}
|
||||
// Update bike transmitter with speed data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setSpeedKph(speed);
|
||||
}
|
||||
}
|
||||
|
||||
void setPower(int power) {
|
||||
if (null != powerChannelController) {
|
||||
powerChannelController.power = power;
|
||||
}
|
||||
// Update bike transmitter with power data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setPower(power);
|
||||
}
|
||||
}
|
||||
|
||||
void setCadence(int cadence) {
|
||||
@@ -149,164 +135,16 @@ public class ChannelService extends Service {
|
||||
if (null != sdmChannelController) {
|
||||
sdmChannelController.cadence = cadence;
|
||||
}
|
||||
// Update bike transmitter with cadence data (only if not treadmill)
|
||||
if (!Ant.treadmill && null != bikeTransmitterController) {
|
||||
bikeTransmitterController.setCadence(cadence);
|
||||
}
|
||||
}
|
||||
|
||||
int getHeart() {
|
||||
if (null != heartChannelController) {
|
||||
QLog.v(TAG, "getHeart");
|
||||
Log.v(TAG, "getHeart");
|
||||
return heartChannelController.heart;
|
||||
}
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getHeartRate();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Added getters for bike channel data
|
||||
int getBikeCadence() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getCadence();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getBikePower() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getPower();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double getBikeSpeed() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getSpeedKph();
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
long getBikeDistance() {
|
||||
if (null != bikeChannelController) {
|
||||
return bikeChannelController.getDistance();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
boolean isBikeConnected() {
|
||||
return (bikeChannelController != null && bikeChannelController.isConnected());
|
||||
}
|
||||
|
||||
// ========== BIKE TRANSMITTER METHODS ==========
|
||||
|
||||
/**
|
||||
* Start the bike transmitter (only available if not treadmill)
|
||||
*/
|
||||
boolean startBikeTransmitter() {
|
||||
QLog.v(TAG, "ChannelServiceComm.startBikeTransmitter");
|
||||
|
||||
if (Ant.treadmill) {
|
||||
QLog.w(TAG, "Bike transmitter not available in treadmill mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.startTransmission();
|
||||
}
|
||||
QLog.w(TAG, "Bike transmitter controller is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bike transmitter
|
||||
*/
|
||||
void stopBikeTransmitter() {
|
||||
QLog.v(TAG, "ChannelServiceComm.stopBikeTransmitter");
|
||||
|
||||
if (bikeTransmitterController != null) {
|
||||
bikeTransmitterController.stopTransmission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bike transmitter is active (only if not treadmill)
|
||||
*/
|
||||
boolean isBikeTransmitterActive() {
|
||||
if (Ant.treadmill) {
|
||||
return false;
|
||||
}
|
||||
return (bikeTransmitterController != null && bikeTransmitterController.isTransmitting());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bike transmitter with extended metrics (only if not treadmill)
|
||||
*/
|
||||
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
|
||||
double elapsedTimeSeconds, int resistance,
|
||||
double inclination) {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
bikeTransmitterController.setDistance(distanceMeters);
|
||||
bikeTransmitterController.setHeartRate(heartRate);
|
||||
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
|
||||
bikeTransmitterController.setResistance(resistance);
|
||||
bikeTransmitterController.setInclination(inclination);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested resistance from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
int getRequestedResistanceFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedResistance();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested power from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
int getRequestedPowerFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedPower();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last requested inclination from ANT+ controller (only if not treadmill)
|
||||
*/
|
||||
double getRequestedInclinationFromAnt() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getRequestedInclination();
|
||||
}
|
||||
return -100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any pending control requests (only if not treadmill)
|
||||
*/
|
||||
void clearAntControlRequests() {
|
||||
if (!Ant.treadmill && bikeTransmitterController != null) {
|
||||
bikeTransmitterController.clearControlRequests();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmission info for debugging (only if not treadmill)
|
||||
*/
|
||||
String getBikeTransmitterInfo() {
|
||||
if (Ant.treadmill) {
|
||||
return "Bike transmitter disabled in treadmill mode";
|
||||
}
|
||||
if (bikeTransmitterController != null) {
|
||||
return bikeTransmitterController.getTransmissionInfo();
|
||||
}
|
||||
return "Bike transmitter not initialized";
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all channels currently added.
|
||||
*/
|
||||
@@ -317,7 +155,7 @@ public class ChannelService extends Service {
|
||||
|
||||
public void openAllChannels() throws ChannelNotAvailableException {
|
||||
if (Ant.heartRequest && heartChannelController == null)
|
||||
heartChannelController = new HeartChannelController(Ant.antHeartDeviceNumber);
|
||||
heartChannelController = new HeartChannelController();
|
||||
|
||||
if (Ant.speedRequest) {
|
||||
if(Ant.treadmill && sdmChannelController == null) {
|
||||
@@ -327,72 +165,6 @@ public class ChannelService extends Service {
|
||||
speedChannelController = new SpeedChannelController(acquireChannel());
|
||||
}
|
||||
}
|
||||
|
||||
// Add initialization for BikeChannelController (receiver)
|
||||
if (Ant.bikeRequest && bikeChannelController == null) {
|
||||
bikeChannelController = new BikeChannelController(Ant.technoGymGroupCycle, Ant.antBikeDeviceNumber);
|
||||
}
|
||||
|
||||
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
|
||||
if (!Ant.treadmill && bikeTransmitterController == null) {
|
||||
QLog.v(TAG, "Initializing BikeTransmitterController (not treadmill mode)");
|
||||
try {
|
||||
// Acquire channel like other controllers
|
||||
AntChannel transmitterChannel = acquireChannel();
|
||||
if (transmitterChannel != null) {
|
||||
bikeTransmitterController = new BikeTransmitterController(transmitterChannel);
|
||||
|
||||
// Set up control command listener to handle requests from ANT+ devices
|
||||
bikeTransmitterController.setControlCommandListener(new BikeTransmitterController.ControlCommandListener() {
|
||||
@Override
|
||||
public void onResistanceChangeRequested(int resistance) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Resistance change requested: " + resistance);
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_RESISTANCE_CHANGE");
|
||||
intent.putExtra("resistance", resistance);
|
||||
nativeSetResistance(resistance);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPowerChangeRequested(int power) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Power change requested: " + power + "W");
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_POWER_CHANGE");
|
||||
intent.putExtra("power", power);
|
||||
nativeSetPower(power);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInclinationChangeRequested(double inclination) {
|
||||
QLog.d(TAG, "ChannelService: ANT+ Inclination change requested: " + inclination + "%");
|
||||
// Send broadcast intent to notify the main application
|
||||
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_INCLINATION_CHANGE");
|
||||
intent.putExtra("inclination", inclination);
|
||||
nativeSetInclination(inclination);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
|
||||
QLog.i(TAG, "BikeTransmitterController initialized successfully (bike mode)");
|
||||
|
||||
// Start the bike transmitter immediately after initialization
|
||||
boolean transmissionStarted = bikeTransmitterController.startTransmission();
|
||||
if (transmissionStarted) {
|
||||
QLog.i(TAG, "BikeTransmitterController transmission started automatically");
|
||||
} else {
|
||||
QLog.w(TAG, "Failed to start BikeTransmitterController transmission");
|
||||
}
|
||||
} else {
|
||||
QLog.e(TAG, "Failed to acquire channel for BikeTransmitterController");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Failed to initialize BikeTransmitterController: " + e.getMessage());
|
||||
bikeTransmitterController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closeAllChannels() {
|
||||
@@ -404,18 +176,10 @@ public class ChannelService extends Service {
|
||||
speedChannelController.close();
|
||||
if (sdmChannelController != null)
|
||||
sdmChannelController.close();
|
||||
if (bikeChannelController != null) // Added closing bikeChannelController
|
||||
bikeChannelController.close();
|
||||
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
|
||||
bikeTransmitterController.close(); // Use close() method like other controllers
|
||||
}
|
||||
|
||||
heartChannelController = null;
|
||||
powerChannelController = null;
|
||||
speedChannelController = null;
|
||||
sdmChannelController = null;
|
||||
bikeChannelController = null; // Added nullifying bikeChannelController
|
||||
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
|
||||
}
|
||||
|
||||
AntChannel acquireChannel() throws ChannelNotAvailableException {
|
||||
@@ -436,13 +200,13 @@ public class ChannelService extends Service {
|
||||
else {
|
||||
NetworkKey mNK = new NetworkKey(new byte[]{(byte) 0xb9, (byte) 0xa5, (byte) 0x21, (byte) 0xfb,
|
||||
(byte) 0xbd, (byte) 0x72, (byte) 0xc3, (byte) 0x45});
|
||||
QLog.v(TAG, mNK.toString());
|
||||
Log.v(TAG, mNK.toString());
|
||||
mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
QLog.v(TAG, "ACP Remote Ex");
|
||||
Log.v(TAG, "ACP Remote Ex");
|
||||
} catch (UnsupportedFeatureException e) {
|
||||
QLog.v(TAG, "ACP UnsupportedFeature Ex");
|
||||
Log.v(TAG, "ACP UnsupportedFeature Ex");
|
||||
}
|
||||
}
|
||||
return mAntChannel;
|
||||
@@ -459,14 +223,14 @@ public class ChannelService extends Service {
|
||||
private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d(TAG, "onReceive");
|
||||
Log.d(TAG, "onReceive");
|
||||
if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) {
|
||||
boolean update = false;
|
||||
// Retrieving the data contained in the intent
|
||||
int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0);
|
||||
boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false);
|
||||
|
||||
QLog.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
|
||||
Log.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
|
||||
|
||||
if (mAllowAddChannel) {
|
||||
// Was a acquire channel allowed
|
||||
@@ -485,7 +249,7 @@ public class ChannelService extends Service {
|
||||
try {
|
||||
openAllChannels();
|
||||
} catch (ChannelNotAvailableException exception) {
|
||||
QLog.e(TAG, "Channel not available!!");
|
||||
Log.e(TAG, "Channel not available!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +258,7 @@ public class ChannelService extends Service {
|
||||
};
|
||||
|
||||
private void doBindAntRadioService() {
|
||||
if (BuildConfig.DEBUG) QLog.v(TAG, "doBindAntRadioService");
|
||||
if (BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService");
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
@@ -509,14 +273,14 @@ public class ChannelService extends Service {
|
||||
}
|
||||
|
||||
private void doUnbindAntRadioService() {
|
||||
if (BuildConfig.DEBUG) QLog.v(TAG, "doUnbindAntRadioService");
|
||||
if (BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService");
|
||||
|
||||
// Stop listing for channel available intents
|
||||
try {
|
||||
unregisterReceiver(mChannelProviderStateChangedReceiver);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
if (BuildConfig.DEBUG)
|
||||
QLog.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
|
||||
Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
|
||||
}
|
||||
|
||||
if (mAntRadioServiceBound) {
|
||||
@@ -551,7 +315,7 @@ public class ChannelService extends Service {
|
||||
}
|
||||
|
||||
static void die(String error) {
|
||||
QLog.e(TAG, "DIE: " + error);
|
||||
Log.e(TAG, "DIE: " + error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class ContentHelper {
|
||||
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
QLog.d("ContentHelper", "content");
|
||||
Log.d("ContentHelper", "content");
|
||||
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
QLog.d("ContentHelper", "cursor " + cursor);
|
||||
Log.d("ContentHelper", "cursor " + cursor);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
QLog.d("ContentHelper", "result " + result);
|
||||
Log.d("ContentHelper", "result " + result);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.DisplayCutout;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
|
||||
public class CustomQtActivity extends QtActivity {
|
||||
private static final String TAG = "CustomQtActivity";
|
||||
|
||||
// Declare the native method that will be implemented in C++
|
||||
private static native void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Log.d(TAG, "onCreate: CustomQtActivity initialized");
|
||||
|
||||
// This tells the OS that we want to handle the display cutout area ourselves
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
// This is the core of the new solution. We set a listener on the main view.
|
||||
// The OS will call this listener whenever the insets change (e.g., on rotation).
|
||||
final View decorView = getWindow().getDecorView();
|
||||
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
|
||||
if (density > 0) {
|
||||
// Use system window insets as primary source
|
||||
top = Math.round(insets.getSystemWindowInsetTop() / density);
|
||||
bottom = Math.round(insets.getSystemWindowInsetBottom() / density);
|
||||
left = Math.round(insets.getSystemWindowInsetLeft() / density);
|
||||
right = Math.round(insets.getSystemWindowInsetRight() / density);
|
||||
|
||||
// For API 28+, also check display cutout for additional padding
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
// Use the maximum between system window inset and cutout safe inset
|
||||
left = Math.max(left, Math.round(cutout.getSafeInsetLeft() / density));
|
||||
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
|
||||
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
|
||||
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "onApplyWindowInsets - Top:" + top + " Bottom:" + bottom + " Left:" + left + " Right:" + right);
|
||||
Log.d(TAG, "Raw insets - SystemTop:" + insets.getSystemWindowInsetTop() +
|
||||
" SystemBottom:" + insets.getSystemWindowInsetBottom() +
|
||||
" SystemLeft:" + insets.getSystemWindowInsetLeft() +
|
||||
" SystemRight:" + insets.getSystemWindowInsetRight());
|
||||
Log.d(TAG, "Stable insets - StableTop:" + insets.getStableInsetTop() +
|
||||
" StableBottom:" + insets.getStableInsetBottom() +
|
||||
" StableLeft:" + insets.getStableInsetLeft() +
|
||||
" StableRight:" + insets.getStableInsetRight());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout != null) {
|
||||
Log.d(TAG, "Cutout insets - Top:" + cutout.getSafeInsetTop() +
|
||||
" Bottom:" + cutout.getSafeInsetBottom() +
|
||||
" Left:" + cutout.getSafeInsetLeft() +
|
||||
" Right:" + cutout.getSafeInsetRight());
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new, correct inset values to the C++ layer
|
||||
onInsetsChanged(top, bottom, left, right);
|
||||
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This method is still needed for the QML check
|
||||
public static int getApiLevel() {
|
||||
return Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -29,26 +29,32 @@ public class FloatingHandler {
|
||||
static public int _width;
|
||||
static public int _height;
|
||||
static public int _alpha;
|
||||
static public String _htmlPage = "floating.htm";
|
||||
|
||||
public static void show(Context context, int port, int width, int height, int transparency, String htmlPage) {
|
||||
_context = context;
|
||||
_port = port;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_alpha = transparency;
|
||||
_htmlPage = htmlPage;
|
||||
public static void show(Context context, int port, int width, int height, int transparency) {
|
||||
_context = context;
|
||||
_port = port;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_alpha = transparency;
|
||||
|
||||
if (checkOverlayDisplayPermission()) {
|
||||
if (_intent == null)
|
||||
_intent = new Intent(context, FloatingWindowGFG.class);
|
||||
context.startService(_intent);
|
||||
} else {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
|
||||
Activity a = (Activity) _context;
|
||||
a.startActivityForResult(intent, -1);
|
||||
}
|
||||
}
|
||||
// First it confirms whether the
|
||||
// 'Display over other apps' permission in given
|
||||
if (checkOverlayDisplayPermission()) {
|
||||
if(_intent == null)
|
||||
_intent = new Intent(context, FloatingWindowGFG.class);
|
||||
// FloatingWindowGFG service is started
|
||||
context.startService(_intent);
|
||||
// The MainActivity closes here
|
||||
//finish();
|
||||
} else {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
|
||||
|
||||
// This method will start the intent. It takes two parameter, one is the Intent and the other is
|
||||
// an requestCode Integer. Here it is -1.
|
||||
Activity a = (Activity)_context;
|
||||
a.startActivityForResult(intent, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
if(_intent != null)
|
||||
|
||||
@@ -24,12 +24,8 @@ import android.widget.Toast;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
public class FloatingWindowGFG extends Service {
|
||||
|
||||
@@ -41,14 +37,6 @@ public class FloatingWindowGFG extends Service {
|
||||
private WindowManager.LayoutParams floatWindowLayoutParam;
|
||||
private WindowManager windowManager;
|
||||
private Button maximizeBtn;
|
||||
private Handler handler;
|
||||
private Runnable paddingTimeoutRunnable;
|
||||
private boolean isDraggingEnabled = false;
|
||||
private int originalHeight;
|
||||
private boolean isExpanded = false;
|
||||
private WebView webView;
|
||||
private int originalMargin = 20; // in dp, matching the XML layout
|
||||
private int reducedMargin = 2; // minimal margin when not dragging
|
||||
|
||||
// Retrieve the user preference node for the package com.mycompany
|
||||
SharedPreferences sharedPreferences;
|
||||
@@ -68,9 +56,6 @@ public class FloatingWindowGFG extends Service {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Initialize handler for timeout operations
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// The screen height and width are calculated, cause
|
||||
// the height and width of the floating window is set depending on this
|
||||
/*DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
|
||||
@@ -88,30 +73,23 @@ public class FloatingWindowGFG extends Service {
|
||||
// inflate a new view hierarchy from the floating_layout xml
|
||||
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
|
||||
|
||||
webView = (WebView)floatView.findViewById(R.id.webview);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
WebView wv = (WebView)floatView.findViewById(R.id.webview);
|
||||
wv.setWebViewClient(new WebViewClient(){
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
WebSettings settings = webView.getSettings();
|
||||
WebSettings settings = wv.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
|
||||
// Add JavaScript interface for communication with HTML
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
|
||||
webView.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
|
||||
webView.clearView();
|
||||
webView.measure(100, 100);
|
||||
webView.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/floating.htm");
|
||||
wv.clearView();
|
||||
wv.measure(100, 100);
|
||||
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
QLog.d("QZ","loadurl");
|
||||
|
||||
// Initially set reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
Log.d("QZ","loadurl");
|
||||
|
||||
|
||||
// WindowManager.LayoutParams takes a lot of parameters to set the
|
||||
@@ -138,18 +116,17 @@ public class FloatingWindowGFG extends Service {
|
||||
// 5) Next parameter is Layout_Format. System chooses a format that supports
|
||||
// translucency by PixelFormat.TRANSLUCENT
|
||||
|
||||
originalHeight = FloatingHandler._height;
|
||||
floatWindowLayoutParam = new WindowManager.LayoutParams(
|
||||
(int) (FloatingHandler._width ),
|
||||
(int) (originalHeight ),
|
||||
(int) (FloatingHandler._height ),
|
||||
LAYOUT_TYPE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
|
||||
// The Gravity of the Floating Window is set.
|
||||
// Use TOP | LEFT for free positioning without constraints
|
||||
floatWindowLayoutParam.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
// The Window will appear in the center of the screen
|
||||
floatWindowLayoutParam.gravity = Gravity.CENTER;
|
||||
|
||||
// X and Y value of the window is set
|
||||
floatWindowLayoutParam.x = 0;
|
||||
@@ -168,86 +145,48 @@ public class FloatingWindowGFG extends Service {
|
||||
// The window can be moved at any position on the screen.
|
||||
floatView.setOnTouchListener(new View.OnTouchListener() {
|
||||
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
|
||||
int initialX;
|
||||
int initialY;
|
||||
float initialTouchX;
|
||||
float initialTouchY;
|
||||
boolean isDragging = false;
|
||||
final int TOUCH_THRESHOLD = 10; // Threshold for distinguishing tap vs drag
|
||||
double x;
|
||||
double y;
|
||||
double px;
|
||||
double py;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
QLog.d("QZ","onTouch action: " + event.getAction());
|
||||
Log.d("QZ","onTouch");
|
||||
|
||||
switch (event.getAction()) {
|
||||
// When the window will be touched,
|
||||
// the x and y position of that position
|
||||
// will be retrieved
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Store initial positions
|
||||
initialX = floatWindowLayoutUpdateParam.x;
|
||||
initialY = floatWindowLayoutUpdateParam.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
isDragging = false;
|
||||
|
||||
// Enable dragging for 5 seconds
|
||||
enableDraggingTemporarily();
|
||||
break;
|
||||
x = floatWindowLayoutUpdateParam.x;
|
||||
y = floatWindowLayoutUpdateParam.y;
|
||||
|
||||
// returns the original raw X
|
||||
// coordinate of this event
|
||||
px = event.getRawX();
|
||||
|
||||
// returns the original raw Y
|
||||
// coordinate of this event
|
||||
py = event.getRawY();
|
||||
break;
|
||||
// When the window will be dragged around,
|
||||
// it will update the x, y of the Window Layout Parameter
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Calculate distance moved
|
||||
float deltaX = event.getRawX() - initialTouchX;
|
||||
float deltaY = event.getRawY() - initialTouchY;
|
||||
|
||||
// Check if we've moved enough to consider this a drag
|
||||
if (!isDragging && (Math.abs(deltaX) > TOUCH_THRESHOLD || Math.abs(deltaY) > TOUCH_THRESHOLD)) {
|
||||
isDragging = true;
|
||||
}
|
||||
|
||||
// Only allow dragging if it's temporarily enabled
|
||||
if (isDragging && isDraggingEnabled) {
|
||||
// Get screen dimensions for boundary checking
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
// Calculate new position
|
||||
int newX = initialX + (int) deltaX;
|
||||
int newY = initialY + (int) deltaY;
|
||||
|
||||
// Apply boundary constraints
|
||||
// Keep window within screen bounds
|
||||
int windowWidth = FloatingHandler._width;
|
||||
int windowHeight = FloatingHandler._height;
|
||||
|
||||
if (newX < 0) newX = 0;
|
||||
if (newY < 0) newY = 0;
|
||||
if (newX + windowWidth > screenWidth) newX = screenWidth - windowWidth;
|
||||
if (newY + windowHeight > screenHeight) newY = screenHeight - windowHeight;
|
||||
|
||||
// Update position
|
||||
floatWindowLayoutUpdateParam.x = newX;
|
||||
floatWindowLayoutUpdateParam.y = newY;
|
||||
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
|
||||
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
|
||||
|
||||
// Save position to preferences
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.apply(); // Use apply() instead of commit() for better performance
|
||||
SharedPreferences.Editor myEdit = sharedPreferences.edit();
|
||||
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
|
||||
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
|
||||
myEdit.commit();
|
||||
|
||||
// Apply updated parameter to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
}
|
||||
// updated parameter is applied to the WindowManager
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
// If it wasn't a drag, it's a tap - let the WebView handle it
|
||||
if (!isDragging) {
|
||||
return false; // Let the event propagate to WebView
|
||||
}
|
||||
isDragging = false;
|
||||
break;
|
||||
}
|
||||
return isDragging && isDraggingEnabled; // Consume the event only if we're dragging and dragging is enabled
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -261,107 +200,4 @@ public class FloatingWindowGFG extends Service {
|
||||
// Window is removed from the screen
|
||||
windowManager.removeView(floatView);
|
||||
}
|
||||
|
||||
// Method to enable dragging temporarily for 5 seconds
|
||||
private void enableDraggingTemporarily() {
|
||||
isDraggingEnabled = true;
|
||||
|
||||
// Increase margin for better dragging experience
|
||||
setWebViewMargin(originalMargin);
|
||||
|
||||
// Cancel any existing timeout
|
||||
if (paddingTimeoutRunnable != null) {
|
||||
handler.removeCallbacks(paddingTimeoutRunnable);
|
||||
}
|
||||
|
||||
// Create new timeout runnable
|
||||
paddingTimeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
isDraggingEnabled = false;
|
||||
// Restore reduced margin for normal operation
|
||||
setWebViewMargin(reducedMargin);
|
||||
QLog.d("QZ", "Dragging disabled after timeout, margin restored");
|
||||
}
|
||||
};
|
||||
|
||||
// Schedule timeout for 5 seconds
|
||||
handler.postDelayed(paddingTimeoutRunnable, 5000);
|
||||
}
|
||||
|
||||
// Method to expand window height dynamically
|
||||
private void expandWindow(int additionalHeight) {
|
||||
if (!isExpanded) {
|
||||
isExpanded = true;
|
||||
floatWindowLayoutParam.height = originalHeight + additionalHeight;
|
||||
|
||||
// Adjust Y position to keep window within screen bounds
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
|
||||
if (floatWindowLayoutParam.y + floatWindowLayoutParam.height > screenHeight) {
|
||||
floatWindowLayoutParam.y = screenHeight - floatWindowLayoutParam.height;
|
||||
if (floatWindowLayoutParam.y < 0) {
|
||||
floatWindowLayoutParam.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window expanded to height: " + floatWindowLayoutParam.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to restore original window height
|
||||
private void restoreWindow() {
|
||||
if (isExpanded) {
|
||||
isExpanded = false;
|
||||
floatWindowLayoutParam.height = originalHeight;
|
||||
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
|
||||
QLog.d("QZ", "Window restored to original height: " + originalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set WebView margin dynamically
|
||||
private void setWebViewMargin(int marginDp) {
|
||||
if (webView != null) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) webView.getLayoutParams();
|
||||
int marginPx = (int) (marginDp * getResources().getDisplayMetrics().density);
|
||||
params.setMargins(marginPx, marginPx, marginPx, marginPx);
|
||||
webView.setLayoutParams(params);
|
||||
QLog.d("QZ", "WebView margin set to: " + marginDp + "dp (" + marginPx + "px)");
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript interface class
|
||||
public class WebAppInterface {
|
||||
@JavascriptInterface
|
||||
public void expandFloatingWindow(int additionalHeight) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
expandWindow(additionalHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void restoreFloatingWindow() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoreWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void enableDraggingMargins() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
enableDraggingTemporarily();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class ForegroundService extends Service {
|
||||
public static final String CHANNEL_ID = "ForegroundServiceChannel";
|
||||
@@ -43,7 +43,7 @@ public class ForegroundService extends Service {
|
||||
startForeground(1, notification);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
Log.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
//do heavy work on a background thread
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
@@ -53,32 +53,26 @@ public class Garmin {
|
||||
private static Integer Power = 0;
|
||||
|
||||
public static int getHR() {
|
||||
QLog.d(TAG, "getHR " + HR);
|
||||
Log.d(TAG, "getHR " + HR);
|
||||
return HR;
|
||||
}
|
||||
|
||||
public static int getPower() {
|
||||
QLog.d(TAG, "getPower " + Power);
|
||||
Log.d(TAG, "getPower " + Power);
|
||||
return Power;
|
||||
}
|
||||
|
||||
public static double getSpeed() {
|
||||
QLog.d(TAG, "getSpeed " + Speed);
|
||||
Log.d(TAG, "getSpeed " + Speed);
|
||||
return Speed;
|
||||
}
|
||||
|
||||
public static int getFootCad() {
|
||||
QLog.d(TAG, "getFootCad " + FootCad);
|
||||
Log.d(TAG, "getFootCad " + FootCad);
|
||||
return FootCad;
|
||||
}
|
||||
|
||||
public static void init(Context c) {
|
||||
if (connectIqReady || connectIqInitializing) {
|
||||
QLog.d(TAG, "Garmin already initialized or initializing");
|
||||
return;
|
||||
}
|
||||
connectIqInitializing = true;
|
||||
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -89,8 +83,7 @@ public class Garmin {
|
||||
|
||||
@Override
|
||||
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
|
||||
QLog.e(TAG, errStatus.toString());
|
||||
connectIqInitializing = false;
|
||||
Log.e(TAG, errStatus.toString());
|
||||
connectIqReady = false;
|
||||
}
|
||||
|
||||
@@ -98,7 +91,7 @@ public class Garmin {
|
||||
public void onSdkReady() {
|
||||
connectIqInitializing = false;
|
||||
connectIqReady = true;
|
||||
QLog.i(TAG, " onSdkReady");
|
||||
Log.i(TAG, " onSdkReady");
|
||||
|
||||
registerWatchMessagesReceiver();
|
||||
registerDeviceStatusReceiver();
|
||||
@@ -125,16 +118,16 @@ public class Garmin {
|
||||
try {
|
||||
List<IQDevice> devices = connectIQ.getConnectedDevices();
|
||||
if (devices != null && devices.size() > 0) {
|
||||
QLog.v(TAG, "getDevice connected: " + devices.get(0).toString() );
|
||||
Log.v(TAG, "getDevice connected: " + devices.get(0).toString() );
|
||||
deviceCache = devices.get(0);
|
||||
return deviceCache;
|
||||
} else {
|
||||
return deviceCache;
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
} catch (ServiceUnavailableException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -200,33 +193,33 @@ public class Garmin {
|
||||
|
||||
@Override
|
||||
public void onApplicationInfoReceived(IQApp app) {
|
||||
QLog.d(TAG, "App installed.");
|
||||
Log.d(TAG, "App installed.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationNotInstalled(String applicationId) {
|
||||
if (getDevice() != null) {
|
||||
Toast.makeText(context, "App not installed on your Garmin watch", Toast.LENGTH_LONG).show();
|
||||
QLog.d(TAG, "watch app not installed.");
|
||||
Log.d(TAG, "watch app not installed.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (InvalidStateException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
} catch (ServiceUnavailableException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerDeviceStatusReceiver() {
|
||||
QLog.d(TAG, "registerDeviceStatusReceiver");
|
||||
Log.d(TAG, "registerDeviceStatusReceiver");
|
||||
IQDevice device = getDevice();
|
||||
try {
|
||||
if (device != null) {
|
||||
connectIQ.registerForDeviceEvents(device, new ConnectIQ.IQDeviceEventListener() {
|
||||
@Override
|
||||
public void onDeviceStatusChanged(IQDevice device, IQDevice.IQDeviceStatus newStatus) {
|
||||
QLog.d(TAG, "Device status changed, now " + newStatus);
|
||||
Log.d(TAG, "Device status changed, now " + newStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -236,7 +229,7 @@ public class Garmin {
|
||||
}
|
||||
|
||||
private static void registerWatchMessagesReceiver(){
|
||||
QLog.d(TAG, "registerWatchMessageReceiver");
|
||||
Log.d(TAG, "registerWatchMessageReceiver");
|
||||
IQDevice device = getDevice();
|
||||
try {
|
||||
if (device != null) {
|
||||
@@ -245,7 +238,7 @@ public class Garmin {
|
||||
public void onMessageReceived(IQDevice device, IQApp app, List<Object> message, ConnectIQ.IQMessageStatus status) {
|
||||
if (status == ConnectIQ.IQMessageStatus.SUCCESS) {
|
||||
//MessageHandler.getInstance().handleMessageFromWatchUsingCIQ(message, status, context);
|
||||
QLog.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
|
||||
Log.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
|
||||
try {
|
||||
String var[] = message.toArray()[0].toString().split(",");
|
||||
HR = Integer.parseInt(var[0].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
|
||||
@@ -256,21 +249,21 @@ public class Garmin {
|
||||
Speed = Double.parseDouble(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
|
||||
}
|
||||
}
|
||||
QLog.d(TAG, "HR " + HR);
|
||||
QLog.d(TAG, "FootCad " + FootCad);
|
||||
Log.d(TAG, "HR " + HR);
|
||||
Log.d(TAG, "FootCad " + FootCad);
|
||||
} catch (Exception e) {
|
||||
QLog.e(TAG, "Processing error", e);
|
||||
Log.e(TAG, "Processing error", e);
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "onMessageReceived error, status: " + status.toString());
|
||||
Log.d(TAG, "onMessageReceived error, status: " + status.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
QLog.d(TAG, "registerWatchMessagesReceiver: No device found.");
|
||||
Log.d(TAG, "registerWatchMessagesReceiver: No device found.");
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,19 +273,19 @@ public class Garmin {
|
||||
|
||||
try {
|
||||
if (context != null) {
|
||||
QLog.d(TAG, "Shutting down with wrapped context");
|
||||
Log.d(TAG, "Shutting down with wrapped context");
|
||||
connectIQ.shutdown(context);
|
||||
} else {
|
||||
QLog.d(TAG, "Shutting down without wrapped context");
|
||||
Log.d(TAG, "Shutting down without wrapped context");
|
||||
connectIQ.shutdown(applicationContext);
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
// This is usually because the SDK was already shut down so no worries.
|
||||
QLog.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
|
||||
Log.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
} catch (RuntimeException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,11 +299,11 @@ public class Garmin {
|
||||
}
|
||||
}
|
||||
} catch (InvalidStateException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
} catch (RuntimeException e) {
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127
src/android/src/HealthConnect.java
Normal file
127
src/android/src/HealthConnect.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IntentFilter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.health.connect.client.HealthConnectClient;
|
||||
import androidx.health.connect.client.PermissionController;
|
||||
import androidx.health.connect.client.permission.HealthPermission;
|
||||
import androidx.health.connect.client.records.*;
|
||||
import androidx.health.connect.client.request.*;
|
||||
import androidx.health.connect.client.time.*;
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class HealthConnect {
|
||||
|
||||
private static final Set<HealthPermission> PERMISSIONS = new HashSet<>();
|
||||
private ActivityResultLauncher<Set<HealthPermission>> requestPermissions;
|
||||
private static HealthConnectClient healthConnectClient;
|
||||
private static final int PERMISSION_REQUEST_CODE = 123; // Define your request code
|
||||
|
||||
private static void checkAndRun(Context context) {
|
||||
int availabilityStatus = HealthConnectClient.getSdkStatus(context, "com.google.android.apps.healthdata");
|
||||
if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
|
||||
return; // early return as there is no viable integration
|
||||
}
|
||||
if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
|
||||
String uriString = "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding";
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage("com.android.vending");
|
||||
intent.setData(Uri.parse(uriString));
|
||||
intent.putExtra("overlay", true);
|
||||
intent.putExtra("callerId", context.getPackageName());
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
healthConnectClient = HealthConnectClient.getOrCreate(context);
|
||||
// Issue operations with healthConnectClient
|
||||
|
||||
PERMISSIONS.add(HealthPermission.getReadPermission(kotlin.jvm.JvmClassMappingKt.getKotlinClass(StepsRecord.class)));
|
||||
PERMISSIONS.add(HealthPermission.getWritePermission(kotlin.jvm.JvmClassMappingKt.getKotlinClass(StepsRecord.class)));
|
||||
PERMISSIONS.add(HealthPermission.getReadPermission(kotlin.jvm.JvmClassMappingKt.getKotlinClass(HeartRateRecord.class)));
|
||||
PERMISSIONS.add(HealthPermission.getWritePermission(kotlin.jvm.JvmClassMappingKt.getKotlinClass(HeartRateRecord.class)));
|
||||
|
||||
checkPermissionsAndRun(context);
|
||||
}
|
||||
|
||||
private void handlePermissionsResult(Boolean granted) {
|
||||
if (granted) {
|
||||
// Permissions successfully granted
|
||||
} else {
|
||||
// Lack of required permissions
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkPermissionsAndRun(Context context) {
|
||||
permissionController = HealthConnectClient.getOrCreate(context).getPermissionController();
|
||||
Set<HealthPermission> granted = permissionController.getGrantedPermissions();
|
||||
if (granted.containsAll(PERMISSIONS)) {
|
||||
// Permissions already granted; proceed with inserting or reading data
|
||||
} else {
|
||||
Intent intent = permissionController.createPermissionRequestIntent(PERMISSIONS);
|
||||
startActivityForResult(intent, PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
// Permissions successfully granted
|
||||
} else {
|
||||
// Lack of required permissions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readStepsByTimeRange(HealthConnectClient healthConnectClient, Instant startTime, Instant endTime) {
|
||||
try {
|
||||
ReadRecordsRequest request = new ReadRecordsRequest(
|
||||
StepsRecord.class,
|
||||
new TimeRangeFilter.between(startTime, endTime)
|
||||
);
|
||||
ReadRecordsResponse response = healthConnectClient.readRecords(request);
|
||||
for (StepsRecord stepRecord : response.getRecords()) {
|
||||
// Process each step record
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Run error handling here.
|
||||
}
|
||||
}
|
||||
|
||||
public static void init(Context c) {
|
||||
checkAndRun(c);
|
||||
readStepsByTimeRange(healthConnectClient, Instant.now(), Instant.now());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.content.Context;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.app.Activity;
|
||||
|
||||
// ANT+ Plugin imports
|
||||
@@ -42,14 +42,14 @@ public class HeartChannelController {
|
||||
private boolean isConnected = false;
|
||||
public int heart = 0; // Public to be accessible from ChannelService
|
||||
|
||||
public HeartChannelController(int antHeartDeviceNumber) {
|
||||
public HeartChannelController() {
|
||||
this.context = Ant.activity;
|
||||
openChannel(antHeartDeviceNumber);
|
||||
openChannel();
|
||||
}
|
||||
|
||||
public boolean openChannel(int deviceNumber) {
|
||||
// Request access to heart rate device (deviceNumber = 0 means first available)
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, deviceNumber, 0,
|
||||
public boolean openChannel() {
|
||||
// Request access to first available heart rate device
|
||||
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
|
||||
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
|
||||
@Override
|
||||
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
|
||||
@@ -57,26 +57,26 @@ public class HeartChannelController {
|
||||
case SUCCESS:
|
||||
hrPcc = result;
|
||||
isConnected = true;
|
||||
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
|
||||
Log.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
|
||||
subscribeToHrEvents();
|
||||
break;
|
||||
case CHANNEL_NOT_AVAILABLE:
|
||||
QLog.e(TAG, "Channel Not Available");
|
||||
Log.e(TAG, "Channel Not Available");
|
||||
break;
|
||||
case ADAPTER_NOT_DETECTED:
|
||||
QLog.e(TAG, "ANT Adapter Not Available");
|
||||
Log.e(TAG, "ANT Adapter Not Available");
|
||||
break;
|
||||
case BAD_PARAMS:
|
||||
QLog.e(TAG, "Bad request parameters");
|
||||
Log.e(TAG, "Bad request parameters");
|
||||
break;
|
||||
case OTHER_FAILURE:
|
||||
QLog.e(TAG, "RequestAccess failed");
|
||||
Log.e(TAG, "RequestAccess failed");
|
||||
break;
|
||||
case DEPENDENCY_NOT_INSTALLED:
|
||||
QLog.e(TAG, "Dependency not installed");
|
||||
Log.e(TAG, "Dependency not installed");
|
||||
break;
|
||||
default:
|
||||
QLog.e(TAG, "Unrecognized result: " + resultCode);
|
||||
Log.e(TAG, "Unrecognized result: " + resultCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class HeartChannelController {
|
||||
new IDeviceStateChangeReceiver() {
|
||||
@Override
|
||||
public void onDeviceStateChange(DeviceState newDeviceState) {
|
||||
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
Log.d(TAG, "Device State Changed to: " + newDeviceState);
|
||||
if (newDeviceState == DeviceState.DEAD) {
|
||||
isConnected = false;
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public class HeartChannelController {
|
||||
BigDecimal heartBeatEventTime, DataState dataState) {
|
||||
|
||||
heart = computedHeartRate;
|
||||
QLog.d(TAG, "Heart Rate: " + heart);
|
||||
Log.d(TAG, "Heart Rate: " + heart);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -117,7 +117,7 @@ public class HeartChannelController {
|
||||
}
|
||||
hrPcc = null;
|
||||
isConnected = false;
|
||||
QLog.d(TAG, "Channel Closed");
|
||||
Log.d(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
|
||||
@@ -16,7 +16,7 @@ import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.os.Build;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
@@ -169,7 +169,7 @@ public class HidBridge {
|
||||
} catch(NullPointerException e)
|
||||
{
|
||||
Log("Error happened while writing. Could not connect to the device or interface is busy?");
|
||||
QLog.e("HidBridge", QLog.getStackTraceString(e));
|
||||
Log.e("HidBridge", Log.getStackTraceString(e));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -289,7 +289,7 @@ public class HidBridge {
|
||||
|
||||
catch (NullPointerException e) {
|
||||
Log("Error happened while reading. No device or the connection is busy");
|
||||
QLog.e("HidBridge", QLog.getStackTraceString(e));
|
||||
Log.e("HidBridge", Log.getStackTraceString(e));
|
||||
}
|
||||
catch (ThreadDeath e) {
|
||||
if (readConnection != null) {
|
||||
@@ -332,7 +332,7 @@ public class HidBridge {
|
||||
}
|
||||
}
|
||||
else {
|
||||
QLog.d("TAG", "permission denied for the device " + device);
|
||||
Log.d("TAG", "permission denied for the device " + device);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +344,7 @@ public class HidBridge {
|
||||
* @param message to log.
|
||||
*/
|
||||
private void Log(String message) {
|
||||
QLog.d("HidBridge", message);
|
||||
Log.d("HidBridge", message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.garmin.android.connectiq.IQDevice;
|
||||
|
||||
import java.nio.BufferUnderflowException;
|
||||
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
private final BroadcastReceiver receiver;
|
||||
@@ -20,7 +20,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d(TAG, "onReceive intent " + intent.getAction());
|
||||
Log.d(TAG, "onReceive intent " + intent.getAction());
|
||||
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
|
||||
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
|
||||
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
|
||||
@@ -32,7 +32,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
try {
|
||||
receiver.onReceive(context, intent);
|
||||
} catch (IllegalArgumentException | BufferUnderflowException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
Log.d(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
|
||||
intent.putExtra(extraName, device.getDeviceIdentifier());
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
QLog.d(TAG, e.toString());
|
||||
Log.d(TAG, e.toString());
|
||||
// It's already a long, i.e. on the simulator.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.LocationManager;
|
||||
import android.provider.Settings;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class LocationHelper {
|
||||
private static final String TAG = "LocationHelper";
|
||||
@@ -13,7 +13,7 @@ public class LocationHelper {
|
||||
private static boolean isBluetoothEnabled = false;
|
||||
|
||||
public static boolean start(Context context) {
|
||||
QLog.d(TAG, "Starting LocationHelper check...");
|
||||
Log.d(TAG, "Starting LocationHelper check...");
|
||||
|
||||
isLocationEnabled = isLocationEnabled(context);
|
||||
isBluetoothEnabled = isBluetoothEnabled();
|
||||
@@ -23,7 +23,7 @@ public class LocationHelper {
|
||||
|
||||
public static void requestPermissions(Context context) {
|
||||
if (!isLocationEnabled || !isBluetoothEnabled) {
|
||||
QLog.d(TAG, "Some services are disabled. Prompting user...");
|
||||
Log.d(TAG, "Some services are disabled. Prompting user...");
|
||||
if (!isLocationEnabled) {
|
||||
promptEnableLocation(context);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class LocationHelper {
|
||||
promptEnableBluetooth(context);
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "All services are already enabled.");
|
||||
Log.d(TAG, "All services are already enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ public class LocationHelper {
|
||||
}
|
||||
|
||||
private static void promptEnableLocation(Context context) {
|
||||
QLog.d(TAG, "Prompting to enable Location...");
|
||||
Log.d(TAG, "Prompting to enable Location...");
|
||||
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void promptEnableBluetooth(Context context) {
|
||||
QLog.d(TAG, "Prompting to enable Bluetooth...");
|
||||
Log.d(TAG, "Prompting to enable Bluetooth...");
|
||||
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
|
||||
@@ -5,19 +5,15 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
private static MediaButtonReceiver instance;
|
||||
private static final int TARGET_VOLUME = 7; // Middle volume value for infinite gear changes
|
||||
private static boolean restoringVolume = false; // Flag to prevent recursion
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
QLog.d("MediaButtonReceiver", "Received intent: " + intent.toString());
|
||||
Log.d("MediaButtonReceiver", "Received intent: " + intent.toString());
|
||||
String intentAction = intent.getAction();
|
||||
if ("android.media.VOLUME_CHANGED_ACTION".equals(intentAction)) {
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
@@ -25,30 +21,8 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
|
||||
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
|
||||
|
||||
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Previous: " + previousVolume + ", Max: " + maxVolume + ", Restoring: " + restoringVolume);
|
||||
|
||||
// If we're restoring volume, skip processing and reset flag
|
||||
if (restoringVolume) {
|
||||
QLog.d("MediaButtonReceiver", "Volume restore completed");
|
||||
restoringVolume = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the gear change
|
||||
Log.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
|
||||
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
|
||||
|
||||
// Auto-restore volume to middle value after a short delay to enable infinite gear changes
|
||||
if (currentVolume != TARGET_VOLUME) {
|
||||
final AudioManager am = audioManager;
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("MediaButtonReceiver", "Auto-restoring volume to: " + TARGET_VOLUME);
|
||||
restoringVolume = true;
|
||||
am.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
|
||||
}
|
||||
}, 100); // 100ms delay to ensure gear change is processed first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +36,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
|
||||
|
||||
if (context == null) {
|
||||
QLog.e("MediaButtonReceiver", "Context is null, cannot register receiver");
|
||||
Log.e("MediaButtonReceiver", "Context is null, cannot register receiver");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,39 +44,21 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
|
||||
} catch (SecurityException se) {
|
||||
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
context.registerReceiver(instance, filter);
|
||||
} catch (SecurityException se) {
|
||||
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
Log.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
|
||||
}
|
||||
}
|
||||
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
|
||||
|
||||
// Initialize volume to target value for gear control
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (audioManager != null) {
|
||||
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
|
||||
if (currentVolume != TARGET_VOLUME) {
|
||||
QLog.d("MediaButtonReceiver", "Initializing volume to: " + TARGET_VOLUME);
|
||||
restoringVolume = true;
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
|
||||
// Reset flag after initialization
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
restoringVolume = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("MediaButtonReceiver", "Receiver registered successfully");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
|
||||
Log.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
QLog.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
|
||||
Log.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.util.DisplayMetrics;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.app.AppOpsManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.annotation.TargetApi;
|
||||
|
||||
import com.rvalerio.fgchecker.AppChecker;
|
||||
@@ -83,11 +83,11 @@ public class MediaProjection {
|
||||
@Override
|
||||
public void onForeground(String packageName) {
|
||||
_packageName = packageName;
|
||||
/*QLog.e("MediaProjection", packageName);
|
||||
/*Log.e("MediaProjection", packageName);
|
||||
if(isLandscape())
|
||||
QLog.e("MediaProjection", "Landscape");
|
||||
Log.e("MediaProjection", "Landscape");
|
||||
else
|
||||
QLog.e("MediaProjection", "Portrait");*/
|
||||
Log.e("MediaProjection", "Portrait");*/
|
||||
}
|
||||
})
|
||||
.timeout(1000)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
|
||||
@@ -12,6 +12,6 @@ public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
activity_ = this;
|
||||
QLog.v(TAG, "onCreate");
|
||||
Log.v(TAG, "onCreate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -12,7 +12,7 @@ public class NativeScanCallback extends ScanCallback {
|
||||
public native void scanError(int code);
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
QLog.i(TAG, "Res " + result);
|
||||
Log.i(TAG, "Res " + result);
|
||||
newScanResult(new ScanRecordResult(result));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class NativeScanCallback extends ScanCallback {
|
||||
|
||||
@Override
|
||||
public void onScanFailed(int errorCode) {
|
||||
QLog.i(TAG, "onScanFailed "+errorCode);
|
||||
Log.i(TAG, "onScanFailed "+errorCode);
|
||||
scanError(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -61,7 +61,7 @@ public class PowerChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
Log.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -92,7 +92,7 @@ public class PowerChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
QLog.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
|
||||
Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
|
||||
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
@@ -102,7 +102,7 @@ public class PowerChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.w(TAG, "No channel available");
|
||||
Log.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -112,7 +112,7 @@ public class PowerChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
QLog.e(TAG, logString);
|
||||
Log.e(TAG, logString);
|
||||
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class PowerChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
QLog.e(TAG, logString.toString());
|
||||
Log.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public class PowerChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
Log.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,13 +175,13 @@ public class PowerChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
QLog.e(TAG, "Channel Death");
|
||||
Log.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
if(carousalTimer == null) {
|
||||
@@ -189,7 +189,7 @@ public class PowerChannelController {
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
byte[] payload = new byte[8];
|
||||
eventCount = (eventCount + 1) & 0xFF;
|
||||
cumulativePower = (cumulativePower + power) & 0xFFFF;
|
||||
@@ -225,7 +225,7 @@ public class PowerChannelController {
|
||||
// Rx Data
|
||||
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
|
||||
payload = new AcknowledgedDataMessage(antParcel).getPayload();
|
||||
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
Log.d(TAG, "AcknowledgedDataMessage: " + payload);
|
||||
|
||||
if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) {
|
||||
payload[0] = (byte) 0x01;
|
||||
@@ -268,7 +268,7 @@ public class PowerChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -320,7 +320,7 @@ public class PowerChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
QLog.e(TAG, "No Device Found");
|
||||
Log.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* QLog - Wrapper for Android's Log class that redirects logs to Qt's logging system
|
||||
* Usage: import org.cagnulen.qdomyoszwift.Log;
|
||||
*/
|
||||
public class QLog {
|
||||
public static native void sendToQt(int level, String tag, String message);
|
||||
|
||||
static {
|
||||
try {
|
||||
// Try to load the native library if needed
|
||||
System.loadLibrary("qtlogging_native");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Library might be loaded elsewhere, or will be loaded later
|
||||
Log.w("QLog", "Native library not loaded yet: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Debug level methods
|
||||
public static int d(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(3, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.d(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Error level methods
|
||||
public static int e(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(6, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.e(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Info level methods
|
||||
public static int i(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(4, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.i(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Verbose level methods
|
||||
public static int v(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(2, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg);
|
||||
}
|
||||
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.v(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Warning level methods
|
||||
public static int w(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(5, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg);
|
||||
}
|
||||
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, msg, tr);
|
||||
}
|
||||
|
||||
public static int w(String tag, Throwable tr) {
|
||||
try {
|
||||
sendToQt(5, tag, Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.w(tag, tr);
|
||||
}
|
||||
|
||||
// What a Terrible Failure: Report an exception that should never happen
|
||||
public static int wtf(String tag, String msg) {
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, tr);
|
||||
}
|
||||
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
try {
|
||||
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.wtf(tag, msg, tr);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
public static String getStackTraceString(Throwable tr) {
|
||||
return Log.getStackTraceString(tr);
|
||||
}
|
||||
|
||||
public static boolean isLoggable(String tag, int level) {
|
||||
return Log.isLoggable(tag, level);
|
||||
}
|
||||
|
||||
// Additional utility methods
|
||||
public static int println(int priority, String tag, String msg) {
|
||||
try {
|
||||
sendToQt(priority, tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// Native library not available, continue with Android logging only
|
||||
}
|
||||
return Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
// API Level 28+ (Android 9+) methods
|
||||
public static RuntimeException getStackTraceElement() {
|
||||
try {
|
||||
return (RuntimeException) Log.class.getMethod("getStackTraceElement").invoke(null);
|
||||
} catch (Exception e) {
|
||||
return new RuntimeException("QLog: Failed to get stack trace element");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.RadioButton;
|
||||
@@ -43,8 +43,6 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
private static final String LOG_TAG = "QZ:AdbRemote";
|
||||
private static String lastCommand = "";
|
||||
private static boolean ADBConnected = false;
|
||||
private static boolean cryptoReady = false;
|
||||
private static final Object cryptoLock = new Object();
|
||||
|
||||
private static String _address = "127.0.0.1";
|
||||
private static Context _context;
|
||||
@@ -64,46 +62,31 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
@Override
|
||||
public void notifyConnectionEstablished(DeviceConnection devConn) {
|
||||
QLog.d(LOG_TAG, "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = true;
|
||||
QLog.i(LOG_TAG, "notifyConnectionEstablished - CONNECTED=true, lastCommand=" + lastCommand);
|
||||
QLog.d(LOG_TAG, "notifyConnectionEstablished - END: ADBConnected=" + ADBConnected);
|
||||
Log.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d(LOG_TAG, "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
|
||||
if (e != null) {
|
||||
QLog.e(LOG_TAG, "notifyConnectionFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
Log.e(LOG_TAG, e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d(LOG_TAG, "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
|
||||
if (e != null) {
|
||||
QLog.e(LOG_TAG, "notifyStreamFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
Log.e(LOG_TAG, e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamClosed(DeviceConnection devConn) {
|
||||
QLog.d(LOG_TAG, "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
ADBConnected = false;
|
||||
QLog.e(LOG_TAG, "notifyStreamClosed - ADBConnected=" + ADBConnected);
|
||||
Log.e(LOG_TAG, "notifyStreamClosed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdbCrypto loadAdbCrypto(DeviceConnection devConn) {
|
||||
QLog.d(LOG_TAG, "loadAdbCrypto - START: devConn=" + devConn + ", context=" + _context);
|
||||
|
||||
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
QLog.d(LOG_TAG, "loadAdbCrypto - RESULT: crypto=" + (crypto != null ? "valid" : "null"));
|
||||
return crypto;
|
||||
return AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,7 +96,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
@Override
|
||||
public void receivedData(DeviceConnection devConn, byte[] data, int offset, int length) {
|
||||
QLog.i(LOG_TAG, data.toString());
|
||||
Log.i(LOG_TAG, data.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,132 +111,96 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
|
||||
|
||||
private DeviceConnection startConnection(String host, int port) {
|
||||
QLog.d(LOG_TAG, "startConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
|
||||
/* Create the connection object */
|
||||
DeviceConnection conn = binder.createConnection(host, port);
|
||||
QLog.d(LOG_TAG, "startConnection - CONNECTION_CREATED: conn=" + conn);
|
||||
|
||||
/* Add this activity as a connection listener */
|
||||
binder.addListener(conn, this);
|
||||
QLog.d(LOG_TAG, "startConnection - LISTENER_ADDED: this=" + this);
|
||||
|
||||
/* Begin the async connection process */
|
||||
QLog.d(LOG_TAG, "startConnection - STARTING_CONNECT: about to call conn.startConnect()");
|
||||
conn.startConnect();
|
||||
QLog.d(LOG_TAG, "startConnection - END: startConnect() called, returning conn=" + conn);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
private DeviceConnection connectOrLookupConnection(String host, int port) {
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
|
||||
DeviceConnection conn = binder.findConnection(host, port);
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - EXISTING_CONN: conn=" + (conn != null ? "found" : "null"));
|
||||
if (conn == null) {
|
||||
/* No existing connection, so start the connection process */
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - NEW_CONNECTION: starting new connection");
|
||||
conn = startConnection(host, port);
|
||||
}
|
||||
else {
|
||||
/* Add ourselves as a new listener of this connection */
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - REUSE_CONNECTION: adding listener to existing connection");
|
||||
binder.addListener(conn, this);
|
||||
}
|
||||
QLog.d(LOG_TAG, "connectOrLookupConnection - END: returning conn=" + conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public ServiceConnection serviceConn = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
|
||||
QLog.d(LOG_TAG, "onServiceConnected - START: componentName=" + arg0 + ", binder=" + arg1 + ", _address=" + _address);
|
||||
binder = (ShellService.ShellServiceBinder)arg1;
|
||||
QLog.d(LOG_TAG, "onServiceConnected - BINDER_SET: binder=" + binder + ", existing_connection=" + connection);
|
||||
if (connection != null) {
|
||||
QLog.d(LOG_TAG, "onServiceConnected - REMOVING_OLD_LISTENER: connection=" + connection);
|
||||
binder.removeListener(connection, QZAdbRemote.getInstance());
|
||||
}
|
||||
QLog.d(LOG_TAG, "onServiceConnected - CONNECTING: about to call connectOrLookupConnection");
|
||||
connection = connectOrLookupConnection(_address, 5555);
|
||||
QLog.d(LOG_TAG, "onServiceConnected - END: connection=" + connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0) {
|
||||
QLog.d(LOG_TAG, "onServiceDisconnected - START: componentName=" + arg0 + ", old_binder=" + binder);
|
||||
binder = null;
|
||||
QLog.d(LOG_TAG, "onServiceDisconnected - END: binder set to null");
|
||||
}
|
||||
};
|
||||
|
||||
static public void createConnection(String ip, Context context) {
|
||||
QLog.d(LOG_TAG, "createConnection - START: ip=" + ip + ", context=" + context + ", existing_binder=" + binder);
|
||||
_address = ip;
|
||||
_context = context;
|
||||
QLog.d(LOG_TAG, "createConnection - PARAMS_SET: _address=" + _address + ", _context=" + _context);
|
||||
|
||||
/* If we have old RSA keys, just use them */
|
||||
QLog.d(LOG_TAG, "createConnection - CHECKING_CRYPTO: reading existing crypto config");
|
||||
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_CHECK: crypto=" + (crypto != null ? "exists" : "null"));
|
||||
if (crypto == null)
|
||||
{
|
||||
/* We need to make a new pair */
|
||||
QLog.i(LOG_TAG,
|
||||
Log.i(LOG_TAG,
|
||||
"This will only be done once.");
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - GENERATING_CRYPTO: synchronously generating crypto keys");
|
||||
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
|
||||
|
||||
if (crypto == null) {
|
||||
QLog.e(LOG_TAG, "Unable to generate and save RSA key pair");
|
||||
cryptoReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_GENERATED: crypto keys generated successfully");
|
||||
synchronized (cryptoLock) {
|
||||
cryptoReady = true;
|
||||
}
|
||||
} else {
|
||||
QLog.d(LOG_TAG, "createConnection - CRYPTO_EXISTS: marking crypto as ready");
|
||||
synchronized (cryptoLock) {
|
||||
cryptoReady = true;
|
||||
}
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AdbCrypto crypto;
|
||||
|
||||
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
|
||||
|
||||
if (crypto == null)
|
||||
{
|
||||
Log.e(LOG_TAG,
|
||||
"Unable to generate and save RSA key pair");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_CHECK: binder=" + (binder != null ? "exists" : "null"));
|
||||
if (binder == null) {
|
||||
QLog.i(LOG_TAG, "createConnection - STARTING_SERVICE: Starting ShellService.class");
|
||||
|
||||
service = new Intent(_context, ShellService.class);
|
||||
service.putExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_INTENT: service=" + service);
|
||||
|
||||
/* Bind the service if we're not bound already. After binding, the callback will
|
||||
* perform the initial connection. */
|
||||
QLog.d(LOG_TAG, "createConnection - BINDING_SERVICE: about to bind service");
|
||||
_context.bindService(service, QZAdbRemote.getInstance().serviceConn, Service.BIND_AUTO_CREATE);
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_BOUND: bindService called");
|
||||
|
||||
QLog.d(LOG_TAG, "createConnection - STARTING_SERVICE: SDK_INT=" + Build.VERSION.SDK_INT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
QLog.d(LOG_TAG, "createConnection - FOREGROUND_SERVICE: starting foreground service");
|
||||
_context.startForegroundService(service);
|
||||
}
|
||||
else {
|
||||
QLog.d(LOG_TAG, "createConnection - REGULAR_SERVICE: starting regular service");
|
||||
_context.startService(service);
|
||||
}
|
||||
QLog.d(LOG_TAG, "createConnection - SERVICE_STARTED: service start completed");
|
||||
} else {
|
||||
QLog.d(LOG_TAG, "createConnection - SKIP_SERVICE: binder already exists, skipping service creation");
|
||||
}
|
||||
QLog.d(LOG_TAG, "createConnection - END: method completed");
|
||||
}
|
||||
|
||||
static public void sendCommand(String command) {
|
||||
QLog.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
|
||||
Log.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
|
||||
if(ADBConnected) {
|
||||
StringBuilder commandBuffer = new StringBuilder();
|
||||
|
||||
@@ -265,7 +212,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
|
||||
/* Send it to the device */
|
||||
connection.queueCommand(commandBuffer.toString());
|
||||
} else {
|
||||
QLog.e(LOG_TAG, "sendCommand ADB is not connected!");
|
||||
Log.e(LOG_TAG, "sendCommand ADB is not connected!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -68,7 +68,7 @@ public class SDMChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
Log.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -99,7 +99,7 @@ public class SDMChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
@@ -108,7 +108,7 @@ public class SDMChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.w(TAG, "No channel available");
|
||||
Log.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -117,7 +117,7 @@ public class SDMChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
QLog.e(TAG, logString);
|
||||
Log.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
@@ -146,11 +146,11 @@ public class SDMChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
QLog.e(TAG, logString.toString());
|
||||
Log.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
|
||||
QLog.e(TAG, "ANT Command Failed");
|
||||
Log.e(TAG, "ANT Command Failed");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@@ -164,7 +164,7 @@ public class SDMChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
Log.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,20 +186,20 @@ public class SDMChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
QLog.e(TAG, "Channel Death");
|
||||
Log.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
long realtimeMillis = SystemClock.elapsedRealtime();
|
||||
double speedM_s = speed / 3.6;
|
||||
long deltaTime = (realtimeMillis - lastTime);
|
||||
@@ -208,13 +208,13 @@ public class SDMChannelController {
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
payload[0] = (byte) 0x01;
|
||||
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
|
||||
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
|
||||
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
|
||||
payload[2] = (byte) ((lastTime % 256000) / 1000);
|
||||
payload[3] = (byte) 0x00;
|
||||
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
|
||||
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
|
||||
payload[6] = (byte) stride_count++;
|
||||
payload[7] = (byte) 0; // update latency: no delay in real-time system
|
||||
payload[4] = (byte) speedM_s;
|
||||
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
|
||||
payload[6] = (byte) stride_count++; // bad but it works on zwift
|
||||
payload[7] = (byte) ((double)deltaTime * 0.03125);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
@@ -243,7 +243,7 @@ public class SDMChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -257,13 +257,13 @@ public class SDMChannelController {
|
||||
byte[] payload = new byte[8];
|
||||
|
||||
payload[0] = (byte) 0x01;
|
||||
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
|
||||
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
|
||||
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
|
||||
payload[2] = (byte) ((lastTime % 256000) / 1000);
|
||||
payload[3] = (byte) 0x00;
|
||||
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
|
||||
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
|
||||
payload[6] = (byte) stride_count++;
|
||||
payload[7] = (byte) 0; // update latency: no delay in real-time system
|
||||
payload[4] = (byte) speedM_s;
|
||||
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
|
||||
payload[6] = (byte) stride_count;
|
||||
payload[7] = (byte) ((double)deltaTime * 0.03125);
|
||||
|
||||
if (mIsOpen) {
|
||||
try {
|
||||
@@ -278,7 +278,7 @@ public class SDMChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
QLog.e(TAG, "No Device Found");
|
||||
Log.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
@@ -18,7 +18,7 @@ import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.WindowManager;
|
||||
@@ -43,7 +43,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.Point;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.os.Build;
|
||||
|
||||
public class ScreenCaptureService extends Service {
|
||||
@@ -137,7 +137,7 @@ public class ScreenCaptureService extends Service {
|
||||
int pixelStride = planes[0].getPixelStride();
|
||||
int rowStride = planes[0].getRowStride();
|
||||
int rowPadding = rowStride - pixelStride * mWidth;
|
||||
//QLog.e(TAG, "Image reviewing");
|
||||
//Log.e(TAG, "Image reviewing");
|
||||
|
||||
isRunning = true;
|
||||
|
||||
@@ -152,7 +152,7 @@ public class ScreenCaptureService extends Service {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
|
||||
|
||||
IMAGES_PRODUCED++;
|
||||
QLog.e(TAG, "captured image: " + IMAGES_PRODUCED);
|
||||
Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
|
||||
*/
|
||||
|
||||
InputImage inputImage = InputImage.fromBitmap(bitmap, 0);
|
||||
@@ -169,7 +169,7 @@ public class ScreenCaptureService extends Service {
|
||||
public void onSuccess(Text result) {
|
||||
// Task completed successfully
|
||||
|
||||
//QLog.e(TAG, "Image done!");
|
||||
//Log.e(TAG, "Image done!");
|
||||
|
||||
String resultText = result.getText();
|
||||
lastText = resultText;
|
||||
@@ -204,12 +204,12 @@ public class ScreenCaptureService extends Service {
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
// Task failed with an exception
|
||||
//QLog.e(TAG, "Image fail");
|
||||
//Log.e(TAG, "Image fail");
|
||||
isRunning = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//QLog.e(TAG, "Image ignored");
|
||||
//Log.e(TAG, "Image ignored");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -246,7 +246,7 @@ public class ScreenCaptureService extends Service {
|
||||
private class MediaProjectionStopCallback extends MediaProjection.Callback {
|
||||
@Override
|
||||
public void onStop() {
|
||||
QLog.e(TAG, "stopping projection.");
|
||||
Log.e(TAG, "stopping projection.");
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -276,12 +276,12 @@ public class ScreenCaptureService extends Service {
|
||||
if (!storeDirectory.exists()) {
|
||||
boolean success = storeDirectory.mkdirs();
|
||||
if (!success) {
|
||||
QLog.e(TAG, "failed to create file storage directory.");
|
||||
Log.e(TAG, "failed to create file storage directory.");
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
|
||||
Log.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ public class ScreenCaptureService extends Service {
|
||||
startForeground(notification.first, notification.second);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
Log.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
// start projection
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.content.IntentFilter;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.app.Service;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
@@ -35,7 +35,7 @@ public class Shortcuts {
|
||||
|
||||
List<ShortcutInfo> shortcuts = new ArrayList<>();
|
||||
|
||||
QLog.d("Shortcuts", folder);
|
||||
Log.d("Shortcuts", folder);
|
||||
File[] files = new File(folder, "profiles").listFiles();
|
||||
if (files != null) {
|
||||
for (int i = 0; i < files.length && i < 5; i++) { // Limit to 5 shortcuts
|
||||
@@ -45,7 +45,7 @@ public class Shortcuts {
|
||||
if (dotIndex > 0) { // Check if there is a dot, indicating an extension exists
|
||||
fileNameWithoutExtension = fileNameWithoutExtension.substring(0, dotIndex);
|
||||
}
|
||||
QLog.d("Shortcuts", file.getAbsolutePath());
|
||||
Log.d("Shortcuts", file.getAbsolutePath());
|
||||
Intent intent = new Intent(context, context.getClass());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra("profile_path", file.getAbsolutePath());
|
||||
@@ -74,7 +74,7 @@ public class Shortcuts {
|
||||
for (String key : extras.keySet()) {
|
||||
Object value = extras.get(key);
|
||||
if("profile_path".equals(key)) {
|
||||
QLog.d("Shortcuts", "profile_path: " + value.toString());
|
||||
Log.d("Shortcuts", "profile_path: " + value.toString());
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class Shortcuts {
|
||||
if (extras != null) {
|
||||
for (String key : extras.keySet()) {
|
||||
Object value = extras.get(key);
|
||||
QLog.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
|
||||
Log.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.dsi.ant.channel.AntChannel;
|
||||
import com.dsi.ant.channel.AntCommandFailedException;
|
||||
@@ -67,7 +67,7 @@ public class SpeedChannelController {
|
||||
boolean openChannel() {
|
||||
if (null != mAntChannel) {
|
||||
if (mIsOpen) {
|
||||
QLog.w(TAG, "Channel was already open");
|
||||
Log.w(TAG, "Channel was already open");
|
||||
} else {
|
||||
// Channel ID message contains device number, type and transmission type. In
|
||||
// order for master (TX) channels and slave (RX) channels to connect, they
|
||||
@@ -98,7 +98,7 @@ public class SpeedChannelController {
|
||||
mAntChannel.open();
|
||||
mIsOpen = true;
|
||||
|
||||
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
|
||||
} catch (RemoteException e) {
|
||||
channelError(e);
|
||||
} catch (AntCommandFailedException e) {
|
||||
@@ -107,7 +107,7 @@ public class SpeedChannelController {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QLog.w(TAG, "No channel available");
|
||||
Log.w(TAG, "No channel available");
|
||||
}
|
||||
|
||||
return mIsOpen;
|
||||
@@ -116,7 +116,7 @@ public class SpeedChannelController {
|
||||
void channelError(RemoteException e) {
|
||||
String logString = "Remote service communication failed.";
|
||||
|
||||
QLog.e(TAG, logString);
|
||||
Log.e(TAG, logString);
|
||||
}
|
||||
|
||||
void channelError(String error, AntCommandFailedException e) {
|
||||
@@ -145,11 +145,11 @@ public class SpeedChannelController {
|
||||
.append(failureReason);
|
||||
}
|
||||
|
||||
QLog.e(TAG, logString.toString());
|
||||
Log.e(TAG, logString.toString());
|
||||
|
||||
mAntChannel.release();
|
||||
|
||||
QLog.e(TAG, "ANT Command Failed");
|
||||
Log.e(TAG, "ANT Command Failed");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@@ -163,7 +163,7 @@ public class SpeedChannelController {
|
||||
mAntChannel = null;
|
||||
}
|
||||
|
||||
QLog.e(TAG, "Channel Closed");
|
||||
Log.e(TAG, "Channel Closed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,20 +185,20 @@ public class SpeedChannelController {
|
||||
@Override
|
||||
public void onChannelDeath() {
|
||||
// Display channel death message when channel dies
|
||||
QLog.e(TAG, "Channel Death");
|
||||
Log.e(TAG, "Channel Death");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
|
||||
QLog.d(TAG, "Rx: " + antParcel);
|
||||
QLog.d(TAG, "Message Type: " + messageType);
|
||||
Log.d(TAG, "Rx: " + antParcel);
|
||||
Log.d(TAG, "Message Type: " + messageType);
|
||||
|
||||
if(carousalTimer == null) {
|
||||
carousalTimer = new Timer(); // At this line a new Thread will be created
|
||||
carousalTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d(TAG, "Tx Unsollicited");
|
||||
Log.d(TAG, "Tx Unsollicited");
|
||||
long realtimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
if (lastTime != 0) {
|
||||
@@ -252,7 +252,7 @@ public class SpeedChannelController {
|
||||
// Constructing channel event message from parcel
|
||||
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
|
||||
EventCode code = eventMessage.getEventCode();
|
||||
QLog.d(TAG, "Event Code: " + code);
|
||||
Log.d(TAG, "Event Code: " + code);
|
||||
|
||||
// Switching on event code to handle the different types of channel events
|
||||
switch (code) {
|
||||
@@ -296,7 +296,7 @@ public class SpeedChannelController {
|
||||
break;
|
||||
case RX_SEARCH_TIMEOUT:
|
||||
// TODO May want to keep searching
|
||||
QLog.e(TAG, "No Device Found");
|
||||
Log.e(TAG, "No Device Found");
|
||||
break;
|
||||
case CHANNEL_CLOSED:
|
||||
case RX_FAIL:
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.content.IntentFilter;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.app.Service;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
@@ -43,16 +43,12 @@ public class Usbserial {
|
||||
static int lastReadLen = 0;
|
||||
|
||||
public static void open(Context context) {
|
||||
open(context, 2400); // Default baud rate for Computrainer
|
||||
}
|
||||
|
||||
public static void open(Context context, int baudRate) {
|
||||
QLog.d("QZ","UsbSerial open with baud rate: " + baudRate);
|
||||
Log.d("QZ","UsbSerial open");
|
||||
// Find all available drivers from attached devices.
|
||||
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
|
||||
if (availableDrivers.isEmpty()) {
|
||||
QLog.d("QZ","UsbSerial no available drivers");
|
||||
Log.d("QZ","UsbSerial no available drivers");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +58,7 @@ public class Usbserial {
|
||||
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
RingtoneManager.getRingtone(context, notification).play();
|
||||
|
||||
QLog.d("QZ","USB permission ...");
|
||||
Log.d("QZ","USB permission ...");
|
||||
final Boolean[] granted = {null};
|
||||
BroadcastReceiver usbReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@@ -89,12 +85,12 @@ public class Usbserial {
|
||||
// Do something here
|
||||
}
|
||||
}
|
||||
QLog.d("QZ","USB permission "+granted[0]);
|
||||
Log.d("QZ","USB permission "+granted[0]);
|
||||
}
|
||||
|
||||
UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
|
||||
if (connection == null) {
|
||||
QLog.d("QZ","UsbSerial no permissions");
|
||||
Log.d("QZ","UsbSerial no permissions");
|
||||
// add UsbManager.requestPermission(driver.getDevice(), ..) handling here
|
||||
return;
|
||||
}
|
||||
@@ -102,19 +98,20 @@ public class Usbserial {
|
||||
port = driver.getPorts().get(0); // Most devices have just one port (port 0)
|
||||
try {
|
||||
port.open(connection);
|
||||
port.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
|
||||
QLog.d("QZ","UsbSerial port opened successfully at " + baudRate + " baud");
|
||||
port.setParameters(2400, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
|
||||
}
|
||||
catch (IOException e) {
|
||||
QLog.d("QZ","UsbSerial port open failed: " + e.getMessage());
|
||||
// Do something here
|
||||
}
|
||||
|
||||
Log.d("QZ","UsbSerial port opened");
|
||||
}
|
||||
|
||||
public static void write (byte[] bytes) {
|
||||
if(port == null)
|
||||
return;
|
||||
|
||||
QLog.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
|
||||
Log.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
|
||||
try {
|
||||
port.write(bytes, 2000);
|
||||
}
|
||||
@@ -135,7 +132,7 @@ public class Usbserial {
|
||||
|
||||
try {
|
||||
lastReadLen = port.read(receiveData, 2000);
|
||||
QLog.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
|
||||
Log.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (IOException e) {
|
||||
// Do something here
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -33,7 +33,7 @@ public class WearableController {
|
||||
_intent = new Intent(context, WearableMessageListenerService.class);
|
||||
// FloatingWindowGFG service is started
|
||||
context.startService(_intent);
|
||||
QLog.v("WearableController", "started");
|
||||
Log.v("WearableController", "started");
|
||||
}
|
||||
|
||||
public static int getHeart() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.google.android.gms.wearable.Wearable;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.wearable.DataItemBuffer;
|
||||
import com.google.android.gms.wearable.DataMap;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.gms.common.api.Status;
|
||||
import java.io.InputStream;
|
||||
@@ -31,7 +31,7 @@ public class WearableMessageListenerService extends Service implements
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
QLog.v("WearableMessageListenerService","onCreate");
|
||||
Log.v("WearableMessageListenerService","onCreate");
|
||||
}
|
||||
|
||||
public static int getHeart() {
|
||||
@@ -55,7 +55,7 @@ public class WearableMessageListenerService extends Service implements
|
||||
mWearableClient.addListener(this);
|
||||
Wearable.getDataClient(this).addListener(this);
|
||||
|
||||
QLog.v("WearableMessageListenerService","onStartCommand");
|
||||
Log.v("WearableMessageListenerService","onStartCommand");
|
||||
|
||||
// Return START_STICKY to restart the service if it's killed by the system
|
||||
return START_STICKY;
|
||||
@@ -65,9 +65,9 @@ public class WearableMessageListenerService extends Service implements
|
||||
public void onDataChanged(DataEventBuffer dataEvents) {
|
||||
for (DataEvent event : dataEvents) {
|
||||
if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||
QLog.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
|
||||
Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
|
||||
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
|
||||
QLog.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
|
||||
Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
|
||||
if(event.getDataItem().getUri().getPath().equals("/qz")) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
@@ -78,14 +78,14 @@ public class WearableMessageListenerService extends Service implements
|
||||
heart_rate = DataMap.fromByteArray(result.get(0).getData())
|
||||
.getInt("heart_rate", 0);
|
||||
} else {
|
||||
QLog.e(TAG, "Unexpected number of DataItems found.\n"
|
||||
Log.e(TAG, "Unexpected number of DataItems found.\n"
|
||||
+ "\tExpected: 1\n"
|
||||
+ "\tActual: " + result.getCount());
|
||||
}
|
||||
} else {
|
||||
QLog.d(TAG, "onHandleIntent: failed to get current alarm state");
|
||||
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onHandleIntent: failed to get current alarm state");
|
||||
}
|
||||
QLog.d(TAG, "Heart: " + heart_rate);
|
||||
Log.d(TAG, "Heart: " + heart_rate);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -96,17 +96,17 @@ public class WearableMessageListenerService extends Service implements
|
||||
|
||||
@Override
|
||||
public void onConnected(Bundle bundle) {
|
||||
QLog.v("WearableMessageListenerService","onConnected");
|
||||
Log.v("WearableMessageListenerService","onConnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int i) {
|
||||
QLog.v("WearableMessageListenerService","onConnectionSuspended");
|
||||
Log.v("WearableMessageListenerService","onConnectionSuspended");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||
QLog.v("WearableMessageListenerService","onConnectionFailed");
|
||||
Log.v("WearableMessageListenerService","onConnectionFailed");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,8 +117,8 @@ public class WearableMessageListenerService extends Service implements
|
||||
// Handle the received message data here
|
||||
String messageData = new String(data); // Assuming it's a simple string message
|
||||
|
||||
QLog.v("Wearable", path);
|
||||
QLog.v("Wearable", messageData);
|
||||
Log.v("Wearable", path);
|
||||
Log.v("Wearable", messageData);
|
||||
|
||||
// You can then perform actions or update data in your service based on the received message
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IntentFilter;
|
||||
@@ -44,12 +44,12 @@ public class ZapClickLayer {
|
||||
}
|
||||
|
||||
public static int processCharacteristic(byte[] value) {
|
||||
QLog.d(TAG, "processCharacteristic");
|
||||
Log.d(TAG, "processCharacteristic");
|
||||
return device.processCharacteristic("QZ", value);
|
||||
}
|
||||
|
||||
public static byte[] buildHandshakeStart() {
|
||||
QLog.d(TAG, "buildHandshakeStart");
|
||||
Log.d(TAG, "buildHandshakeStart");
|
||||
return device.buildHandshakeStart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
@@ -49,17 +49,17 @@ public class ZwiftAPI {
|
||||
// Ora puoi usare 'message' come un oggetto normale
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
// Gestisci l'eccezione se il messaggio non può essere parsato
|
||||
QLog.e(TAG, e.toString());
|
||||
Log.e(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static float getAltitude() {
|
||||
QLog.d(TAG, "getAltitude " + playerState.getAltitude());
|
||||
Log.d(TAG, "getAltitude " + playerState.getAltitude());
|
||||
return playerState.getAltitude();
|
||||
}
|
||||
|
||||
public static float getDistance() {
|
||||
QLog.d(TAG, "getDistance " + playerState.getDistance());
|
||||
Log.d(TAG, "getDistance " + playerState.getDistance());
|
||||
return playerState.getDistance();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
import com.garmin.android.connectiq.ConnectIQ;
|
||||
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
|
||||
import com.garmin.android.connectiq.IQApp;
|
||||
|
||||
@@ -9,7 +9,6 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
/**
|
||||
* This class represents an ADB connection.
|
||||
@@ -125,13 +124,10 @@ public class AdbConnection implements Closeable {
|
||||
try {
|
||||
/* Read and parse a message off the socket's input stream */
|
||||
AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
|
||||
QLog.d("AdbConnection", "connectionThread - Received packet: command=0x" + Integer.toHexString(msg.command) + ", arg0=" + msg.arg0 + ", arg1=" + msg.arg1);
|
||||
|
||||
/* Verify magic and checksum */
|
||||
if (!AdbProtocol.validateMessage(msg)) {
|
||||
QLog.w("AdbConnection", "connectionThread - Invalid message, dropping packet");
|
||||
if (!AdbProtocol.validateMessage(msg))
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (msg.command)
|
||||
{
|
||||
@@ -179,25 +175,21 @@ public class AdbConnection implements Closeable {
|
||||
break;
|
||||
|
||||
case AdbProtocol.CMD_AUTH:
|
||||
QLog.d("AdbConnection", "connectionThread - Received AUTH packet, type=" + msg.arg0);
|
||||
|
||||
byte[] packet;
|
||||
|
||||
if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN)
|
||||
{
|
||||
/* This is an authentication challenge */
|
||||
QLog.d("AdbConnection", "connectionThread - AUTH_TYPE_TOKEN challenge, sentSignature=" + conn.sentSignature);
|
||||
if (conn.sentSignature)
|
||||
{
|
||||
/* We've already tried our signature, so send our public key */
|
||||
QLog.d("AdbConnection", "connectionThread - Sending RSA public key");
|
||||
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
|
||||
conn.crypto.getAdbPublicKeyPayload());
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We'll sign the token */
|
||||
QLog.d("AdbConnection", "connectionThread - Signing token with private key");
|
||||
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
|
||||
conn.crypto.signAdbTokenPayload(msg.payload));
|
||||
conn.sentSignature = true;
|
||||
@@ -206,22 +198,16 @@ public class AdbConnection implements Closeable {
|
||||
/* Write the AUTH reply */
|
||||
conn.outputStream.write(packet);
|
||||
conn.outputStream.flush();
|
||||
QLog.d("AdbConnection", "connectionThread - AUTH response sent");
|
||||
}
|
||||
else {
|
||||
QLog.w("AdbConnection", "connectionThread - Unhandled AUTH type: " + msg.arg0);
|
||||
}
|
||||
break;
|
||||
|
||||
case AdbProtocol.CMD_CNXN:
|
||||
QLog.d("AdbConnection", "connectionThread - Received CNXN packet! maxData=" + msg.arg1);
|
||||
synchronized (conn) {
|
||||
/* We need to store the max data size */
|
||||
conn.maxData = msg.arg1;
|
||||
|
||||
/* Mark us as connected and unwait anyone waiting on the connection */
|
||||
conn.connected = true;
|
||||
QLog.d("AdbConnection", "connectionThread - Connection established! Notifying waiting threads");
|
||||
conn.notifyAll();
|
||||
}
|
||||
break;
|
||||
@@ -233,7 +219,6 @@ public class AdbConnection implements Closeable {
|
||||
} catch (Exception e) {
|
||||
/* The cleanup is taken care of by a combination of this thread
|
||||
* and close() */
|
||||
QLog.e("AdbConnection", "connectionThread - Exception in connection thread: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -285,32 +270,23 @@ public class AdbConnection implements Closeable {
|
||||
if (connected)
|
||||
throw new IllegalStateException("Already connected");
|
||||
|
||||
QLog.d("AdbConnection", "connect() - Starting ADB connection");
|
||||
|
||||
/* Write the CONNECT packet */
|
||||
outputStream.write(AdbProtocol.generateConnect());
|
||||
outputStream.flush();
|
||||
QLog.d("AdbConnection", "connect() - CONNECT packet sent, starting connection thread");
|
||||
|
||||
/* Start the connection thread to respond to the peer */
|
||||
connectAttempted = true;
|
||||
connectionThread.start();
|
||||
QLog.d("AdbConnection", "connect() - Connection thread started, waiting for connection...");
|
||||
|
||||
/* Wait for the connection to go live */
|
||||
synchronized (this) {
|
||||
if (!connected) {
|
||||
QLog.d("AdbConnection", "connect() - Waiting for connection to complete...");
|
||||
if (!connected)
|
||||
wait();
|
||||
QLog.d("AdbConnection", "connect() - Wait completed, connected=" + connected);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
QLog.e("AdbConnection", "connect() - Connection failed after wait");
|
||||
throw new IOException("Connection failed");
|
||||
}
|
||||
}
|
||||
QLog.d("AdbConnection", "connect() - Successfully connected!");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.cgutman.adblib.AdbConnection;
|
||||
import com.cgutman.adblib.AdbCrypto;
|
||||
import com.cgutman.adblib.AdbStream;
|
||||
import com.cgutman.androidremotedebugger.AdbUtils;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
|
||||
public class DeviceConnection implements Closeable {
|
||||
private static final int CONN_TIMEOUT = 5000;
|
||||
@@ -60,58 +59,42 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
public void startConnect() {
|
||||
QLog.d("DeviceConnection", "startConnect - START: host=" + host + ", port=" + port + ", listener=" + listener);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("DeviceConnection", "startConnect.run - THREAD_START: host=" + host + ", port=" + port);
|
||||
boolean connected = false;
|
||||
Socket socket = new Socket();
|
||||
AdbCrypto crypto;
|
||||
|
||||
/* Load the crypto config */
|
||||
QLog.d("DeviceConnection", "startConnect.run - LOADING_CRYPTO: calling loadAdbCrypto");
|
||||
crypto = listener.loadAdbCrypto(DeviceConnection.this);
|
||||
if (crypto == null) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - CRYPTO_FAILED: crypto is null, returning");
|
||||
return;
|
||||
}
|
||||
QLog.d("DeviceConnection", "startConnect.run - CRYPTO_LOADED: crypto=" + crypto);
|
||||
|
||||
try {
|
||||
/* Establish a connect to the remote host */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECT: connecting to " + host + ":" + port + " with timeout=" + CONN_TIMEOUT);
|
||||
socket.connect(new InetSocketAddress(host, port), CONN_TIMEOUT);
|
||||
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECTED: socket connected successfully");
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - SOCKET_FAILED: connection failed", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/* Establish the application layer connection */
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTION: creating AdbConnection");
|
||||
connection = AdbConnection.create(socket, crypto);
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECT: calling connection.connect()");
|
||||
connection.connect();
|
||||
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTED: ADB connection established");
|
||||
|
||||
/* Open the shell stream */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SHELL_STREAM: opening shell stream");
|
||||
shellStream = connection.open("shell:");
|
||||
QLog.d("DeviceConnection", "startConnect.run - SHELL_OPENED: shell stream opened successfully");
|
||||
connected = true;
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - ADB_IO_ERROR: IOException during ADB connection", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
} catch (InterruptedException e) {
|
||||
QLog.e("DeviceConnection", "startConnect.run - ADB_INTERRUPTED: InterruptedException during ADB connection", e);
|
||||
listener.notifyConnectionFailed(DeviceConnection.this, e);
|
||||
} finally {
|
||||
/* Cleanup if the connection failed */
|
||||
if (!connected) {
|
||||
QLog.d("DeviceConnection", "startConnect.run - CLEANUP: connection failed, cleaning up");
|
||||
AdbUtils.safeClose(shellStream);
|
||||
|
||||
/* The AdbConnection object will close the underlying socket
|
||||
@@ -129,16 +112,12 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
/* Notify the listener that the connection is complete */
|
||||
QLog.d("DeviceConnection", "startConnect.run - NOTIFY_SUCCESS: calling listener.notifyConnectionEstablished");
|
||||
listener.notifyConnectionEstablished(DeviceConnection.this);
|
||||
QLog.d("DeviceConnection", "startConnect.run - NOTIFIED: notifyConnectionEstablished called");
|
||||
|
||||
/* Start the receive thread */
|
||||
QLog.d("DeviceConnection", "startConnect.run - START_RECEIVE: starting receive thread");
|
||||
startReceiveThread();
|
||||
|
||||
/* Enter the blocking send loop */
|
||||
QLog.d("DeviceConnection", "startConnect.run - SEND_LOOP: entering send loop");
|
||||
sendLoop();
|
||||
}
|
||||
}).start();
|
||||
@@ -169,32 +148,23 @@ public class DeviceConnection implements Closeable {
|
||||
}
|
||||
|
||||
private void startReceiveThread() {
|
||||
QLog.d("DeviceConnection", "startReceiveThread - START: creating receive thread");
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - THREAD_START: receive thread started");
|
||||
try {
|
||||
while (!shellStream.isClosed()) {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - READING: waiting for data from shellStream");
|
||||
byte[] data = shellStream.read();
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - DATA_RECEIVED: " + data.length + " bytes received");
|
||||
listener.receivedData(DeviceConnection.this, data, 0, data.length);
|
||||
}
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - STREAM_CLOSED: shellStream is closed");
|
||||
listener.notifyStreamClosed(DeviceConnection.this);
|
||||
} catch (IOException e) {
|
||||
QLog.e("DeviceConnection", "startReceiveThread.run - IO_ERROR: IOException in receive thread", e);
|
||||
listener.notifyStreamFailed(DeviceConnection.this, e);
|
||||
} catch (InterruptedException e) {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - INTERRUPTED: receive thread interrupted");
|
||||
} finally {
|
||||
QLog.d("DeviceConnection", "startReceiveThread.run - CLEANUP: cleaning up receive thread");
|
||||
AdbUtils.safeClose(DeviceConnection.this);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
QLog.d("DeviceConnection", "startReceiveThread - END: receive thread started");
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
public class ShellService extends Service implements DeviceConnectionListener {
|
||||
|
||||
@@ -48,20 +48,14 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
|
||||
public class ShellServiceBinder extends Binder {
|
||||
public DeviceConnection createConnection(String host, int port) {
|
||||
QLog.d("ShellService", "createConnection - START: host=" + host + ", port=" + port + ", listener=" + listener);
|
||||
DeviceConnection conn = new DeviceConnection(listener, host, port);
|
||||
QLog.d("ShellService", "createConnection - CONNECTION_CREATED: conn=" + conn);
|
||||
listener.addListener(conn, ShellService.this);
|
||||
QLog.d("ShellService", "createConnection - LISTENER_ADDED: returning conn=" + conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public DeviceConnection findConnection(String host, int port) {
|
||||
String connStr = host+":"+port;
|
||||
QLog.d("ShellService", "findConnection - SEARCH: connStr=" + connStr + ", mapSize=" + currentConnectionMap.size());
|
||||
DeviceConnection found = currentConnectionMap.get(connStr);
|
||||
QLog.d("ShellService", "findConnection - RESULT: found=" + (found != null ? "exists" : "null"));
|
||||
return found;
|
||||
return currentConnectionMap.get(connStr);
|
||||
}
|
||||
|
||||
public void notifyPausingActivity(DeviceConnection devConn) {
|
||||
@@ -82,95 +76,68 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
}
|
||||
|
||||
public void addListener(DeviceConnection conn, DeviceConnectionListener listener) {
|
||||
QLog.d("ShellService", "addListener - START: conn=" + conn + ", listener=" + listener);
|
||||
ShellService.this.listener.addListener(conn, listener);
|
||||
QLog.d("ShellService", "addListener - END: listener added");
|
||||
}
|
||||
|
||||
public void removeListener(DeviceConnection conn, DeviceConnectionListener listener) {
|
||||
QLog.d("ShellService", "removeListener - START: conn=" + conn + ", listener=" + listener);
|
||||
ShellService.this.listener.removeListener(conn, listener);
|
||||
QLog.d("ShellService", "removeListener - END: listener removed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent arg0) {
|
||||
QLog.d("ShellService", "onBind - START: intent=" + arg0 + ", binder=" + binder);
|
||||
QLog.d("ShellService", "onBind - END: returning binder");
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
QLog.d("ShellService", "onUnbind - START: intent=" + intent + ", connections=" + currentConnectionMap.size());
|
||||
/* Stop the service if no connections remain */
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "onUnbind - STOPPING_SERVICE: no connections remain");
|
||||
stopSelf();
|
||||
} else {
|
||||
QLog.d("ShellService", "onUnbind - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
|
||||
}
|
||||
|
||||
QLog.d("ShellService", "onUnbind - END: returning false");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
QLog.d("ShellService", "onStartCommand - START: intent=" + intent + ", flags=" + flags + ", startId=" + startId + ", foregroundId=" + foregroundId);
|
||||
if (foregroundId == 0) {
|
||||
try {
|
||||
int serviceType = intent.getIntExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
// If we're not already running in the foreground, use a placeholder
|
||||
// notification until a real connection is established. After connection
|
||||
// establishment, the real notification will replace this one.
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_START: serviceType=" + serviceType + ", SDK_INT=" + Build.VERSION.SDK_INT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_Q+: starting with service type");
|
||||
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification(), serviceType);
|
||||
} else {
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_LEGACY: starting without service type");
|
||||
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification());
|
||||
}
|
||||
QLog.d("ShellService", "onStartCommand - FOREGROUND_SUCCESS: foreground service started");
|
||||
} catch (Exception e) {
|
||||
QLog.e("ForegroundService", "Failed to start foreground service", e);
|
||||
Log.e("ForegroundService", "Failed to start foreground service", e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
} else {
|
||||
QLog.d("ShellService", "onStartCommand - SKIP_FOREGROUND: already running in foreground with id=" + foregroundId);
|
||||
}
|
||||
|
||||
// Don't restart if we've been killed. We will have already lost our connections
|
||||
// when we died, so we'll just be running doing nothing if the OS restarted us.
|
||||
QLog.d("ShellService", "onStartCommand - END: returning START_NOT_STICKY");
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
QLog.d("ShellService", "onCreate - START: initializing service");
|
||||
super.onCreate();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: creating notification channel");
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Connection Info", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: channel created");
|
||||
}
|
||||
|
||||
QLog.d("ShellService", "onCreate - WIFI_LOCK: creating wifi lock");
|
||||
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
wlanLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, "RemoteADBShell:ShellService");
|
||||
QLog.d("ShellService", "onCreate - WIFI_LOCK: wlanLock=" + wlanLock);
|
||||
|
||||
QLog.d("ShellService", "onCreate - WAKE_LOCK: creating wake lock");
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "RemoteADBShell:ShellService");
|
||||
QLog.d("ShellService", "onCreate - WAKE_LOCK: wakeLock=" + wakeLock);
|
||||
QLog.d("ShellService", "onCreate - END: service initialization complete");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -281,76 +248,44 @@ public class ShellService extends Service implements DeviceConnectionListener {
|
||||
}
|
||||
|
||||
private synchronized void addNewConnection(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "addNewConnection - START: devConn=" + devConn + ", currentSize=" + currentConnectionMap.size());
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "addNewConnection - ACQUIRING_LOCKS: first connection, acquiring locks");
|
||||
wakeLock.acquire();
|
||||
wlanLock.acquire();
|
||||
QLog.d("ShellService", "addNewConnection - LOCKS_ACQUIRED: wakeLock and wlanLock acquired");
|
||||
}
|
||||
|
||||
String connString = getConnectionString(devConn);
|
||||
QLog.d("ShellService", "addNewConnection - ADDING: connString=" + connString);
|
||||
currentConnectionMap.put(connString, devConn);
|
||||
QLog.d("ShellService", "addNewConnection - END: connection added, newSize=" + currentConnectionMap.size());
|
||||
currentConnectionMap.put(getConnectionString(devConn), devConn);
|
||||
}
|
||||
|
||||
private synchronized void removeConnection(DeviceConnection devConn) {
|
||||
String connString = getConnectionString(devConn);
|
||||
QLog.d("ShellService", "removeConnection - START: devConn=" + devConn + ", connString=" + connString + ", currentSize=" + currentConnectionMap.size());
|
||||
currentConnectionMap.remove(connString);
|
||||
QLog.d("ShellService", "removeConnection - REMOVED: newSize=" + currentConnectionMap.size());
|
||||
currentConnectionMap.remove(getConnectionString(devConn));
|
||||
|
||||
/* Stop the service if no connections remain */
|
||||
if (currentConnectionMap.isEmpty()) {
|
||||
QLog.d("ShellService", "removeConnection - STOPPING_SERVICE: no connections remain");
|
||||
stopSelf();
|
||||
} else {
|
||||
QLog.d("ShellService", "removeConnection - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
|
||||
}
|
||||
QLog.d("ShellService", "removeConnection - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionEstablished(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
addNewConnection(devConn);
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - CONNECTION_ADDED: updating notification");
|
||||
updateNotification(devConn, true);
|
||||
QLog.d("ShellService", "notifyConnectionEstablished - END: connection established successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d("ShellService", "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
QLog.e("ShellService", "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
|
||||
if (e != null) {
|
||||
QLog.e("ShellService", "notifyConnectionFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
/* No notification is displaying here */
|
||||
QLog.d("ShellService", "notifyConnectionFailed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
|
||||
QLog.d("ShellService", "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
QLog.e("ShellService", "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
|
||||
if (e != null) {
|
||||
QLog.e("ShellService", "notifyStreamFailed - STACK_TRACE: ", e);
|
||||
}
|
||||
updateNotification(devConn, false);
|
||||
QLog.d("ShellService", "notifyStreamFailed - NOTIFICATION_UPDATED: removing connection");
|
||||
removeConnection(devConn);
|
||||
QLog.d("ShellService", "notifyStreamFailed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyStreamClosed(DeviceConnection devConn) {
|
||||
QLog.d("ShellService", "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
|
||||
updateNotification(devConn, false);
|
||||
QLog.d("ShellService", "notifyStreamClosed - NOTIFICATION_UPDATED: removing connection");
|
||||
removeConnection(devConn);
|
||||
QLog.d("ShellService", "notifyStreamClosed - END");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -55,7 +55,7 @@ import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
@@ -65,16 +65,13 @@ import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.ConsumeParams;
|
||||
import com.android.billingclient.api.ConsumeResponseListener;
|
||||
import com.android.billingclient.api.PendingPurchasesParams;
|
||||
import com.android.billingclient.api.ProductDetails;
|
||||
import com.android.billingclient.api.ProductDetailsResponseListener;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.Purchase.PurchaseState;
|
||||
import com.android.billingclient.api.PurchasesResponseListener;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.QueryProductDetailsParams;
|
||||
import com.android.billingclient.api.QueryPurchasesParams;
|
||||
import com.android.billingclient.api.QueryProductDetailsResult;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
@@ -82,7 +79,7 @@ import com.android.billingclient.api.QueryProductDetailsResult;
|
||||
** Add Dependencies below to build.gradle file:
|
||||
|
||||
dependencies {
|
||||
def billing_version = "8.0.0"
|
||||
def billing_version = "4.0.0"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
}
|
||||
|
||||
@@ -100,8 +97,8 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
|
||||
public static final int RESULT_OK = BillingClient.BillingResponseCode.OK;
|
||||
public static final int RESULT_USER_CANCELED = BillingClient.BillingResponseCode.USER_CANCELED;
|
||||
public static final String TYPE_INAPP = BillingClient.ProductType.INAPP;
|
||||
public static final String TYPE_SUBS = BillingClient.ProductType.SUBS;
|
||||
public static final String TYPE_INAPP = BillingClient.SkuType.INAPP;
|
||||
public static final String TYPE_SUBS = BillingClient.SkuType.SUBS;
|
||||
public static final String TAG = "InAppPurchase";
|
||||
|
||||
// Should be in sync with InAppTransaction::FailureReason
|
||||
@@ -122,28 +119,25 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
|
||||
public void initializeConnection(){
|
||||
QLog.w(TAG, "initializeConnection start");
|
||||
PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder()
|
||||
.enableOneTimeProducts()
|
||||
.build();
|
||||
Log.w(TAG, "initializeConnection start");
|
||||
billingClient = BillingClient.newBuilder(m_context)
|
||||
.enablePendingPurchases(pendingPurchasesParams)
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
billingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||
QLog.w(TAG, "onBillingSetupFinished");
|
||||
Log.w(TAG, "onBillingSetupFinished");
|
||||
if (billingResult.getResponseCode() == RESULT_OK) {
|
||||
purchasedProductsQueried(m_nativePointer);
|
||||
} else {
|
||||
QLog.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
|
||||
Log.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
QLog.w(TAG, "Billing service disconnected");
|
||||
Log.w(TAG, "Billing service disconnected");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -152,23 +146,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
|
||||
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
QLog.d(TAG, "onPurchasesUpdated called. Response code: " + responseCode + ", Debug message: " + billingResult.getDebugMessage());
|
||||
|
||||
if (purchases == null) {
|
||||
QLog.e(TAG, "Purchase failed: Data missing from result (purchases is null)");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
|
||||
return;
|
||||
}
|
||||
|
||||
if (billingResult.getResponseCode() == RESULT_OK) {
|
||||
QLog.d(TAG, "Purchase successful, handling " + purchases.size() + " purchases");
|
||||
handlePurchase(purchases);
|
||||
} else if (responseCode == RESULT_USER_CANCELED) {
|
||||
QLog.d(TAG, "Purchase cancelled by user");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
|
||||
} else {
|
||||
String errorString = getErrorString(responseCode);
|
||||
QLog.e(TAG, "Purchase failed with error: " + errorString + " (code: " + responseCode + ")");
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +191,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult)
|
||||
{
|
||||
QLog.d(TAG, "Purchase acknowledged ");
|
||||
Log.d(TAG, "Purchase acknowledged ");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -210,9 +199,9 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
|
||||
public void queryDetails(final String[] productIds) {
|
||||
QLog.d(TAG, "queryDetails: start");
|
||||
Log.d(TAG, "queryDetails: start");
|
||||
int index = 0;
|
||||
QLog.d(TAG, "queryDetails: productIds.length " + productIds.length);
|
||||
Log.d(TAG, "queryDetails: productIds.length " + productIds.length);
|
||||
while (index < productIds.length) {
|
||||
List<String> productIdList = new ArrayList<>();
|
||||
for (int i = index; i < Math.min(index + 20, productIds.length); ++i) {
|
||||
@@ -220,44 +209,31 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
}
|
||||
index += productIdList.size();
|
||||
|
||||
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
|
||||
for (String productId : productIdList) {
|
||||
productList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(productId)
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build());
|
||||
}
|
||||
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productList)
|
||||
.build();
|
||||
billingClient.queryProductDetailsAsync(params,
|
||||
(billingResult, productDetailsResult) -> {
|
||||
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(productIdList).setType(TYPE_SUBS);
|
||||
billingClient.querySkuDetailsAsync(params.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
|
||||
Log.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
|
||||
|
||||
if (responseCode != RESULT_OK) {
|
||||
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
|
||||
Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
|
||||
return;
|
||||
}
|
||||
if (productDetailsList == null || productDetailsList.isEmpty()) {
|
||||
QLog.e(TAG, "queryDetails: No details list in response.");
|
||||
if (skuDetailsList == null) {
|
||||
Log.e(TAG, "queryDetails: No details list in response.");
|
||||
return;
|
||||
}
|
||||
|
||||
QLog.d(TAG, "onProductDetailsResponse: productDetailsList " + productDetailsList);
|
||||
for (ProductDetails productDetails : productDetailsList) {
|
||||
Log.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
|
||||
for (SkuDetails skuDetails : skuDetailsList) {
|
||||
try {
|
||||
String queriedProductId = productDetails.getProductId();
|
||||
String queriedPrice = "";
|
||||
String queriedTitle = productDetails.getTitle();
|
||||
String queriedDescription = productDetails.getDescription();
|
||||
|
||||
// Get price from subscription offer details
|
||||
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
|
||||
queriedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
|
||||
}
|
||||
String queriedProductId = skuDetails.getSku();
|
||||
String queriedPrice = skuDetails.getPrice();
|
||||
String queriedTitle = skuDetails.getTitle();
|
||||
String queriedDescription = skuDetails.getDescription();
|
||||
registerProduct(m_nativePointer,
|
||||
queriedProductId,
|
||||
queriedPrice,
|
||||
@@ -267,6 +243,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -278,51 +255,33 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
public void launchBillingFlow(String identifier, final int requestCode){
|
||||
|
||||
purchaseRequestCode = requestCode;
|
||||
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
|
||||
productList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(identifier)
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build());
|
||||
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productList)
|
||||
.build();
|
||||
billingClient.queryProductDetailsAsync(params,
|
||||
(billingResult, productDetailsResult) -> {
|
||||
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
|
||||
List<String> skuList = new ArrayList<>();
|
||||
skuList.add(identifier);
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(skuList).setType(TYPE_SUBS);
|
||||
billingClient.querySkuDetailsAsync(params.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
|
||||
if (billingResult.getResponseCode() != RESULT_OK) {
|
||||
QLog.e(TAG, "Unable to launch Google Play purchase screen. Response code: " + billingResult.getResponseCode() + ", Debug message: " + billingResult.getDebugMessage());
|
||||
Log.e(TAG, "Unable to launch Google Play purchase screen");
|
||||
String errorString = getErrorString(requestCode);
|
||||
purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
|
||||
return;
|
||||
}
|
||||
else if (productDetailsList == null || productDetailsList.isEmpty()){
|
||||
else if (skuDetailsList == null){
|
||||
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
|
||||
return;
|
||||
}
|
||||
|
||||
ProductDetails productDetails = productDetailsList.get(0);
|
||||
BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails);
|
||||
|
||||
// For subscriptions, we need to set the offer token
|
||||
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
|
||||
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
|
||||
QLog.d(TAG, "Setting offer token for subscription: " + offerToken);
|
||||
productDetailsParamsBuilder.setOfferToken(offerToken);
|
||||
} else {
|
||||
QLog.w(TAG, "No subscription offer details found for product: " + identifier);
|
||||
}
|
||||
|
||||
BillingFlowParams.ProductDetailsParams productDetailsParams = productDetailsParamsBuilder.build();
|
||||
|
||||
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(java.util.Arrays.asList(productDetailsParams))
|
||||
.setSkuDetails(skuDetailsList.get(0))
|
||||
.build();
|
||||
|
||||
//Results will be delivered to onPurchasesUpdated
|
||||
billingClient.launchBillingFlow((Activity) m_context, purchaseParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,7 +291,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||
if (billingResult.getResponseCode() != RESULT_OK) {
|
||||
QLog.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
|
||||
Log.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -353,7 +312,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() != RESULT_OK){
|
||||
QLog.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
|
||||
Log.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -362,21 +321,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
|
||||
|
||||
public void queryPurchasedProducts(final List<String> productIdList) {
|
||||
|
||||
QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder()
|
||||
.setProductType(TYPE_SUBS)
|
||||
.build();
|
||||
billingClient.queryPurchasesAsync(queryPurchasesParams, new PurchasesResponseListener() {
|
||||
billingClient.queryPurchasesAsync(TYPE_INAPP, new PurchasesResponseListener() {
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> list) {
|
||||
for (Purchase purchase : list) {
|
||||
|
||||
if (productIdList.contains(purchase.getProducts().get(0))) {
|
||||
if (productIdList.contains(purchase.getSkus().get(0))) {
|
||||
registerPurchased(m_nativePointer,
|
||||
purchase.getProducts().get(0),
|
||||
purchase.getSkus().get(0),
|
||||
purchase.getSignature(),
|
||||
purchase.getOriginalJson(),
|
||||
purchase.getPurchaseToken(),
|
||||
"", // getDeveloperPayload() is deprecated
|
||||
purchase.getDeveloperPayload(),
|
||||
purchase.getPurchaseTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package org.qtproject.qt.android.purchasing;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import org.cagnulen.qdomyoszwift.QLog;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -58,7 +58,7 @@ public class Security {
|
||||
*/
|
||||
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
|
||||
if (signedData == null) {
|
||||
QLog.e(TAG, "data is null");
|
||||
Log.e(TAG, "data is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class Security {
|
||||
PublicKey key = Security.generatePublicKey(base64PublicKey);
|
||||
verified = Security.verify(key, signedData, signature);
|
||||
if (!verified) {
|
||||
QLog.w(TAG, "signature does not match data.");
|
||||
Log.w(TAG, "signature does not match data.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,10 @@ public class Security {
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
QLog.e(TAG, "Invalid key specification.");
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (Base64DecoderException e) {
|
||||
QLog.e(TAG, "Base64 decoding failed.");
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
@@ -113,18 +113,18 @@ public class Security {
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(signedData.getBytes());
|
||||
if (!sig.verify(Base64.decode(signature))) {
|
||||
QLog.e(TAG, "Signature verification failed.");
|
||||
Log.e(TAG, "Signature verification failed.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
QLog.e(TAG, "NoSuchAlgorithmException.");
|
||||
Log.e(TAG, "NoSuchAlgorithmException.");
|
||||
} catch (InvalidKeyException e) {
|
||||
QLog.e(TAG, "Invalid key specification.");
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
} catch (SignatureException e) {
|
||||
QLog.e(TAG, "Signature exception.");
|
||||
Log.e(TAG, "Signature exception.");
|
||||
} catch (Base64DecoderException e) {
|
||||
QLog.e(TAG, "Base64 decoding failed.");
|
||||
Log.e(TAG, "Base64 decoding failed.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#include <QDebug>
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QAndroidJniObject>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_QLog_sendToQt(JNIEnv *env, jclass clazz,
|
||||
jint level, jstring tag, jstring message) {
|
||||
const char *tagChars = env->GetStringUTFChars(tag, nullptr);
|
||||
const char *msgChars = env->GetStringUTFChars(message, nullptr);
|
||||
|
||||
QString tagStr = QString::fromUtf8(tagChars);
|
||||
QString msgStr = QString::fromUtf8(msgChars);
|
||||
|
||||
// Converti i livelli di log Android in livelli Qt
|
||||
switch (level) {
|
||||
case 2: // VERBOSE
|
||||
qDebug() << "[VERBOSE:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 3: // DEBUG
|
||||
qDebug() << "[DEBUG:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 4: // INFO
|
||||
qInfo() << "[INFO:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 5: // WARN
|
||||
qWarning() << "[WARN:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 6: // ERROR
|
||||
qCritical() << "[ERROR:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
case 7: // ASSERT/WTF
|
||||
qCritical() << "[ASSERT:" << tagStr << "]" << msgStr;
|
||||
break;
|
||||
default:
|
||||
qDebug() << "[LOG:" << tagStr << "(" << level << ")]" << msgStr;
|
||||
}
|
||||
|
||||
env->ReleaseStringUTFChars(tag, tagChars);
|
||||
env->ReleaseStringUTFChars(message, msgChars);
|
||||
}
|
||||
#endif
|
||||
@@ -1,64 +0,0 @@
|
||||
#include "androidstatusbar.h"
|
||||
#include <QQmlEngine>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QtAndroid>
|
||||
#include <QAndroidJniEnvironment>
|
||||
#endif
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::m_instance = nullptr;
|
||||
|
||||
AndroidStatusBar::AndroidStatusBar(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_instance = this;
|
||||
}
|
||||
|
||||
AndroidStatusBar* AndroidStatusBar::instance()
|
||||
{
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void AndroidStatusBar::registerQmlType()
|
||||
{
|
||||
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
|
||||
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
|
||||
Q_UNUSED(engine)
|
||||
Q_UNUSED(scriptEngine)
|
||||
return new AndroidStatusBar();
|
||||
});
|
||||
}
|
||||
|
||||
int AndroidStatusBar::apiLevel() const
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
return QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/CustomQtActivity", "getApiLevel", "()I");
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
|
||||
{
|
||||
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
|
||||
m_top = top;
|
||||
m_bottom = bottom;
|
||||
m_left = left;
|
||||
m_right = right;
|
||||
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
|
||||
emit insetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// JNI method with standard naming convention
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
if (AndroidStatusBar::instance()) {
|
||||
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef ANDROIDSTATUSBAR_H
|
||||
#define ANDROIDSTATUSBAR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class AndroidStatusBar : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int height READ height NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
|
||||
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit AndroidStatusBar(QObject *parent = nullptr);
|
||||
|
||||
static void registerQmlType();
|
||||
static AndroidStatusBar* instance();
|
||||
|
||||
int height() const { return m_top; }
|
||||
int navigationBarHeight() const { return m_bottom; }
|
||||
int leftInset() const { return m_left; }
|
||||
int rightInset() const { return m_right; }
|
||||
int apiLevel() const;
|
||||
|
||||
public slots:
|
||||
void onInsetsChanged(int top, int bottom, int left, int right);
|
||||
|
||||
signals:
|
||||
void insetsChanged();
|
||||
|
||||
private:
|
||||
int m_top = 0;
|
||||
int m_bottom = 0;
|
||||
int m_left = 0;
|
||||
int m_right = 0;
|
||||
|
||||
static AndroidStatusBar* m_instance;
|
||||
};
|
||||
|
||||
#endif // ANDROIDSTATUSBAR_H
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef BLUETOOTHDEVICETYPE_H
|
||||
#define BLUETOOTHDEVICETYPE_H
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
|
||||
#endif // BLUETOOTHDEVICETYPE_H
|
||||
@@ -1,23 +0,0 @@
|
||||
#include "characteristicnotifier0002.h"
|
||||
#include "bike.h"
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
|
||||
CharacteristicNotifier0002::CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent)
|
||||
: CharacteristicNotifier(0x0002, parent) {
|
||||
Bike = bike;
|
||||
answerList = QList<QByteArray>(); // Initialize empty list
|
||||
}
|
||||
|
||||
void CharacteristicNotifier0002::addAnswer(const QByteArray &newAnswer) {
|
||||
answerList.append(newAnswer);
|
||||
}
|
||||
|
||||
int CharacteristicNotifier0002::notify(QByteArray &value) {
|
||||
if(!answerList.isEmpty()) {
|
||||
value.append(answerList.first()); // Get first item
|
||||
answerList.removeFirst(); // Remove it from list
|
||||
return CN_OK;
|
||||
}
|
||||
return CN_INVALID;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef CHARACTERISTICNOTIFIER0002_H
|
||||
#define CHARACTERISTICNOTIFIER0002_H
|
||||
|
||||
#include "bluetoothdevice.h"
|
||||
#include "characteristicnotifier.h"
|
||||
#include <QList>
|
||||
|
||||
class CharacteristicNotifier0002 : public CharacteristicNotifier {
|
||||
Q_OBJECT
|
||||
bluetoothdevice* Bike = nullptr;
|
||||
QList<QByteArray> answerList;
|
||||
|
||||
public:
|
||||
explicit CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent = nullptr);
|
||||
int notify(QByteArray &value) override;
|
||||
void addAnswer(const QByteArray &newAnswer);
|
||||
};
|
||||
|
||||
#endif // CHARACTERISTICNOTIFIER0002_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user