mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a48171541 | ||
|
|
9b6fd7aa57 | ||
|
|
5d7062171d |
@@ -1,45 +0,0 @@
|
|||||||
# CAPod - Companion for AirPods
|
|
||||||
|
|
||||||
Android app that detects and monitors AirPods via Bluetooth LE. Displays battery levels, triggers popup notifications on case open, and provides home screen widgets.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
| Module | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `app/` | Main Android app (FOSS and Google Play flavors) |
|
|
||||||
| `app-common/` | Shared code between phone and Wear OS apps |
|
|
||||||
|
|
||||||
## Build Flavors
|
|
||||||
|
|
||||||
- **FOSS** (`foss`): Open-source, no Google Play dependencies
|
|
||||||
- **Google Play** (`gplay`): Includes billing client for IAP
|
|
||||||
|
|
||||||
Quick build check: `./gradlew assembleFossDebug`
|
|
||||||
|
|
||||||
## Key Locations
|
|
||||||
|
|
||||||
| Path | Contains |
|
|
||||||
|------|----------|
|
|
||||||
| `app/src/main/java/` | Main app source (activities, fragments, services) |
|
|
||||||
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
|
|
||||||
| `app/src/main/res/` | Layouts, drawables, strings |
|
|
||||||
| `app-common/src/test/` | Unit tests |
|
|
||||||
| `app/build.gradle.kts` | App build config, dependencies, flavors |
|
|
||||||
|
|
||||||
## Development Tips
|
|
||||||
|
|
||||||
- Use `assembleFossDebug` as the fastest build variant for iteration
|
|
||||||
- Shared code goes in `app-common/`, app-specific code in `app/`
|
|
||||||
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
|
|
||||||
- Always use string resources for user-facing text (see localization rules)
|
|
||||||
- Check `git log --oneline -20` for commit message style before committing
|
|
||||||
|
|
||||||
## Rules Reference
|
|
||||||
|
|
||||||
Detailed guidelines are in `.claude/rules/`:
|
|
||||||
|
|
||||||
- `architecture.md` — Module structure, key components, data flow, dependencies
|
|
||||||
- `build-commands.md` — Build, test, lint, and release commands
|
|
||||||
- `localization.md` — String resource naming conventions
|
|
||||||
- `commit-guidelines.md` — Commit message format and prefixes
|
|
||||||
- `agent-instructions.md` — Sub-agent delegation and critical thinking
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
description: Instructions for Claude Code sub-agents and task delegation
|
|
||||||
globs:
|
|
||||||
- "**"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agent Instructions
|
|
||||||
|
|
||||||
## Critical Thinking
|
|
||||||
|
|
||||||
- Do not blindly accept information at face value
|
|
||||||
- Verify assumptions against actual code before proceeding
|
|
||||||
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
|
|
||||||
- If something seems wrong, it probably is — dig deeper
|
|
||||||
|
|
||||||
## Explore vs. Implement
|
|
||||||
|
|
||||||
- **Explore first**: Before making changes, understand the existing code structure and patterns
|
|
||||||
- **Read before writing**: Always read relevant files before modifying them
|
|
||||||
- **Follow existing patterns**: Match the code style and architecture already in use
|
|
||||||
- **Minimal changes**: Only change what's necessary to accomplish the task
|
|
||||||
|
|
||||||
## Sub-Agent Delegation
|
|
||||||
|
|
||||||
When using Task tool to spawn sub-agents:
|
|
||||||
|
|
||||||
- Provide complete context — sub-agents don't share your conversation history unless noted
|
|
||||||
- Be specific about what you need: research only, or research + implementation
|
|
||||||
- Use `Explore` agent type for codebase investigation
|
|
||||||
- Use `Bash` agent type for running builds and tests
|
|
||||||
- Parallelize independent sub-agent tasks for efficiency
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
- Don't create new files when editing existing ones would suffice
|
|
||||||
- Don't add features beyond what was requested
|
|
||||||
- Don't refactor surrounding code when fixing a bug
|
|
||||||
- Don't add comments or documentation to code you didn't change
|
|
||||||
- Don't guess at file paths — use Glob/Grep to find them
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
description: Architecture overview, module structure, key components, data flow, and dependencies
|
|
||||||
globs:
|
|
||||||
- "app/**/*.kt"
|
|
||||||
- "app-common/**/*.kt"
|
|
||||||
- "**/*.gradle.kts"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Architecture
|
|
||||||
|
|
||||||
## Multi-Module Structure
|
|
||||||
|
|
||||||
- **app/**: Main Android application with FOSS and Google Play flavors
|
|
||||||
- **app-common/**: Shared code between main app and Wear OS app
|
|
||||||
|
|
||||||
## Core Patterns
|
|
||||||
|
|
||||||
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
|
|
||||||
- **Dependency Injection**: Hilt/Dagger for dependency management
|
|
||||||
- **Coroutines**: Kotlin coroutines for async operations
|
|
||||||
- **Repository Pattern**: Data layer abstraction for monitoring and settings
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
### PodMonitor System
|
|
||||||
|
|
||||||
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
|
|
||||||
- `MonitorControl`: Manages MonitorService lifecycle
|
|
||||||
- `MonitorService`: Foreground service that continuously scans for AirPods
|
|
||||||
- `BluetoothEventReceiver`: Handles system Bluetooth events
|
|
||||||
|
|
||||||
### Reaction System
|
|
||||||
|
|
||||||
- `ReactionSettingsFragment`: Configuration for popup notifications
|
|
||||||
- `PopUpWindow`: Displays AirPods status when case is opened
|
|
||||||
- `PopUpPodViewFactory`: Creates UI components for different pod models
|
|
||||||
|
|
||||||
### Common Utilities
|
|
||||||
|
|
||||||
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
|
|
||||||
|
|
||||||
## Build Configuration
|
|
||||||
|
|
||||||
### Flavors
|
|
||||||
|
|
||||||
- **FOSS**: Open-source version without Google Play dependencies
|
|
||||||
- **Google Play (gplay)**: Version with billing client for in-app purchases
|
|
||||||
|
|
||||||
### Build Types
|
|
||||||
|
|
||||||
- **debug**: Unobfuscated, full logging, no minification
|
|
||||||
- **beta**: Obfuscated, production-ready with strict lint checks
|
|
||||||
- **release**: Fully optimized for production distribution
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
The app follows a unidirectional data flow:
|
|
||||||
|
|
||||||
1. `BluetoothEventReceiver` detects Bluetooth events
|
|
||||||
2. `MonitorService` scans for AirPods beacon data
|
|
||||||
3. `PodMonitor` processes and stores device information
|
|
||||||
4. ViewModels observe monitor data via repositories
|
|
||||||
5. UI components react to ViewModel state changes
|
|
||||||
6. `ReactionSystem` triggers popups and notifications
|
|
||||||
|
|
||||||
## Bluetooth LE Implementation
|
|
||||||
|
|
||||||
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service.
|
|
||||||
|
|
||||||
## Multi-Platform Considerations
|
|
||||||
|
|
||||||
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
|
|
||||||
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
|
|
||||||
|
|
||||||
## Key Dependencies
|
|
||||||
|
|
||||||
- **Hilt**: Dependency injection framework
|
|
||||||
- **AndroidX Navigation**: Fragment navigation with SafeArgs
|
|
||||||
- **Moshi**: JSON serialization for configuration and debugging
|
|
||||||
- **Material Design**: UI components following Material Design guidelines
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
description: Build, test, lint, and release commands for Gradle
|
|
||||||
globs:
|
|
||||||
- "**/*.gradle.kts"
|
|
||||||
- "**/*.gradle"
|
|
||||||
- "gradle/**"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Build Commands
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build debug version
|
|
||||||
./gradlew assembleDebug
|
|
||||||
|
|
||||||
# Build all variants (FOSS and Google Play flavors)
|
|
||||||
./gradlew assemble
|
|
||||||
|
|
||||||
# Build specific flavor and type
|
|
||||||
./gradlew assembleFossDebug
|
|
||||||
./gradlew assembleGplayRelease
|
|
||||||
|
|
||||||
# Build app bundles for Play Store
|
|
||||||
./gradlew bundleGplayRelease
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all unit tests
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
# Run unit tests for specific variant
|
|
||||||
./gradlew testFossDebugUnitTest
|
|
||||||
|
|
||||||
# Run instrumentation tests (requires connected device/emulator)
|
|
||||||
./gradlew connectedAndroidTest
|
|
||||||
./gradlew connectedFossDebugAndroidTest
|
|
||||||
|
|
||||||
# Run all checks (lint + tests)
|
|
||||||
./gradlew check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run lint for all variants
|
|
||||||
./gradlew lint
|
|
||||||
|
|
||||||
# Run lint for specific variant
|
|
||||||
./gradlew lintFossDebug
|
|
||||||
|
|
||||||
# Auto-fix lint issues where possible
|
|
||||||
./gradlew lintFix
|
|
||||||
|
|
||||||
# Update lint baseline
|
|
||||||
./gradlew updateLintBaseline
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew assembleFossRelease assembleGplayRelease
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
|
|
||||||
- Run `./gradlew check` before submitting changes to catch lint and test issues
|
|
||||||
- Instrumentation tests require a connected device or running emulator
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
description: Git commit message format and conventions
|
|
||||||
globs:
|
|
||||||
- "**"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Commit Guidelines
|
|
||||||
|
|
||||||
## Format
|
|
||||||
|
|
||||||
```
|
|
||||||
<prefix>: <Short summary>
|
|
||||||
```
|
|
||||||
|
|
||||||
Summary line should be concise and describe the change. No period at the end.
|
|
||||||
|
|
||||||
## Prefixes
|
|
||||||
|
|
||||||
Use the existing commit history as reference. Common prefixes:
|
|
||||||
|
|
||||||
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
|
|
||||||
- **feat**: New features
|
|
||||||
- **refactor**: Code restructuring without behavior change
|
|
||||||
- **chore**: Maintenance, dependency updates, build config
|
|
||||||
- **docs**: Documentation changes
|
|
||||||
|
|
||||||
## Component Scope (optional)
|
|
||||||
|
|
||||||
When a change is scoped to a specific area, include it after the prefix:
|
|
||||||
|
|
||||||
- `fix(widget): Fix layout for devices with single charge detection`
|
|
||||||
- `feat(monitor): Add battery level caching`
|
|
||||||
- `refactor(popup): Extract pod view factory`
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Keep the summary line under 72 characters
|
|
||||||
- Use imperative mood ("Add feature" not "Added feature")
|
|
||||||
- Reference issue numbers when applicable
|
|
||||||
- Do not include `Co-authored-by` trailers
|
|
||||||
- Look at recent `git log` output to match the project's existing style
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
description: Guidelines for adding and naming Android string resources
|
|
||||||
globs:
|
|
||||||
- "**/res/values*/strings.xml"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Localization Guidelines
|
|
||||||
|
|
||||||
When adding new user-facing strings:
|
|
||||||
|
|
||||||
- **Always use string resources**: Never hardcode user-facing text in layouts or code
|
|
||||||
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
|
|
||||||
- **Provide context**: String names should indicate usage and location
|
|
||||||
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
|
|
||||||
|
|
||||||
## Naming Examples
|
|
||||||
|
|
||||||
- `profiles_create_title` (screen title)
|
|
||||||
- `profiles_name_label` (form field label)
|
|
||||||
- `profiles_delete_confirmation` (dialog message)
|
|
||||||
- `error_network_unavailable` (error message)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"mcp__ide__getDiagnostics",
|
|
||||||
"Bash(./gradlew tasks:*)",
|
|
||||||
"Bash(./gradlew:*)",
|
|
||||||
"Bash(find:*)",
|
|
||||||
"Bash(ls:*)",
|
|
||||||
"Bash(grep:*)",
|
|
||||||
"Bash(rg:*)",
|
|
||||||
"Bash(git add:*)",
|
|
||||||
"Bash(git log:*)",
|
|
||||||
"Bash(git commit:*)",
|
|
||||||
"Bash(git show:*)",
|
|
||||||
"Bash(git checkout:*)",
|
|
||||||
"Bash(git branch:*)",
|
|
||||||
"Bash(git switch:*)",
|
|
||||||
"Bash(git diff:*)",
|
|
||||||
"Bash(git status:*)",
|
|
||||||
"Bash(git stash:*)",
|
|
||||||
"Bash(./crowdin.sh:*)",
|
|
||||||
"Bash(gh issue list:*)",
|
|
||||||
"Bash(gh pr list:*)",
|
|
||||||
"Bash(gh label list:*)",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch(domain:support.google.com)",
|
|
||||||
"WebFetch(domain:github.com)",
|
|
||||||
"WebFetch(domain:mvnrepository.com)",
|
|
||||||
"WebFetch(domain:kotlinlang.org)",
|
|
||||||
"WebFetch(domain:developer.android.com)",
|
|
||||||
"WebFetch(domain:issuetracker.google.com)"
|
|
||||||
],
|
|
||||||
"deny": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
variant: [ Debug ]
|
variant: [ Debug,Beta,Release ]
|
||||||
flavor: [ testFoss,testGplay ]
|
flavor: [ testFoss,testGplay ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ jobs:
|
|||||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
name: ${{ steps.tagger.outputs.tag }}
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: app/build/outputs/apk/foss/beta/eu.darken.capod-*.apk
|
files: |
|
||||||
|
app/build/outputs/apk/foss/beta/*.apk
|
||||||
|
app-wear/build/outputs/apk/foss/beta/*.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ jobs:
|
|||||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||||
name: ${{ steps.tagger.outputs.tag }}
|
name: ${{ steps.tagger.outputs.tag }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: app/build/outputs/apk/foss/release/eu.darken.capod-*.apk
|
files: app/build/outputs/apk/foss/release/*.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -128,6 +130,14 @@ jobs:
|
|||||||
ruby-version: 3.3.6
|
ruby-version: 3.3.6
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
|
# - name: Assemble WearOS beta and upload to Google Play
|
||||||
|
# if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
|
# run: bundle exec fastlane beta_wearos
|
||||||
|
# env:
|
||||||
|
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Assemble beta and upload to Google Play
|
- name: Assemble beta and upload to Google Play
|
||||||
if: contains(steps.tagger.outputs.tag, '-beta')
|
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
run: bundle exec fastlane beta
|
run: bundle exec fastlane beta
|
||||||
@@ -136,6 +146,14 @@ jobs:
|
|||||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
# - name: Assemble WearOS production and upload to Google Play
|
||||||
|
# if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||||
|
# run: bundle exec fastlane production_wearos
|
||||||
|
# env:
|
||||||
|
# STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
|
||||||
|
# KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Assemble production and upload to Google Play
|
- name: Assemble production and upload to Google Play
|
||||||
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
if: "!contains(steps.tagger.outputs.tag, '-beta')"
|
||||||
run: bundle exec fastlane production
|
run: bundle exec fastlane production
|
||||||
|
|||||||
+1
-3
@@ -12,6 +12,4 @@
|
|||||||
*.jks
|
*.jks
|
||||||
.local/*
|
.local/*
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
/fastlane/Appfile
|
/fastlane/Appfile
|
||||||
/fastlane/README.md
|
|
||||||
.kotlin
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
---
|
|
||||||
layout: plain
|
|
||||||
permalink: /changelog
|
|
||||||
title: "Changelog"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Changelog for CAPod
|
|
||||||
|
|
||||||
{% for release in site.github.releases %}
|
|
||||||
|
|
||||||
## {{ release.tag_name }} - {{ release.published_at | date: "%B %d, %Y" }}
|
|
||||||
|
|
||||||
{% assign clean_body = release.body | strip %}
|
|
||||||
{% assign no_comments = clean_body | replace: "<!-- Release notes generated using configuration in .github/release.yml", "" %}
|
|
||||||
{% assign no_comments = no_comments | split: "-->" %}
|
|
||||||
{% if no_comments.size > 1 %}
|
|
||||||
{% assign clean_content = no_comments[1] | strip %}
|
|
||||||
{% else %}
|
|
||||||
{% assign clean_content = no_comments[0] | strip %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% comment %} Make links clickable {% endcomment %}
|
|
||||||
{% assign lines = clean_content | split: "
|
|
||||||
" %}
|
|
||||||
{% assign processed_lines = "" %}
|
|
||||||
{% for line in lines %}
|
|
||||||
{% if line contains "**Full Changelog**:" %}
|
|
||||||
{% comment %} Handle Full Changelog links {% endcomment %}
|
|
||||||
{% assign parts = line | split: ": " %}
|
|
||||||
{% if parts.size > 1 %}
|
|
||||||
{% assign url = parts[1] | strip %}
|
|
||||||
{% assign clickable_line = "**[View Changes](" | append: url | append: ")**" %}
|
|
||||||
{% assign processed_lines = processed_lines | append: clickable_line | append: "
|
|
||||||
" %}
|
|
||||||
{% else %}
|
|
||||||
{% assign processed_lines = processed_lines | append: line | append: "
|
|
||||||
" %}
|
|
||||||
{% endif %}
|
|
||||||
{% elsif line contains " in https://github.com/" and line contains "/pull/" %}
|
|
||||||
{% comment %} Handle pull request links {% endcomment %}
|
|
||||||
{% assign pr_parts = line | split: " in https://github.com/" %}
|
|
||||||
{% if pr_parts.size > 1 %}
|
|
||||||
{% assign before_url = pr_parts[0] %}
|
|
||||||
{% assign after_url = pr_parts[1] %}
|
|
||||||
{% assign url = "https://github.com/" | append: after_url %}
|
|
||||||
{% assign pr_number = after_url | split: "/pull/" %}
|
|
||||||
{% if pr_number.size > 1 %}
|
|
||||||
{% assign pr_num = pr_number[1] | split: " " | first %}
|
|
||||||
{% assign clickable_line = before_url | append: " in [#" | append: pr_num | append: "](" | append: url | append: ")" %}
|
|
||||||
{% assign processed_lines = processed_lines | append: clickable_line | append: "
|
|
||||||
" %}
|
|
||||||
{% else %}
|
|
||||||
{% assign processed_lines = processed_lines | append: line | append: "
|
|
||||||
" %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% assign processed_lines = processed_lines | append: line | append: "
|
|
||||||
" %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% assign processed_lines = processed_lines | append: line | append: "
|
|
||||||
" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% comment %} Add proper spacing between sections and bullet points {% endcomment %}
|
|
||||||
{% assign final_content = processed_lines | replace: "
|
|
||||||
### ", "
|
|
||||||
|
|
||||||
### " %}
|
|
||||||
{% assign final_content = final_content | replace: "
|
|
||||||
## ", "
|
|
||||||
|
|
||||||
## " %}
|
|
||||||
{% assign final_content = final_content | replace: "
|
|
||||||
- ", "
|
|
||||||
|
|
||||||
- " %}
|
|
||||||
|
|
||||||
{% comment %} Check if there are any bullet points (actual release notes) {% endcomment %}
|
|
||||||
{% if final_content contains "## " or final_content contains "- " %}
|
|
||||||
{{ final_content | markdownify }}
|
|
||||||
{% else %}
|
|
||||||
*No release notes available.*
|
|
||||||
|
|
||||||
{{ final_content | markdownify }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
---
|
|
||||||
{% endfor %}
|
|
||||||
+76
-87
@@ -1,48 +1,43 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.7)
|
CFPropertyList (3.0.6)
|
||||||
base64
|
|
||||||
nkf
|
|
||||||
rexml
|
rexml
|
||||||
addressable (2.8.7)
|
addressable (2.8.4)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.2)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.1109.0)
|
aws-partitions (1.784.0)
|
||||||
aws-sdk-core (3.224.1)
|
aws-sdk-core (3.177.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.5)
|
||||||
base64
|
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
aws-sdk-kms (1.70.0)
|
||||||
aws-sdk-kms (1.101.0)
|
aws-sdk-core (~> 3, >= 3.177.0)
|
||||||
aws-sdk-core (~> 3, >= 3.216.0)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sdk-s3 (1.128.0)
|
||||||
aws-sdk-s3 (1.188.0)
|
aws-sdk-core (~> 3, >= 3.177.0)
|
||||||
aws-sdk-core (~> 3, >= 3.224.1)
|
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.6)
|
||||||
aws-sigv4 (1.11.0)
|
aws-sigv4 (1.6.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.3.0)
|
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.109.0)
|
excon (0.100.0)
|
||||||
faraday (1.10.4)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -58,27 +53,27 @@ GEM
|
|||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (~> 1.0.0)
|
||||||
faraday-em_http (1.0.0)
|
faraday-em_http (1.0.0)
|
||||||
faraday-em_synchrony (1.0.1)
|
faraday-em_synchrony (1.0.0)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.1.1)
|
faraday-multipart (1.0.4)
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.1)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.4.0)
|
fastimage (2.2.7)
|
||||||
fastlane (2.228.0)
|
fastlane (2.213.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
babosa (>= 1.0.3, < 2.0.0)
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored (~> 1.2)
|
colored
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
@@ -87,38 +82,33 @@ GEM
|
|||||||
faraday-cookie_jar (~> 0.0.6)
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
faraday_middleware (~> 1.0)
|
faraday_middleware (~> 1.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
fastlane-sirp (>= 1.0.0)
|
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
|
||||||
google-cloud-storage (~> 1.31)
|
google-cloud-storage (~> 1.31)
|
||||||
highline (~> 2.0)
|
highline (~> 2.0)
|
||||||
http-cookie (~> 1.0.5)
|
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
optparse (>= 0.1.1, < 1.0.0)
|
optparse (~> 0.1.1)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
security (= 0.1.5)
|
security (= 0.1.3)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
terminal-table (~> 3)
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
tty-screen (>= 0.6.3, < 1.0.0)
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
word_wrap (~> 1.0.0)
|
word_wrap (~> 1.0.0)
|
||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.4.1)
|
xcpretty (~> 0.3.0)
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
fastlane-sirp (1.0.0)
|
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.54.0)
|
google-apis-androidpublisher_v3 (0.45.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-core (0.11.3)
|
google-apis-core (0.11.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -126,66 +116,64 @@ GEM
|
|||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.29.0)
|
google-apis-storage_v1 (0.19.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
google-cloud-core (1.6.1)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.3.1)
|
||||||
google-cloud-storage (1.45.0)
|
google-cloud-storage (1.44.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.29.0)
|
google-apis-storage_v1 (~> 0.19.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.8.1)
|
googleauth (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
|
memoist (~> 0.16)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.8)
|
http-cookie (1.0.5)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.9.0)
|
httpclient (2.8.3)
|
||||||
mutex_m
|
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.6)
|
json (2.6.3)
|
||||||
jwt (2.10.1)
|
jwt (2.7.1)
|
||||||
base64
|
memoist (0.16.2)
|
||||||
logger (1.7.0)
|
mini_magick (4.12.0)
|
||||||
mini_magick (4.13.2)
|
mini_mime (1.1.2)
|
||||||
mini_mime (1.1.5)
|
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.3.0)
|
||||||
mutex_m (0.3.0)
|
nanaimo (0.3.0)
|
||||||
nanaimo (0.4.0)
|
naturally (2.2.1)
|
||||||
naturally (2.3.0)
|
optparse (0.1.1)
|
||||||
nkf (0.2.0)
|
|
||||||
optparse (0.6.0)
|
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.2)
|
plist (3.7.0)
|
||||||
public_suffix (5.1.1)
|
public_suffix (5.0.1)
|
||||||
rake (13.3.0)
|
rake (13.0.6)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.4.1)
|
rexml (3.2.5)
|
||||||
rouge (3.28.0)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.3.2)
|
||||||
security (0.1.5)
|
security (0.1.3)
|
||||||
signet (0.18.0)
|
signet (0.17.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -193,33 +181,34 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-screen (0.8.2)
|
tty-screen (0.8.1)
|
||||||
tty-spinner (0.9.3)
|
tty-spinner (0.9.3)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unf (0.2.0)
|
unf (0.1.4)
|
||||||
unicode-display_width (2.6.0)
|
unf_ext
|
||||||
|
unf_ext (0.0.8.2)
|
||||||
|
unicode-display_width (1.8.0)
|
||||||
|
webrick (1.8.1)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.27.0)
|
xcodeproj (1.22.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.4.0)
|
nanaimo (~> 0.3.0)
|
||||||
rexml (>= 3.3.6, < 4.0)
|
rexml (~> 3.2.4)
|
||||||
xcpretty (0.4.1)
|
xcpretty (0.3.0)
|
||||||
rouge (~> 3.28.0)
|
rouge (~> 2.0.7)
|
||||||
xcpretty-travis-formatter (1.0.1)
|
xcpretty-travis-formatter (1.0.1)
|
||||||
xcpretty (~> 0.2, >= 0.0.7)
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
universal-darwin-24
|
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ A companion app that adds support for AirPod specific features to Android:
|
|||||||
* Ear detection with automatic play/pause.
|
* Ear detection with automatic play/pause.
|
||||||
* Automatically connect phone & AirPods.
|
* Automatically connect phone & AirPods.
|
||||||
* Show popup when case is opened.
|
* Show popup when case is opened.
|
||||||
|
* Support for Wear OS
|
||||||
* Widgets
|
* Widgets
|
||||||
|
|
||||||
CAPod is ad-free. Some additional features require an in-app purchase.
|
CAPod is ad-free. Some additional features require an in-app purchase.
|
||||||
@@ -31,7 +32,6 @@ Currently supported models:
|
|||||||
* AirPods Pro 1. Generation
|
* AirPods Pro 1. Generation
|
||||||
* AirPods Pro 2. Generation
|
* AirPods Pro 2. Generation
|
||||||
* AirPods Pro 2. Generation (USB-C)
|
* AirPods Pro 2. Generation (USB-C)
|
||||||
* AirPods Pro 3. Generation
|
|
||||||
* AirPods Max
|
* AirPods Max
|
||||||
* Power Beats Pro
|
* Power Beats Pro
|
||||||
* Power Beats 3
|
* Power Beats 3
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
+1
-6
@@ -1,9 +1,6 @@
|
|||||||
theme: minima
|
theme: minima
|
||||||
plugins:
|
plugins:
|
||||||
- jekyll-relative-links
|
- jekyll-relative-links
|
||||||
- jekyll-github-metadata
|
|
||||||
- jemoji
|
|
||||||
|
|
||||||
relative_links:
|
relative_links:
|
||||||
enabled: true
|
enabled: true
|
||||||
collections: true
|
collections: true
|
||||||
@@ -14,8 +11,6 @@ author: "by Matthias Urhahn"
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- PRIVACY_POLICY.md
|
- PRIVACY_POLICY.md
|
||||||
- README.md
|
|
||||||
- CHANGELOG.md
|
|
||||||
exclude:
|
exclude:
|
||||||
- buildSrc
|
- buildSrc
|
||||||
- gradle/wrapper
|
- gradle/wrapper
|
||||||
@@ -25,4 +20,4 @@ exclude:
|
|||||||
- crowdin*
|
- crowdin*
|
||||||
- app
|
- app
|
||||||
- app-common
|
- app-common
|
||||||
- CONTRIBUTING.md
|
- app-wear
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("kotlin-android")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
apply(plugin = "dagger.hilt.android.plugin")
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = ProjectConfig.compileSdk
|
||||||
|
namespace = "${ProjectConfig.packageName}.common"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = ProjectConfig.minSdk
|
||||||
|
targetSdk = ProjectConfig.targetSdk
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
|
||||||
|
buildConfigField("Long", "VERSION_CODE", "${ProjectConfig.Version.code}L")
|
||||||
|
buildConfigField("String", "VERSION_NAME", "\"${ProjectConfig.Version.name}\"")
|
||||||
|
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
|
||||||
|
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
||||||
|
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||||
|
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-opt-in=kotlin.time.ExperimentalTime",
|
||||||
|
"-opt-in=kotlin.ExperimentalUnsignedTypes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions.add("version")
|
||||||
|
productFlavors {
|
||||||
|
create("foss") {
|
||||||
|
dimension = "version"
|
||||||
|
}
|
||||||
|
create("gplay") {
|
||||||
|
dimension = "version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
val customProguardRules = fileTree(File("../proguard")) {
|
||||||
|
include("*.pro")
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
proguardFiles(*customProguardRules.toList().toTypedArray())
|
||||||
|
proguardFiles("proguard-rules-debug.pro")
|
||||||
|
}
|
||||||
|
create("beta") {
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
proguardFiles(*customProguardRules.toList().toTypedArray())
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
proguardFiles(*customProguardRules.toList().toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
|
|
||||||
|
addBaseAndroid()
|
||||||
|
addBaseAndroidUi()
|
||||||
|
addBaseKotlin()
|
||||||
|
addDagger()
|
||||||
|
addMoshi()
|
||||||
|
addBaseWorkManager()
|
||||||
|
addNavigation()
|
||||||
|
|
||||||
|
addTesting()
|
||||||
|
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
+1
@@ -5,6 +5,7 @@ import dagger.Module
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
|
import eu.darken.capod.debug.autoreport.FossAutoReporting
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.darken.capod.common">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<receiver
|
||||||
|
android:name=".bluetooth.BleScanResultReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package eu.darken.capod.common
|
||||||
|
|
||||||
|
|
||||||
|
// Can't be const because that prevents them from being mocked in tests
|
||||||
|
@Suppress("MayBeConstant")
|
||||||
|
object BuildConfigWrap {
|
||||||
|
val DEBUG: Boolean = BuildConfig.DEBUG
|
||||||
|
|
||||||
|
val BUILD_TYPE: BuildType = when (val typ = BuildConfig.BUILD_TYPE) {
|
||||||
|
"debug" -> BuildType.DEV
|
||||||
|
"beta" -> BuildType.BETA
|
||||||
|
"release" -> BuildType.RELEASE
|
||||||
|
else -> throw IllegalArgumentException("Unknown buildtype: $typ")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class BuildType {
|
||||||
|
DEV,
|
||||||
|
BETA,
|
||||||
|
RELEASE,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
val FLAVOR: Flavor = when (val flav = BuildConfig.FLAVOR) {
|
||||||
|
"gplay" -> Flavor.GPLAY
|
||||||
|
"foss" -> Flavor.FOSS
|
||||||
|
else -> throw IllegalStateException("Unknown flavor: $flav")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Flavor {
|
||||||
|
GPLAY,
|
||||||
|
FOSS,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
val APPLICATION_ID: String = BuildConfig.APPLICATION_ID
|
||||||
|
|
||||||
|
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
|
||||||
|
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
||||||
|
val GIT_SHA: String = BuildConfig.GITSHA
|
||||||
|
val BUILDTIME: String = BuildConfig.BUILDTIME
|
||||||
|
|
||||||
|
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
||||||
|
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
|
||||||
|
val VERSION_DESCRIPTION_TINY: String = "v$VERSION_NAME"
|
||||||
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
+34
-131
@@ -12,8 +12,8 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
|
import android.os.ParcelUuid
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
@@ -21,38 +21,27 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.io.IOException
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BluetoothManager2 @Inject constructor(
|
class BluetoothManager2 @Inject constructor(
|
||||||
@AppScope private val appScope: CoroutineScope,
|
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val manager: BluetoothManager,
|
private val manager: BluetoothManager,
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val adapter: BluetoothAdapter?
|
val adapter: BluetoothAdapter?
|
||||||
@@ -100,7 +89,7 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile)" }
|
log(TAG, WARN) { "onServiceDisconnected(profile=$profile)" }
|
||||||
close() // Close gracefully without exception to prevent crash
|
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}, profile)
|
}, profile)
|
||||||
@@ -114,118 +103,59 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun monitorProfile(
|
private fun monitorDevicesForProfile(
|
||||||
profile: Int = BluetoothProfile.HEADSET
|
profile: Int = BluetoothProfile.HEADSET
|
||||||
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
|
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): for profile=$profile starting" }
|
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
|
||||||
|
trySend(bluetoothProfile.connectedDevices)
|
||||||
try {
|
|
||||||
trySend(bluetoothProfile.connectedDevices)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "monitorProfile(): Error querying initial connected devices: $e" }
|
|
||||||
close(e)
|
|
||||||
return@callbackFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
|
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
val handlerThread = HandlerThread("BluetoothEventReceiver").apply { start() }
|
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
|
||||||
|
start()
|
||||||
|
}
|
||||||
val handler = Handler(handlerThread.looper)
|
val handler = Handler(handlerThread.looper)
|
||||||
|
|
||||||
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
||||||
|
val action = intent.action
|
||||||
if (intent.action == null) {
|
if (action == null) {
|
||||||
log(TAG, ERROR) { "monitorProfile(): Bluetooth event without action?" }
|
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
|
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
log(TAG, ERROR) { "monitorProfile(): Event is missing EXTRA_DEVICE" }
|
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this@callbackFlow.launch {
|
this@callbackFlow.launch {
|
||||||
if (intent.action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
|
val currentDevices = bluetoothProfile.connectedDevices
|
||||||
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile connection changed - query actual state from proxy
|
when (action) {
|
||||||
val statePrevious = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1)
|
BluetoothDevice.ACTION_ACL_CONNECTED -> {
|
||||||
log(TAG) { "monitorProfile(): HEADSET profile state changed for $device - previous: $statePrevious" }
|
log(TAG) { "Adding $device to current devices $currentDevices" }
|
||||||
|
trySend(currentDevices.plus(device))
|
||||||
val stateNow = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)
|
|
||||||
log(TAG) { "monitorProfile(): HEADSET profile state changed for $device - now: $stateNow" }
|
|
||||||
|
|
||||||
val currentDevices = try {
|
|
||||||
bluetoothProfile.connectedDevices
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "monitorProfile(): Error handling profile event: $e" }
|
|
||||||
// Log but continue - don't kill the whole Flow for one bad event
|
|
||||||
emptySet()
|
|
||||||
}.toMutableSet()
|
|
||||||
log(TAG) { "monitorProfile(): currentDevices: $currentDevices" }
|
|
||||||
|
|
||||||
when (stateNow) {
|
|
||||||
BluetoothProfile.STATE_CONNECTING -> {
|
|
||||||
log(TAG) { "monitorProfile(): Currently connecting $device" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BluetoothProfile.STATE_CONNECTED -> {
|
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
|
||||||
log(TAG) { "monitorProfile(): Device has connected $device" }
|
log(TAG) { "Removing $device from current devices $currentDevices" }
|
||||||
if (!currentDevices.contains(device)) {
|
trySend(currentDevices.minus(device))
|
||||||
log(
|
|
||||||
TAG,
|
|
||||||
VERBOSE
|
|
||||||
) { "monitorProfile(): $device not in proxy yet, adding manually" }
|
|
||||||
currentDevices.add(device)
|
|
||||||
}
|
|
||||||
trySend(currentDevices)
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothProfile.STATE_DISCONNECTING -> {
|
|
||||||
log(TAG) { "monitorProfile(): Currently DISconnecting $device" }
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
||||||
log(TAG) { "monitorProfile(): Device has disconnected $device" }
|
|
||||||
if (currentDevices.contains(device)) {
|
|
||||||
log(
|
|
||||||
TAG,
|
|
||||||
VERBOSE
|
|
||||||
) { "monitorProfile(): $device still in proxy, removing manually" }
|
|
||||||
currentDevices.remove(device)
|
|
||||||
}
|
|
||||||
trySend(currentDevices)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context.registerReceiver(receiver, filter, null, handler)
|
||||||
try {
|
|
||||||
context.registerReceiver(receiver, filter, null, handler)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "monitorProfile(): Failed to register receiver: $e" }
|
|
||||||
handlerThread.quitSafely()
|
|
||||||
close(e)
|
|
||||||
return@callbackFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): profile=$profile closed." }
|
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
|
||||||
try {
|
context.unregisterReceiver(receiver)
|
||||||
context.unregisterReceiver(receiver)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log(TAG, ERROR) { "monitorProfile(): Error unregistering receiver: $e" }
|
|
||||||
} finally {
|
|
||||||
handlerThread.quitSafely()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +163,10 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
private val seenDevicesLock = Mutex()
|
private val seenDevicesLock = Mutex()
|
||||||
private val seenDevicesCache = mutableMapOf<String, Instant>()
|
private val seenDevicesCache = mutableMapOf<String, Instant>()
|
||||||
|
|
||||||
val connectedDevices: Flow<List<BluetoothDevice2>> = isBluetoothEnabled
|
fun connectedDevices(
|
||||||
.flatMapLatest { enabled ->
|
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
|
||||||
if (enabled) monitorProfile(BluetoothProfile.HEADSET)
|
): Flow<List<BluetoothDevice2>> = isBluetoothEnabled
|
||||||
else flowOf(emptySet()) // Return empty when Bluetooth is off
|
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
|
||||||
}
|
|
||||||
.map { devices ->
|
.map { devices ->
|
||||||
val currentAddresses = devices.map { it.address }
|
val currentAddresses = devices.map { it.address }
|
||||||
|
|
||||||
@@ -248,9 +177,7 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
devices
|
devices
|
||||||
.filter { device ->
|
.filter { device -> featureFilter.any { feature -> device.hasFeature(feature) } }
|
||||||
ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> device.hasFeature(feature) }
|
|
||||||
}
|
|
||||||
.map { device ->
|
.map { device ->
|
||||||
BluetoothDevice2(
|
BluetoothDevice2(
|
||||||
internal = device,
|
internal = device,
|
||||||
@@ -262,30 +189,6 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.retryWhen { cause, attempt ->
|
|
||||||
log(TAG, WARN) { "connectedDevices Flow failed (attempt ${attempt + 1}): $cause" }
|
|
||||||
if (attempt < 3) {
|
|
||||||
delay(1000 * (attempt + 1)) // 1s, 2s, 3s exponential backoff
|
|
||||||
true // Retry
|
|
||||||
} else {
|
|
||||||
false // Give up after 3 attempts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.catch { e ->
|
|
||||||
log(TAG, ERROR) { "connectedDevices Flow failed after retries: $e" }
|
|
||||||
emit(emptyList()) // Emit empty list and complete gracefully
|
|
||||||
}
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.setupCommonEventHandlers(TAG) { "connectedDevices" }
|
|
||||||
.stateIn(
|
|
||||||
scope = appScope + dispatcherProvider.IO,
|
|
||||||
started = SharingStarted.WhileSubscribed(
|
|
||||||
stopTimeoutMillis = 5_000L,
|
|
||||||
replayExpirationMillis = 0L,
|
|
||||||
),
|
|
||||||
initialValue = null
|
|
||||||
)
|
|
||||||
.filterNotNull()
|
|
||||||
|
|
||||||
fun bondedDevices(): Flow<Set<BluetoothDevice2>> = flow {
|
fun bondedDevices(): Flow<Set<BluetoothDevice2>> = flow {
|
||||||
val rawDevices = adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
val rawDevices = adapter?.bondedDevices ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
||||||
+1
-1
@@ -3,7 +3,7 @@ package eu.darken.capod.common.bluetooth
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class ScannerMode(
|
enum class ScannerMode(
|
||||||
+6
@@ -5,6 +5,7 @@ import android.app.NotificationManager
|
|||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import androidx.work.WorkManager
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -29,6 +30,11 @@ class AndroidModule {
|
|||||||
fun bluetoothManager(context: Context): BluetoothManager =
|
fun bluetoothManager(context: Context): BluetoothManager =
|
||||||
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun workerManager(context: Context): WorkManager =
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun audioManager(context: Context): AudioManager =
|
fun audioManager(context: Context): AudioManager =
|
||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
package eu.darken.capod.common.debug
|
package eu.darken.capod.common.debug
|
||||||
|
|
||||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
-1
@@ -2,7 +2,6 @@ package eu.darken.capod.common.error
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.darken.capod.R
|
|
||||||
|
|
||||||
fun Throwable.asErrorDialogBuilder(
|
fun Throwable.asErrorDialogBuilder(
|
||||||
context: Context
|
context: Context
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package eu.darken.capod.common.error
|
package eu.darken.capod.common.error
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface HasLocalizedError {
|
interface HasLocalizedError {
|
||||||
fun getLocalizedError(context: Context): LocalizedError
|
fun getLocalizedError(context: Context): LocalizedError
|
||||||
+1
-10
@@ -6,16 +6,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.shareIn
|
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
+1
-16
@@ -8,22 +8,7 @@ import eu.darken.capod.common.error.hasCause
|
|||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.scan
|
|
||||||
import kotlinx.coroutines.flow.shareIn
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.transform
|
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-6
@@ -4,12 +4,7 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.*
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import androidx.annotation.LayoutRes
|
|
||||||
import androidx.annotation.PluralsRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.darken.capod.common.getColorForAttr
|
import eu.darken.capod.common.getColorForAttr
|
||||||
+1
-1
@@ -2,7 +2,7 @@ package eu.darken.capod.common.lists.modular.mods
|
|||||||
|
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
|
|
||||||
class ClickMod<VHT : ModularAdapter.VH>(
|
class ClickMod<VHT : ModularAdapter.VH> constructor(
|
||||||
private val listener: (VHT, Int) -> Unit
|
private val listener: (VHT, Int) -> Unit
|
||||||
) : ModularAdapter.Module.Binder<VHT> {
|
) : ModularAdapter.Module.Binder<VHT> {
|
||||||
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import androidx.viewbinding.ViewBinding
|
|||||||
import eu.darken.capod.common.lists.BindableVH
|
import eu.darken.capod.common.lists.BindableVH
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
|
|
||||||
class DataBinderMod<ItemT, HolderT>(
|
class DataBinderMod<ItemT, HolderT> constructor(
|
||||||
private val data: List<ItemT>,
|
private val data: List<ItemT>,
|
||||||
private val customBinder: (
|
private val customBinder: (
|
||||||
(adapter: ModularAdapter<HolderT>, vh: HolderT, pos: Int, payload: MutableList<Any>) -> Unit
|
(adapter: ModularAdapter<HolderT>, vh: HolderT, pos: Int, payload: MutableList<Any>) -> Unit
|
||||||
+1
-1
@@ -3,7 +3,7 @@ package eu.darken.capod.common.lists.modular.mods
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
|
|
||||||
class SimpleVHCreatorMod<HolderT>(
|
class SimpleVHCreatorMod<HolderT> constructor(
|
||||||
private val viewType: Int = 0,
|
private val viewType: Int = 0,
|
||||||
private val factory: (ViewGroup) -> HolderT
|
private val factory: (ViewGroup) -> HolderT
|
||||||
) : ModularAdapter.Module.Creator<HolderT> where HolderT : ModularAdapter.VH {
|
) : ModularAdapter.Module.Creator<HolderT> where HolderT : ModularAdapter.VH {
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import eu.darken.capod.common.lists.differ.DifferItem
|
import eu.darken.capod.common.lists.differ.DifferItem
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
|
|
||||||
class StableIdMod<ItemT : DifferItem>(
|
class StableIdMod<ItemT : DifferItem> constructor(
|
||||||
private val data: List<ItemT>,
|
private val data: List<ItemT>,
|
||||||
private val customResolver: (position: Int) -> Long = {
|
private val customResolver: (position: Int) -> Long = {
|
||||||
(data[it] as? DifferItem)?.stableId ?: RecyclerView.NO_ID
|
(data[it] as? DifferItem)?.stableId ?: RecyclerView.NO_ID
|
||||||
+2
-2
@@ -3,14 +3,14 @@ package eu.darken.capod.common.lists.modular.mods
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
|
|
||||||
class TypedVHCreatorMod<HolderT>(
|
class TypedVHCreatorMod<HolderT> constructor(
|
||||||
private val typeResolver: (Int) -> Boolean,
|
private val typeResolver: (Int) -> Boolean,
|
||||||
private val factory: (ViewGroup) -> HolderT
|
private val factory: (ViewGroup) -> HolderT
|
||||||
) : ModularAdapter.Module.Typing,
|
) : ModularAdapter.Module.Typing,
|
||||||
ModularAdapter.Module.Creator<HolderT> where HolderT : ModularAdapter.VH {
|
ModularAdapter.Module.Creator<HolderT> where HolderT : ModularAdapter.VH {
|
||||||
|
|
||||||
private fun ModularAdapter<*>.determineOurViewType(): Int {
|
private fun ModularAdapter<*>.determineOurViewType(): Int {
|
||||||
val typingModules = modules.filterIsInstance<ModularAdapter.Module.Typing>()
|
val typingModules = modules.filterIsInstance(ModularAdapter.Module.Typing::class.java)
|
||||||
return typingModules.indexOf(this@TypedVHCreatorMod)
|
return typingModules.indexOf(this@TypedVHCreatorMod)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.darken.capod.common.navigation
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavArgs
|
||||||
|
import androidx.navigation.NavArgsLazy
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
// TODO Remove with "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha/stable"
|
||||||
|
inline fun <reified Args : NavArgs> SavedStateHandle.navArgs() = NavArgsLazy(Args::class) {
|
||||||
|
Bundle().apply {
|
||||||
|
keys().forEach {
|
||||||
|
when (val value = get<Any>(it)) {
|
||||||
|
is Serializable -> putSerializable(it, value)
|
||||||
|
is Parcelable -> putParcelable(it, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import android.os.PowerManager
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.withinApiLevel
|
import eu.darken.capod.common.withinApiLevel
|
||||||
|
|
||||||
enum class Permission(
|
enum class Permission(
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class FlowPreference<T>(
|
class FlowPreference<T> constructor(
|
||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
val key: String,
|
val key: String,
|
||||||
val rawReader: (Any?) -> T,
|
val rawReader: (Any?) -> T,
|
||||||
+1
-2
@@ -3,13 +3,12 @@ package eu.darken.capod.common.preferences
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.darken.capod.R
|
|
||||||
|
|
||||||
class MaterialSwitchPreference(context: Context, attrs: AttributeSet?) :
|
class MaterialSwitchPreference(context: Context, attrs: AttributeSet?) :
|
||||||
SwitchPreferenceCompat(context, attrs) {
|
SwitchPreferenceCompat(context, attrs) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Use material switch
|
// Use material switch
|
||||||
widgetLayoutResource = R.layout.preference_material_switch
|
widgetLayoutResource = eu.darken.capod.common.R.layout.preference_material_switch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user