mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b3f431e9e | ||
|
|
6014004c0f | ||
|
|
ebbc5394a7 | ||
|
|
e36868a2fc | ||
|
|
1c6fb8ed31 | ||
|
|
e780fd17d2 | ||
|
|
86d3f3bc61 | ||
|
|
ba554c8787 | ||
|
|
968829072a | ||
|
|
0d1f331551 | ||
|
|
a8bb42d40b | ||
|
|
9a3fb11ef7 | ||
|
|
12d2c4dd06 | ||
|
|
cfed733fa7 | ||
|
|
1af8743533 | ||
|
|
2f3f1f3f70 | ||
|
|
6223524a1e | ||
|
|
ebfffb1536 | ||
|
|
5aa36b148b | ||
|
|
29391feaf8 | ||
|
|
4e279ca44d | ||
|
|
9ff8d9e04e | ||
|
|
10d5a823fc | ||
|
|
54afadb136 | ||
|
|
e4db9387ca | ||
|
|
4834eb276a |
@@ -0,0 +1,45 @@
|
||||
# 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
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
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)
|
||||
@@ -8,6 +8,20 @@
|
||||
"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)",
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to fix import statements after removing app-common module.
|
||||
This script fixes R class imports from app-common to app module.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def fix_r_import(file_path):
|
||||
"""Fix R import in a single file."""
|
||||
print(f"Fixing R import in: {file_path}")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace the import statement
|
||||
updated_content = re.sub(
|
||||
r'import eu\.darken\.capod\.common\.R$',
|
||||
'import eu.darken.capod.R',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
if updated_content != content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
print(f" ✓ Updated import in {file_path}")
|
||||
return True
|
||||
else:
|
||||
print(f" - No changes needed in {file_path}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main function to fix all import statements."""
|
||||
project_root = Path.cwd()
|
||||
app_src = project_root / "app" / "src"
|
||||
|
||||
# Find all Kotlin files that import eu.darken.capod.common.R
|
||||
files_to_fix = []
|
||||
for kt_file in app_src.rglob("*.kt"):
|
||||
with open(kt_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if re.search(r'import eu\.darken\.capod\.common\.R$', content, re.MULTILINE):
|
||||
files_to_fix.append(kt_file)
|
||||
|
||||
print(f"Found {len(files_to_fix)} files with incorrect R imports")
|
||||
|
||||
success_count = 0
|
||||
for file_path in files_to_fix:
|
||||
if fix_r_import(file_path):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nFixed imports in {success_count} files")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to fix fully qualified R references in code.
|
||||
This script finds and replaces eu.darken.capod.common.R with R.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def fix_qualified_r_references(file_path):
|
||||
"""Fix fully qualified R references in a single file."""
|
||||
print(f"Fixing qualified R references in: {file_path}")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace fully qualified references
|
||||
updated_content = re.sub(
|
||||
r'\beu\.darken\.capod\.common\.R\.',
|
||||
'R.',
|
||||
content
|
||||
)
|
||||
|
||||
if updated_content != content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
print(f" ✓ Fixed qualified R references in {file_path}")
|
||||
return True
|
||||
else:
|
||||
print(f" - No changes needed in {file_path}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main function to fix all qualified R references."""
|
||||
project_root = Path.cwd()
|
||||
app_src = project_root / "app" / "src"
|
||||
|
||||
# Find all Kotlin files that have qualified R references
|
||||
files_to_fix = []
|
||||
for kt_file in app_src.rglob("*.kt"):
|
||||
with open(kt_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if re.search(r'\beu\.darken\.capod\.common\.R\.', content):
|
||||
files_to_fix.append(kt_file)
|
||||
|
||||
print(f"Found {len(files_to_fix)} files with qualified R references")
|
||||
|
||||
success_count = 0
|
||||
for file_path in files_to_fix:
|
||||
if fix_qualified_r_references(file_path):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nFixed qualified R references in {success_count} files")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to fix remaining R import issues.
|
||||
This script finds files that use R.* but don't have R imports and adds them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def needs_r_import(file_path):
|
||||
"""Check if file uses R.* but doesn't import R."""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if file uses R.something
|
||||
uses_r = re.search(r'\bR\.[a-zA-Z_]', content)
|
||||
if not uses_r:
|
||||
return False
|
||||
|
||||
# Check if file already imports R
|
||||
has_import = re.search(r'import.*\.R$', content, re.MULTILINE)
|
||||
if has_import:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def add_r_import(file_path):
|
||||
"""Add R import to a file."""
|
||||
print(f"Adding R import to: {file_path}")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the package line
|
||||
package_match = re.search(r'^package\s+[^\n]+$', content, re.MULTILINE)
|
||||
if not package_match:
|
||||
print(f" Error: No package declaration found in {file_path}")
|
||||
return False
|
||||
|
||||
# Find existing imports
|
||||
import_pattern = r'^import\s+[^\n]+$'
|
||||
existing_imports = list(re.finditer(import_pattern, content, re.MULTILINE))
|
||||
|
||||
if existing_imports:
|
||||
# Insert after the last import
|
||||
last_import = existing_imports[-1]
|
||||
insert_pos = last_import.end()
|
||||
updated_content = (content[:insert_pos] +
|
||||
"\nimport eu.darken.capod.R" +
|
||||
content[insert_pos:])
|
||||
else:
|
||||
# Insert after package line
|
||||
insert_pos = package_match.end()
|
||||
updated_content = (content[:insert_pos] +
|
||||
"\n\nimport eu.darken.capod.R" +
|
||||
content[insert_pos:])
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
|
||||
print(f" ✓ Added R import to {file_path}")
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Main function to fix all R import issues."""
|
||||
project_root = Path.cwd()
|
||||
app_src = project_root / "app" / "src"
|
||||
|
||||
# Find all Kotlin files that need R import
|
||||
files_to_fix = []
|
||||
for kt_file in app_src.rglob("*.kt"):
|
||||
if needs_r_import(kt_file):
|
||||
files_to_fix.append(kt_file)
|
||||
|
||||
print(f"Found {len(files_to_fix)} files that need R imports")
|
||||
|
||||
success_count = 0
|
||||
for file_path in files_to_fix:
|
||||
if add_r_import(file_path):
|
||||
success_count += 1
|
||||
|
||||
print(f"\nAdded R imports to {success_count} files")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to merge strings.xml files from app-common module to app module.
|
||||
This script merges all string entries from app-common into the corresponding app module files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
def merge_strings_xml(app_common_file, app_file):
|
||||
"""Merge strings from app-common file into app file."""
|
||||
print(f"Processing: {app_common_file.name} -> {app_file.name}")
|
||||
|
||||
# Read the app-common strings.xml content
|
||||
with open(app_common_file, 'r', encoding='utf-8') as f:
|
||||
common_content = f.read()
|
||||
|
||||
# Read the app strings.xml content
|
||||
with open(app_file, 'r', encoding='utf-8') as f:
|
||||
app_content = f.read()
|
||||
|
||||
# Extract string entries from app-common (including comments)
|
||||
# Find everything between <resources> and </resources> tags, excluding the tags themselves
|
||||
common_match = re.search(r'<resources[^>]*>(.*?)</resources>', common_content, re.DOTALL)
|
||||
if not common_match:
|
||||
print(f" Warning: No resources found in {app_common_file}")
|
||||
return False
|
||||
|
||||
common_strings = common_match.group(1).strip()
|
||||
|
||||
if not common_strings:
|
||||
print(f" Warning: No string content found in {app_common_file}")
|
||||
return False
|
||||
|
||||
# Find the insertion point in the app file (before </resources>)
|
||||
app_match = re.search(r'(.*?)(\s*</resources>)', app_content, re.DOTALL)
|
||||
if not app_match:
|
||||
print(f" Error: Invalid XML structure in {app_file}")
|
||||
return False
|
||||
|
||||
# Merge the content
|
||||
before_closing = app_match.group(1)
|
||||
closing_tag = app_match.group(2)
|
||||
|
||||
# Add the common strings with proper spacing
|
||||
merged_content = f"{before_closing}\n\n <!-- Strings from app-common -->\n{common_strings}\n{closing_tag}"
|
||||
|
||||
# Write the merged content back to the app file
|
||||
with open(app_file, 'w', encoding='utf-8') as f:
|
||||
f.write(merged_content)
|
||||
|
||||
print(f" ✓ Merged successfully")
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Main function to merge all strings.xml files."""
|
||||
project_root = Path.cwd()
|
||||
app_common_res = project_root / "app-common" / "src" / "main" / "res"
|
||||
app_res = project_root / "app" / "src" / "main" / "res"
|
||||
|
||||
if not app_common_res.exists():
|
||||
print(f"Error: app-common resources directory not found: {app_common_res}")
|
||||
return 1
|
||||
|
||||
if not app_res.exists():
|
||||
print(f"Error: app resources directory not found: {app_res}")
|
||||
return 1
|
||||
|
||||
# Find all strings.xml files in app-common
|
||||
common_strings_files = list(app_common_res.glob("*/strings.xml"))
|
||||
|
||||
if not common_strings_files:
|
||||
print("Error: No strings.xml files found in app-common")
|
||||
return 1
|
||||
|
||||
print(f"Found {len(common_strings_files)} strings.xml files to merge")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for common_file in sorted(common_strings_files):
|
||||
# Determine the corresponding app file
|
||||
locale_dir = common_file.parent.name
|
||||
app_file = app_res / locale_dir / "strings.xml"
|
||||
|
||||
if not app_file.exists():
|
||||
print(f" Error: Corresponding app file does not exist: {app_file}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
if merge_strings_xml(common_file, app_file):
|
||||
success_count += 1
|
||||
else:
|
||||
error_count += 1
|
||||
|
||||
print(f"\nMerge complete:")
|
||||
print(f" ✓ Successfully merged: {success_count}")
|
||||
print(f" ✗ Errors: {error_count}")
|
||||
|
||||
return 0 if error_count == 0 else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: [ Debug,Beta,Release ]
|
||||
variant: [ Debug ]
|
||||
flavor: [ testFoss,testGplay ]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||
name: ${{ steps.tagger.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
files: app/build/outputs/apk/foss/beta/*.apk
|
||||
files: app/build/outputs/apk/foss/beta/eu.darken.capod-*.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
tag_name: ${{ steps.tagger.outputs.tag }}
|
||||
name: ${{ steps.tagger.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
files: app/build/outputs/apk/foss/release/*.apk
|
||||
files: app/build/outputs/apk/foss/release/eu.darken.capod-*.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
### Build Commands
|
||||
|
||||
```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 Commands
|
||||
|
||||
```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 Commands
|
||||
|
||||
```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 Commands
|
||||
|
||||
```bash
|
||||
./gradlew assembleFossRelease assembleGplayRelease
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Multi-Module Structure
|
||||
|
||||
- **app/**: Main Android application with FOSS and Google Play flavors
|
||||
- **app-common/**: Shared code between main app
|
||||
|
||||
### Core Architecture Patterns
|
||||
|
||||
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
|
||||
- **Dependency Injection**: Hilt/Dagger for dependency management
|
||||
- **Coroutines**: Extensive use of 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 background monitoring worker lifecycle
|
||||
- `MonitorWorker`: Background worker 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**: 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 Architecture
|
||||
|
||||
The app follows a unidirectional data flow:
|
||||
|
||||
1. `BluetoothEventReceiver` detects Bluetooth events
|
||||
2. `MonitorWorker` 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
|
||||
|
||||
### 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
|
||||
- **WorkManager**: Background task scheduling for monitoring
|
||||
- **Moshi**: JSON serialization for configuration and debugging
|
||||
- **Material Design**: UI components following Material Design guidelines
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Bluetooth LE Implementation
|
||||
|
||||
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in
|
||||
`MonitorWorker` which runs as a long-lived background task.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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
|
||||
|
||||
Examples of correct string naming:
|
||||
- `profiles_create_title` (screen title)
|
||||
- `profiles_name_label` (form field label)
|
||||
- `profiles_delete_confirmation` (dialog message)
|
||||
- `error_network_unavailable` (error message)
|
||||
@@ -26,4 +26,3 @@ exclude:
|
||||
- app
|
||||
- app-common
|
||||
- CONTRIBUTING.md
|
||||
- CLAUDE.md
|
||||
|
||||
+38
-15
@@ -35,7 +35,7 @@ android {
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
val basePath = File(System.getProperty("user.home"), ".appconfig/${projectConfig.packageName}")
|
||||
val basePath = File(System.getProperty("user.home"), ".config/projects/${projectConfig.packageName}")
|
||||
create("releaseFoss") {
|
||||
setupCredentials(File(basePath, "signing-foss.properties"))
|
||||
}
|
||||
@@ -94,21 +94,9 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildOutputs.all {
|
||||
val variantOutputImpl = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
val variantName: String = variantOutputImpl.name
|
||||
|
||||
if (listOf("release", "beta").any { variantName.lowercase().contains(it) }) {
|
||||
val outputFileName = projectConfig.packageName +
|
||||
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
|
||||
"-${variantName.uppercase()}.apk"
|
||||
|
||||
variantOutputImpl.outputFileName = outputFileName
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -141,6 +129,42 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val buildType = variant.buildType ?: return@onVariants
|
||||
if (buildType != "release" && buildType != "beta") return@onVariants
|
||||
|
||||
val formattedVariantName = variant.name
|
||||
.replace(Regex("([a-z])([A-Z])"), "$1-$2")
|
||||
.uppercase()
|
||||
|
||||
val apkFolder = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.APK)
|
||||
val loader = variant.artifacts.getBuiltArtifactsLoader()
|
||||
val packageName = projectConfig.packageName
|
||||
|
||||
val renameTask = tasks.register("rename${variant.name.replaceFirstChar { it.uppercase() }}Apk") {
|
||||
inputs.files(apkFolder)
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
doLast {
|
||||
val builtArtifacts = loader.load(apkFolder.get()) ?: return@doLast
|
||||
|
||||
builtArtifacts.elements.forEach { element ->
|
||||
val apkFile = File(element.outputFile)
|
||||
val outputFileName = "$packageName-v${element.versionName}-${element.versionCode}-$formattedVariantName.apk"
|
||||
if (apkFile.exists() && apkFile.name != outputFileName) {
|
||||
apkFile.copyTo(File(apkFile.parentFile, outputFileName), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name == "assemble${variant.name.replaceFirstChar { it.uppercase() }}" }.configureEach {
|
||||
finalizedBy(renameTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
|
||||
@@ -158,7 +182,6 @@ dependencies {
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
|
||||
addNavigation()
|
||||
addBaseWorkManager()
|
||||
|
||||
addTesting()
|
||||
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-dienste is nie beskikbaar nie.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Geen aankope gevind nie. Gebruik jy die regte rekening?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-fout</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Daar was \'n fout in Google Play. Probeer asseblief later weer of herbegin jou foon.\n\nFout: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play-faktureringsfout</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Daar was \'n fout toe Google Play om jou aankoopbesonderhede gevra is. Maak die Google Play-kas skoon en herbegin jou foon.\n\nFout %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">የGoogle Play አገልግሎቶች አይገኙም።</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">ምንም ግዢዎች አልተገኙም። ትክክለኛውን መለያ እየተጠቀሙ ነው?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">የGoogle Play ስህተት</string>
|
||||
<string name="upgrades_gplay_billing_error_description">በGoogle Play ላይ ስህተት ነበር። እባክዎ ቆይተው ይሞክሩ ወይም ስልክዎን እንደገና ያስጀምሩ።\n\nስህተት: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">የGoogle Play ክፍያ ስህተት</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play የግዢ ዝርዝሮችዎን ሲጠይቅ ስህተት ነበር። የGoogle Play መሸጎጫውን ያጽዱ እና ስልክዎን እንደገና ያስጀምሩ።\n\nስህተት %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">خدمات Google Play غير متاحة.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">لم يتم العثور على أي مشتريات. هل تستخدم الحساب الصحيح؟</string>
|
||||
<string name="upgrades_gplay_billing_error_label">خطأ في Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">حدث خطأ في Google Play. يرجى المحاولة مرة أخرى لاحقًا أو إعادة تشغيل هاتفك.\n\nخطأ: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">خطأ في فوترة Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">حدث خطأ عند طلب تفاصيل الشراء من Google Play. امسح ذاكرة التخزين المؤقت لـ Google Play وأعد تشغيل هاتفك.\n\nخطأ %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play xidmətləri əlçatmazdır.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Heç bir satın alma tapılmadı. Doğru hesabı istifadə edirsiniz?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play xətası</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-də xəta baş verdi. Zəhmət olmasa daha sonra yenidən cəhd edin və ya telefonunuzu yenidən başladın.\n\nXəta: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play faktura xətası</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play-dən satınalma təfərrüatlarınızı soruşarkən xəta baş verdi. Google Play keşini təmizləyin və telefonunuzu yenidən başladın.\n\nXəta %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Службы Google Play недаступныя.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Пакупак не знойдзена. Вы выкарыстоўваеце правільны ўліковы запіс?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Памылка Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Адбылася памылка ў Google Play. Калі ласка, паспрабуйце пазней або перазагрузіце тэлефон.\n\nПамылка: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Памылка выстаўлення рахункаў Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Адбылася памылка пры запыце дэталяў пакупкі ў Google Play. Ачысціце кэш Google Play і перазагрузіце тэлефон.\n\nПамылка %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Услугите на Google Play не са налични.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Няма намерени покупки. Правилния акаунт ли използвате?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Грешка в Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Възникна грешка в Google Play. Моля, опитайте отново по-късно или рестартирайте телефона си.\n\nГрешка: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Грешка при фактуриране в Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Възникна грешка при заявката за данни за покупката ви от Google Play. Изчистете кеша на Google Play и рестартирайте телефона си.\n\nГрешка %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play পরিষেবাগুলি উপলব্ধ নেই৷</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">কোনও কেনাকাটা খুঁজে পাওয়া যায়নি। আপনি কি সঠিক অ্যাকাউন্ট ব্যবহার করছেন?</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">কোনও কেনাকাটা খুঁজে পাওয়া যায়নি। আপনি কি সঠিক অ্যাকাউন্ট ব্যবহার করছেন?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play ত্রুটি</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-তে একটি ত্রুটি হয়েছে। অনুগ্রহ করে পরে আবার চেষ্টা করুন অথবা আপনার ফোন রিবুট করুন।\n\nত্রুটি: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play বিলিং ত্রুটি</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play-তে আপনার কেনাকাটার বিবরণ জানতে চাইলে একটি ত্রুটি হয়েছে। Google Play ক্যাশে সাফ করুন এবং আপনার ফোন রিবুট করুন।\n\nত্রুটি %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-tjenester er ikke tilgængelige.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ingen køb fundet. Bruger du den rigtige konto?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-fejl</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Der opstod en fejl i Google Play. Prøv venligst igen senere, eller genstart din telefon.\n\nFejl: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play-faktureringsfejl</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Der opstod en fejl under forespørgsel af dine købsoplysninger fra Google Play. Ryd Google Play-cachen, og genstart din telefon.\n\nFejl %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Οι υπηρεσίες Google Play δεν είναι διαθέσιμες.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Δε βρέθηκαν αγορές. Χρησιμοποιείτε τον σωστό λογαριασμό;</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Σφάλμα Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Παρουσιάστηκε σφάλμα στο Google Play. Δοκιμάστε ξανά αργότερα ή επανεκκινήστε το τηλέφωνό σας.\n\nΣφάλμα: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Σφάλμα χρέωσης Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Παρουσιάστηκε σφάλμα κατά την αίτηση των στοιχείων αγοράς σας από το Google Play. Εκκαθαρίστε την προσωρινή μνήμη του Google Play και επανεκκινήστε το τηλέφωνό σας.\n\nΣφάλμα %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play zerbitzuak ez daude erabilgarri.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ez da erosketarik aurkitu. Kontu egokia erabiltzen ari zara?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play errorea</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Errore bat gertatu da Google Play-n. Mesedez, saiatu berriro geroago edo berrabiarazi telefonoa.\n\nErrorea: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play fakturazio errorea</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Errore bat gertatu da Google Play-ri zure erosketa xehetasunak eskatzean. Garbitu Google Play-ren cachea eta berrabiarazi telefonoa.\n\nErrorea %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">سرویسهای Google Play در دسترس نیستند.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">خریدی یافت نشد. آیا از حساب درستی استفاده میکنید؟</string>
|
||||
<string name="upgrades_gplay_billing_error_label">خطای Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">خطایی در Google Play رخ داد. لطفاً بعداً دوباره امتحان کنید یا گوشی خود را مجدداً راهاندازی کنید.\n\nخطا: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">خطای صورتحساب Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">هنگام درخواست جزئیات خرید از Google Play خطایی رخ داد. حافظه پنهان Google Play را پاک کنید و گوشی خود را مجدداً راهاندازی کنید.\n\nخطا %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play -palvelut eivät ole käytettävissä.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ostoksia ei löytynyt. Käytätkö oikeaa tiliä?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play -virhe</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Playssa tapahtui virhe. Yritä myöhemmin uudelleen tai käynnistä puhelimesi uudelleen.\n\nVirhe: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play -laskutusvirhe</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Ostotietojen hakemisessa Google Playsta tapahtui virhe. Tyhjennä Google Playn välimuisti ja käynnistä puhelimesi uudelleen.\n\nVirhe %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Hindi available ang mga serbisyo ng Google Play.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Walang nakitang binili. Tamang account ba ang gamit mo?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Error sa Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Nagkaroon ng error sa Google Play. Pakisubukan ulit mamaya o i-reboot ang iyong telepono.\n\nError: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Error sa Google Play Billing</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Nagkaroon ng error sa paghingi ng detalye ng iyong pagbili sa Google Play. I-clear ang Google Play cache at i-reboot ang iyong telepono.\n\nError %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Os servizos de Google Play non están dispoñibles.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Non se atoparon compras. Estás a usar a conta correcta?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Erro de Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Houbo un erro en Google Play. Téntao de novo máis tarde ou reinicia o teu teléfono.\n\nErro: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Erro de facturación de Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Houbo un erro ao solicitar os detalles da túa compra a Google Play. Limpa a caché de Google Play e reinicia o teu teléfono.\n\nErro %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play सेवाएं अनुपलब्ध हैं।</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">कोई खरीदारी नहीं मिली, क्या आप सही खाते का उपयोग कर रहे हैं?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play त्रुटि</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play में एक त्रुटि हुई। कृपया बाद में पुनः प्रयास करें या अपना फ़ोन रीबूट करें।\n\nत्रुटि: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play बिलिंग त्रुटि</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play से आपकी खरीदारी का विवरण प्राप्त करते समय एक त्रुटि हुई। Google Play कैश साफ़ करें और अपना फ़ोन रीबूट करें।\n\nत्रुटि %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Usluge Google Playa nisu dostupne.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nisu pronađene kupnje. Koristite li pravi račun?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Greška Google Playa</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Došlo je do greške u Google Playu. Pokušajte ponovo kasnije ili ponovo pokrenite telefon.\n\nGreška: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Greška naplate Google Playa</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Došlo je do greške prilikom dohvaćanja podataka o kupnji iz Google Playa. Očistite predmemoriju Google Playa i ponovo pokrenite telefon.\n\nGreška %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">A Google Play szolgáltatások nem érhetők el.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nem található vásárlás. A megfelelő fiókot használja?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play hiba</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Hiba történt a Google Playben. Kérjük, próbálja újra később, vagy indítsa újra a telefonját.\n\nHiba: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play számlázási hiba</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Hiba történt a vásárlási adatok lekérésekor a Google Playből. Törölje a Google Play gyorsítótárát, és indítsa újra a telefonját.\n\nHiba %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play ծառայությունները հասանելի չեն:</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Գնումներ չեն գտնվել: Արդյո՞ք ճիշտ հաշիվն եք օգտագործում:</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-ի սխալ</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-ում սխալ է տեղի ունեցել: Խնդրում ենք փորձել ավելի ուշ կամ վերագործարկել ձեր հեռախոսը.\n\nՍխալ: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Billing-ի սխալ</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play-ից ձեր գնումների տվյալները հարցնելիս սխալ է տեղի ունեցել: Մաքրեք Google Play-ի քեշը և վերագործարկեք ձեր հեռախոսը.\n\nՍխալ %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Layanan Google Play tidak tersedia.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Tidak ada pembelian yang ditemukan. Apakah Anda menggunakan akun yang benar?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Kesalahan Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Terjadi kesalahan di Google Play. Silakan coba lagi nanti atau mulai ulang ponsel Anda.\n\nKesalahan: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Kesalahan Penagihan Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Terjadi kesalahan saat meminta detail pembelian Anda dari Google Play. Hapus cache Google Play dan mulai ulang ponsel Anda.\n\nKesalahan %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play þjónustur eru ekki tiltækar.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Engin kaup fundust. Ertu að nota réttan aðgang?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play villa</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Villa kom upp í Google Play. Vinsamlegast reyndu aftur síðar eða endurræstu símann þinn.\n\nVilla: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play innheimtuvilla</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Villa kom upp við að sækja upplýsingar um kaupin þín frá Google Play. Hreinsaðu skyndiminni Google Play og endurræstu símann þinn.\n\nVilla %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">I servizi di Google Play non sono disponibili.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nessun acquisto trovato. Stai usando l\'account giusto?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Errore di Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Si è verificato un errore in Google Play. Riprova più tardi o riavvia il telefono.\n\nErrore: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Errore di fatturazione Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Si è verificato un errore durante la richiesta dei dettagli di acquisto a Google Play. Svuota la cache di Google Play e riavvia il telefono.\n\nErrore %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">שירותי Google Play אינם זמינים.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">לא נמצאו רכישות. האם אתה משתמש בחשבון הנכון?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">שגיאת Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">אירעה שגיאה ב-Google Play. נסה שוב מאוחר יותר או הפעל מחדש את הטלפון.\n\nשגיאה: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">שגיאת חיוב Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">אירעה שגיאה בעת בקשת פרטי הרכישה מ-Google Play. נקה את המטמון של Google Play והפעל מחדש את הטלפון.\n\nשגיאה %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Playサービスが利用できません。</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">購入履歴がありません。正しいアカウントを使用していますか?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Playエラー</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Playでエラーが発生しました。後でもう一度お試しいただくか、スマートフォンを再起動してください。\n\nエラー: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play請求エラー</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Playに購入情報を問い合わせる際にエラーが発生しました。Google Playのキャッシュをクリアし、スマートフォンを再起動してください。\n\nエラー %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play სერვისები მიუწვდომელია.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">შენაძენები ვერ მოიძებნა. იყენებთ სწორ ანგარიშს?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-ის შეცდომა</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-ში მოხდა შეცდომა. სცადეთ მოგვიანებით ან გადატვირთეთ ტელეფონი.\n\nშეცდომა: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play ბილინგის შეცდომა</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">შეცდომა მოხდა Google Play-დან შენაძენის დეტალების მოთხოვნისას. გაასუფთავეთ Google Play-ის ქეში და გადატვირთეთ ტელეფონი.\n\nშეცდომა %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">សេវាកម្ម Google Play មិនអាចប្រើបានទេ</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">រកមិនឃើញការទិញទេ។ តើអ្នកកំពុងប្រើគណនីត្រឹមត្រូវទេ?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">កំហុស Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">មានកំហុសនៅក្នុង Google Play។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ ឬចាប់ផ្តើមទូរសព្ទឡើងវិញ។\n\nកំហុស: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">កំហុសការចេញវិក្កយបត្រ Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">មានកំហុសពេលស្នើសុំព័ត៌មានការទិញពី Google Play។ សូមសម្អាតឃ្លាំងសម្ងាត់ Google Play និងចាប់ផ្តើមទូរសព្ទឡើងវិញ។\n\nកំហុស %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Xizmetên Google Play ne berdest in.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Kirîn nehatin dîtin. Hûn hesabê rast bikar tînin?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Çewtiya Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Di Google Play de çewtiyek çêbû. Ji kerema xwe paşê dîsa biceribîne an jî têlefona xwe ji nû ve bide destpêkirin.\n\nÇewtî: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Çewtiya Fatûrekirina Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Dema ku ji Google Play agahdariyên kirîna we hat xwestin çewtiyek çêbû. Pêşbîra Google Play paqij bikin û têlefona xwe ji nû ve bide destpêkirin.\n\nÇewtî %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play ಸೇವೆಗಳು ಲಭ್ಯವಿಲ್ಲ.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">ಯಾವುದೇ ಖರೀದಿಗಳು ಕಂಡುಬಂದಿಲ್ಲ. ನೀವು ಸರಿಯಾದ ಖಾತೆಯನ್ನು ಬಳಸುತ್ತಿದ್ದೀರಾ?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play ದೋಷ</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play ನಲ್ಲಿ ದೋಷ ಸಂಭವಿಸಿದೆ. ದಯವಿಟ್ಟು ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ ಅಥವಾ ನಿಮ್ಮ ಫೋನ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಿ.\n\nದೋಷ: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play ಬಿಲ್ಲಿಂಗ್ ದೋಷ</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play ನಿಂದ ನಿಮ್ಮ ಖರೀದಿ ವಿವರಗಳನ್ನು ಕೇಳುವಾಗ ದೋಷ ಸಂಭವಿಸಿದೆ. Google Play ಕ್ಯಾಶ್ ಅನ್ನು ತೆರವುಗೊಳಿಸಿ ಮತ್ತು ನಿಮ್ಮ ಫೋನ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಿ.\n\nದೋಷ %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play кызматтары жеткиликсиз.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Эч кандай сатып алуулар табылган жок. Туура каттоо эсебин колдонуп жатасызбы?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play катасы</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play\'де ката кетти. Кийинчерээк кайра аракет кылыңыз же телефонуңузду өчүрүп күйгүзүңүз.\n\nКата: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play төлөм катасы</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play\'ден сатып алуу маалыматын сурап жатканда ката кетти. Google Play кэшин тазалаңыз жана телефонуңузду өчүрүп күйгүзүңүз.\n\nКата %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">ບໍລິການ Google Play ບໍ່ສາມາດໃຊ້ໄດ້.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">ບໍ່ພົບການຊື້. ທ່ານກຳລັງໃຊ້ບັນຊີທີ່ຖືກຕ້ອງບໍ?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">ຂໍ້ຜິດພາດ Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">ເກີດຂໍ້ຜິດພາດໃນ Google Play. ກະລຸນາລອງໃໝ່ພາຍຫຼັງ ຫຼືປິດເປີດໂທລະສັບຂອງທ່ານໃໝ່.\n\nຂໍ້ຜິດພາດ: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">ຂໍ້ຜິດພາດການຮຽກເກັບເງິນ Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">ເກີດຂໍ້ຜິດພາດໃນການຂໍລາຍລະອຽດການຊື້ຈາກ Google Play. ລຶບແຄສ Google Play ແລະປິດເປີດໂທລະສັບຂອງທ່ານໃໝ່.\n\nຂໍ້ຜິດພາດ %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">„Google Play“ paslaugos nepasiekiamos.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nerasta jokių pirkinių. Ar naudojate tinkamą paskyrą?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">„Google Play" klaida</string>
|
||||
<string name="upgrades_gplay_billing_error_description">„Google Play" įvyko klaida. Bandykite dar kartą vėliau arba paleiskite telefoną iš naujo.\n\nKlaida: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">„Google Play" atsiskaitymo klaida</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Įvyko klaida gaunant pirkinių informaciją iš „Google Play". Išvalykite „Google Play" talpyklą ir paleiskite telefoną iš naujo.\n\nKlaida %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play pakalpojumi nav pieejami.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nav atrasts neviens pirkums. Vai izmantojat pareizo kontu?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play kļūda</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play radās kļūda. Lūdzu, mēģiniet vēlreiz vēlāk vai pārstartējiet tālruni.\n\nKļūda: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play norēķinu kļūda</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Radās kļūda, pieprasot no Google Play jūsu pirkuma informāciju. Notīriet Google Play kešatmiņu un pārstartējiet tālruni.\n\nKļūda %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Услугите на Google Play не се достапни.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Не се пронајдени купувања. Дали ја користите вистинската сметка?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Грешка во Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Настана грешка во Google Play. Обидете се повторно подоцна или рестартирајте го телефонот.\n\nГрешка: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Грешка при наплата на Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Настана грешка при барање на деталите за вашата купувања од Google Play. Исчистете го кешот на Google Play и рестартирајте го телефонот.\n\nГрешка %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play സേവനങ്ങൾ ലഭ്യമല്ല.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">വാങ്ങലുകളൊന്നും കണ്ടെത്തിയില്ല. നിങ്ങൾ ശരിയായ അക്കൗണ്ട് ആണോ ഉപയോഗിക്കുന്നത്?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play പിശക്</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-ൽ ഒരു പിശക് സംഭവിച്ചു. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക അല്ലെങ്കിൽ നിങ്ങളുടെ ഫോൺ റീബൂട്ട് ചെയ്യുക.\n\nപിശക്: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play ബില്ലിംഗ് പിശക്</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">നിങ്ങളുടെ വാങ്ങൽ വിശദാംശങ്ങൾക്കായി Google Play-യോട് അഭ്യർത്ഥിക്കുമ്പോൾ ഒരു പിശക് സംഭവിച്ചു. Google Play കാഷെ മായ്ച്ച് നിങ്ങളുടെ ഫോൺ റീബൂട്ട് ചെയ്യുക.\n\nപിശക് %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play үйлчилгээ боломжгүй байна.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Худалдан авалт олдсонгүй. Та зөв бүртгэл ашиглаж байна уу?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play алдаа</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play-д алдаа гарлаа. Дараа дахин оролдоно уу эсвэл утсаа дахин асаана уу.\n\nАлдаа: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play төлбөрийн алдаа</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play-с таны худалдан авалтын мэдээллийг авахад алдаа гарлаа. Google Play кэшийг цэвэрлээд утсаа дахин асаана уу.\n\nАлдаа %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play सेवा अनुपलब्ध आहेत.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">कोणतीही खरेदी आढळली नाही. तुम्ही योग्य खाते वापरत आहात का?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play त्रुटी</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play मध्ये त्रुटी आली. कृपया नंतर पुन्हा प्रयत्न करा किंवा तुमचा फोन रीबूट करा.\n\nत्रुटी: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play बिलिंग त्रुटी</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">तुमच्या खरेदीचे तपशील Google Play कडून मिळवताना त्रुटी आली. Google Play कॅशे साफ करा आणि तुमचा फोन रीबूट करा.\n\nत्रुटी %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Perkhidmatan Google Play tidak tersedia.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Tiada pembelian ditemui. Adakah anda menggunakan akaun yang betul?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Ralat Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Terdapat ralat dalam Google Play. Sila cuba lagi kemudian atau mulakan semula telefon anda.\n\nRalat: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Ralat Pengebilan Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Terdapat ralat semasa meminta butiran pembelian anda daripada Google Play. Kosongkan cache Google Play dan mulakan semula telefon anda.\n\nRalat %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play ဝန်ဆောင်မှုများ မရရှိနိုင်ပါ။</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">ဝယ်ယူမှုများ မတွေ့ပါ။ သင်အသုံးပြုနေသော အကောင့်မှန်ကန်ပါသလား။</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play အမှား</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play တွင် အမှားတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။ ကျေးဇူးပြု၍ နောက်မှ ထပ်ကြိုးစားပါ သို့မဟုတ် သင့်ဖုန်းကို ပြန်လည်စတင်ပါ။\n\nအမှား: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play ငွေတောင်းခံမှု အမှား</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play မှ သင့်ဝယ်ယူမှု အသေးစိတ်အချက်အလက်များကို တောင်းဆိုရာတွင် အမှားတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။ Google Play ကက်ရှ်ကို ရှင်းလင်းပြီး သင့်ဖုန်းကို ပြန်လည်စတင်ပါ။\n\nအမှား %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-tjenester er utilgjengelige.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ingen kjøp funnet. Bruker du riktig konto?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-feil</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Det oppstod en feil i Google Play. Prøv igjen senere eller start telefonen på nytt.\n\nFeil: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play-faktureringsfeil</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Det oppstod en feil da Google Play ble spurt om kjøpsdetaljene dine. Tøm Google Play-hurtigbufferen og start telefonen på nytt.\n\nFeil %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play सेवाहरू अनुपलब्ध छन्।</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">कुनै खरिदहरू फेला परेनन्। के तपाईंले सही खाता प्रयोग गरिरहनुभएको छ?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play त्रुटि</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play मा त्रुटि भयो। कृपया पछि पुन: प्रयास गर्नुहोस् वा आफ्नो फोन रिबुट गर्नुहोस्।\n\nत्रुटि: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play बिलिङ त्रुटि</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play बाट तपाईंको खरिद विवरणहरू माग्दा त्रुटि भयो। Google Play क्यास खाली गर्नुहोस् र आफ्नो फोन रिबुट गर्नुहोस्।\n\nत्रुटि %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">De Google Play-diensten zijn niet beschikbaar.</string>
|
||||
<string name="upgrades_gplay_unavailable_error">De Google Play diensten zijn niet beschikbaar.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Geen aankopen gevonden. Gebruikt u de juiste rekening?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play Fout</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Er is een fout opgetreden in Google Play. Probeer het later opnieuw of start je telefoon opnieuw op.\n\nFout: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Factureringsfout</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Factureringsfout bij Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Er is een fout opgetreden bij het opvragen van je aankoopgegevens bij Google Play. Wis de cache van Google Play en start je telefoon opnieuw op.\n\nFout %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play não estão disponíveis.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Você está usando a conta certa?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Erro do Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Houve um erro no Google Play. Por favor, tente novamente mais tarde ou reinicie seu telefone.\n\nError: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Erro de faturamento do Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Houve um erro ao solicitar ao Google Play os seus detalhes de compra. Limpe o cache do Google Play e reinicie o seu telefone. \n\nError %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Os serviços do Google Play estão indisponíveis.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenhuma compra encontrada. Está a usar a conta correta?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Erro do Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Ocorreu um erro no Google Play. Tente novamente mais tarde ou reinicie o telemóvel.\n\nErro: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Erro de faturação do Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Ocorreu um erro ao solicitar os detalhes da sua compra ao Google Play. Limpe a cache do Google Play e reinicie o telemóvel.\n\nErro %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Ils servezzans da Google Play n\'èn betg disponibels.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nagins acquists chattads. Utiliseschas ti il conto gist?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Errur da Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Igl ha dà ina errur en Google Play. Emprova per plaschair pli tard u reavvia tes telefon.\n\nErrur: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Errur da facturaziun da Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Igl ha dà ina errur cun la dumonda dals detagls da tes acquist tar Google Play. Stizza il cache da Google Play e reavvia tes telefon.\n\nErrur %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Serviciile Google Play nu sunt disponibile.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nu s-au găsit achiziții. Folosești contul corect?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Eroare Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">A apărut o eroare în Google Play. Vă rugăm să încercați din nou mai târziu sau reporniți telefonul.\n\nEroare: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Eroare de facturare Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">A apărut o eroare la solicitarea detaliilor achiziției de la Google Play. Ștergeți memoria cache Google Play și reporniți telefonul.\n\nEroare %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Servìtzios Google Play non sunt disponìbiles.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Perunu achistu agatadu. Ses usende su contu giustu?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Errore de Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">B\'at àpidu un errore in Google Play. Proa torra prus a tardu o torra a allùere su telèfonu.\n\nErrore: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Errore de fatturatzione de Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">B\'at àpidu un errore cando si cherent is detàllios de s\'achistu a Google Play. Isbòida sa cache de Google Play e torra a allùere su telèfonu.\n\nErrore %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">ගූගල් ප්ලේ සේවා නොතිබේ.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">කිසිදු මිලදී ගැනීමක් හමු නොවීය. ඔබ ትክክለኛ ගිණුම භාවිතා කරනවාද?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play දෝෂය</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play හි දෝෂයක් ඇති විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න හෝ ඔබේ දුරකථනය නැවත ආරම්භ කරන්න.\n\nදෝෂය: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play බිල්පත් දෝෂය</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">ඔබේ මිලදී ගැනීමේ විස්තර සඳහා Google Play වෙත ඉල්ලීමේදී දෝෂයක් ඇති විය. Google Play හැඹිලිය හිස් කර ඔබේ දුරකථනය නැවත ආරම්භ කරන්න.\n\nදෝෂය %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,4 +3,7 @@
|
||||
<string name="upgrades_gplay_unavailable_error">Služby Google Play nie sú k dispozícii.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nenašli sa žiadne nákupy. Používate správny účet?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Chyba Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">V službe Google Play sa vyskytla chyba. Skúste to prosím neskôr alebo reštartujte telefón.\n\nChyba: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Chyba fakturácie Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Pri získavaní podrobností o vašom nákupe z Google Play sa vyskytla chyba. Vymažte vyrovnávaciu pamäť Google Play a reštartujte telefón.\n\nChyba %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Storitve Google Play niso na voljo.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Ni najdenih nakupov. Ali uporabljate pravi račun?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Napaka Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Prišlo je do napake v storitvi Google Play. Poskusite znova pozneje ali znova zaženite telefon.\n\nNapaka: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Napaka pri obračunavanju Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Prišlo je do napake pri pridobivanju podrobnosti o vašem nakupu iz Google Play. Počistite predpomnilnik Google Play in znova zaženite telefon.\n\nNapaka %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Shërbimet e Google Play nuk janë të disponueshme.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Nuk u gjetën blerje. Po përdorni llogarinë e duhur?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Gabim i Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Kishte një gabim në Google Play. Ju lutemi provoni përsëri më vonë ose rinisni telefonin tuaj.\n\nGabim: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Gabim i faturimit të Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Kishte një gabim kur u kërkuan detajet e blerjes suaj nga Google Play. Pastroni memorien e përkohshme të Google Play dhe rinisni telefonin tuaj.\n\nGabim %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play услуге нису доступне.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Није пронађена ниједна куповина. Да ли користите прави налог?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Грешка Google Play-а</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Дошло је до грешке у Google Play-у. Покушајте поново касније или рестартујте телефон.\n\nГрешка: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Грешка наплате Google Play-а</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Дошло је до грешке при захтевању детаља о вашој куповини од Google Play-а. Обришите кеш Google Play-а и рестартујте телефон.\n\nГрешка %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play-tjänster är inte tillgängliga.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Inga köp hittades. Använder du rätt konto?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play-fel</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Det uppstod ett fel i Google Play. Försök igen senare eller starta om din telefon.\n\nFel: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play faktureringsfel</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Det uppstod ett fel vid hämtning av dina köpuppgifter från Google Play. Rensa Google Play-cachen och starta om din telefon.\n\nFel %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Huduma za Google Play hazipatikani.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Hakuna manunuzi yaliyopatikana. Je, unatumia akaunti sahihi?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Hitilafu ya Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Kulikuwa na hitilafu katika Google Play. Tafadhali jaribu tena baadaye au uzime na uwashe simu yako.\n\nHitilafu: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Hitilafu ya malipo ya Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Kulikuwa na hitilafu wakati wa kuomba maelezo ya ununuzi wako kutoka Google Play. Futa kashe ya Google Play na uzime na uwashe simu yako.\n\nHitilafu %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play சேவைகள் கிடைக்கவில்லை.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">வாங்குதல்கள் எதுவும் இல்லை. நீங்கள் சரியான கணக்கைப் பயன்படுத்துகிறீர்களா?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play பிழை</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play இல் ஒரு பிழை ஏற்பட்டது. தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும் அல்லது உங்கள் தொலைபேசியை மறுதொடக்கம் செய்யவும்.\n\nபிழை: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play பில்லிங் பிழை</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">உங்கள் வாங்குதல் விவரங்களை Google Play-யிடம் கேட்கும்போது ஒரு பிழை ஏற்பட்டது. Google Play கேச்-ஐ அழித்து உங்கள் தொலைபேசியை மறுதொடக்கம் செய்யவும்.\n\nபிழை %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play సేవలు అందుబాటులో లేవు.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">కొనుగోళ్లు ఏవీ కనుగొనబడలేదు. మీరు సరైన ఖాతాను ఉపయోగిస్తున్నారా?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play లోపం</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play లో ఒక లోపం జరిగింది. దయచేసి తర్వాత మళ్లీ ప్రయత్నించండి లేదా మీ ఫోన్ను రీబూట్ చేయండి.\n\nలోపం: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Billing లోపం</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">మీ కొనుగోలు వివరాల కోసం Google Play ను అడిగేటప్పుడు ఒక లోపం జరిగింది. Google Play క్యాష్ను క్లియర్ చేసి మీ ఫోన్ను రీబూట్ చేయండి.\n\nలోపం %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">บริการ Google Play ไม่พร้อมใช้งาน</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">ไม่พบการสั่งซื้อ คุณใช้บัญชีที่ถูกต้องหรือไม่</string>
|
||||
<string name="upgrades_gplay_billing_error_label">ข้อผิดพลาด Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">เกิดข้อผิดพลาดใน Google Play โปรดลองอีกครั้งในภายหลังหรือรีบูตโทรศัพท์ของคุณ\n\nข้อผิดพลาด: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">ข้อผิดพลาดการเรียกเก็บเงินของ Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">เกิดข้อผิดพลาดขณะขอรายละเอียดการสั่งซื้อของคุณจาก Google Play ล้างแคช Google Play แล้วรีบูตโทรศัพท์ของคุณ\n\nข้อผิดพลาด %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play hizmetleri kullanılamıyor.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Satın alma bulunamadı. Doğru hesabı mı kullanıyorsunuz?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play Hatası</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play üzerinde bir hata oluştu. Lütfen daha sonra tekrar deneyin veya telefonunuzu yeniden başlatın.\n\nHata: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Faturalandırma Hatası</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play üzerinden satın alma ayrıntılarınız istenirken bir hata oluştu. Google Play önbelleğini temizleyin ve telefonunuzu yeniden başlatın.\n\nHata %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Сервіси Google Play недоступні.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Покупок не знайдено. Ви справді використовуєте правильний обліковий запис?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Помилка Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">У Google Play сталася помилка. Спробуйте ще раз пізніше або перезавантажте телефон.\n\nПомилка: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Помилка платіжної системи Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Сталася помилка під час отримання даних про покупки з Google Play. Очистіть кеш Google Play та перезавантажте телефон.\n\nПомилка %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play کی سروسز دستیاب نہیں ہیں۔</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">کوئی خریداری نہیں ملی۔ کیا آپ درست اکاؤنٹ استعمال کر رہے ہیں؟</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play خرابی</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play میں ایک خرابی پیش آئی۔ براہ کرم بعد میں دوبارہ کوشش کریں یا اپنا فون ری بوٹ کریں۔\n\nخرابی: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play بلنگ خرابی</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">آپ کی خریداری کی تفصیلات کے لیے Google Play سے پوچھتے وقت ایک خرابی پیش آئی۔ Google Play کیش صاف کریں اور اپنا فون ری بوٹ کریں۔\n\nخرابی %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play xizmatlari mavjud emas.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Xaridlar topilmadi. To‘g‘ri hisobdan foydalanayapsizmi?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play xatosi</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play’da xatolik yuz berdi. Iltimos, keyinroq qayta urinib ko‘ring yoki telefoningizni qayta ishga tushiring.\n\nXato: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Billing xatosi</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Google Play’dan xaridingiz tafsilotlarini so‘rashda xatolik yuz berdi. Google Play keshini tozalang va telefoningizni qayta ishga tushiring.\n\nXato %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Các dịch vụ của Google Play không khả dụng.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Không tìm thấy giao dịch mua nào. Bạn có đang sử dụng đúng tài khoản không?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Lỗi Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Đã xảy ra lỗi trong Google Play. Vui lòng thử lại sau hoặc khởi động lại điện thoại của bạn.\n\nLỗi: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Lỗi thanh toán Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Đã xảy ra lỗi khi yêu cầu Google Play cung cấp chi tiết giao dịch mua của bạn. Hãy xóa bộ nhớ đệm của Google Play và khởi động lại điện thoại của bạn.\n\nLỗi %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服务不可用</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到购买记录,您使用的帐户正确吗?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play 错误</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play 出现错误。请稍后重试或重启手机。\n\n错误:%s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play 结算错误</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">向 Google Play 请求您的购买详情时出错。请清除 Google Play 缓存并重启手机。\n\n错误 %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Google Play 服務無法使用。</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">未找到訂單,確定帳戶無誤?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Google Play 錯誤</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Google Play 發生錯誤。請稍後再試或重新啟動手機。\n\n錯誤:%s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play 結帳錯誤</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">向 Google Play 請求購買明細時發生錯誤。請清除 Google Play 快取並重新啟動手機。\n\n錯誤:%s</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<string name="upgrades_gplay_unavailable_error">Amasevisi e-Google Play awakutholakali.</string>
|
||||
<string name="upgrades_no_purchases_found_check_account">Akukho ukuthenga okutholakele. Ingabe usebenzisa i-akhawunti efanele?</string>
|
||||
<string name="upgrades_gplay_billing_error_label">Iphutha le-Google Play</string>
|
||||
<string name="upgrades_gplay_billing_error_description">Kube nephutha ku-Google Play. Sicela uzame futhi ngemuva kwesikhathi noma uqalise kabusha ifoni yakho.\n\nIphutha: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Iphutha Lokukhokha le-Google Play</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">Kube nephutha lapho kucelwa ku-Google Play imininingwane yokuthenga kwakho. Sula inqolobane ye-Google Play bese uqalisa kabusha ifoni yakho.\n\nIphutha %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="eu.darken.capod">
|
||||
|
||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
@@ -9,6 +8,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
@@ -58,13 +59,21 @@
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".bluetooth.BleScanResultReceiver"
|
||||
android:name=".common.bluetooth.BleScanResultReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".monitor.core.receiver.BootCompletedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".monitor.core.receiver.BluetoothEventReceiver"
|
||||
android:enabled="true"
|
||||
@@ -112,24 +121,10 @@
|
||||
android:name=".common.debug.recording.ui.RecorderActivity"
|
||||
android:theme="@style/AppThemeFloating" />
|
||||
|
||||
<!-- Worker stuff-->
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:name=".monitor.core.worker.MonitorService"
|
||||
android:foregroundServiceType="connectedDevice"
|
||||
tools:node="merge" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
|
||||
</provider>
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,10 +1,7 @@
|
||||
package eu.darken.capod
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||
import eu.darken.capod.common.debug.logging.LogCatLogger
|
||||
@@ -23,13 +20,11 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
open class App : Application(), Configuration.Provider {
|
||||
open class App : Application() {
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@Inject lateinit var autoReporting: AutomaticBugReporter
|
||||
@Inject lateinit var monitorControl: MonitorControl
|
||||
@Inject lateinit var podMonitor: PodMonitor
|
||||
@@ -45,9 +40,7 @@ open class App : Application(), Configuration.Provider {
|
||||
|
||||
log(TAG) { "onCreate() done! ${Exception().asLog()}" }
|
||||
|
||||
appScope.launch {
|
||||
monitorControl.startMonitor(forceStart = true)
|
||||
}
|
||||
monitorControl.startMonitor(forceStart = true)
|
||||
|
||||
podMonitor.devicesWithProfiles()
|
||||
.distinctUntilChanged()
|
||||
@@ -68,21 +61,6 @@ open class App : Application(), Configuration.Provider {
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(
|
||||
when {
|
||||
BuildConfigWrap.DEBUG -> android.util.Log.VERBOSE
|
||||
BuildConfigWrap.BUILD_TYPE == BuildConfigWrap.BuildType.DEV -> android.util.Log.DEBUG
|
||||
BuildConfigWrap.BUILD_TYPE == BuildConfigWrap.BuildType.BETA -> android.util.Log.INFO
|
||||
BuildConfigWrap.BUILD_TYPE == BuildConfigWrap.BuildType.RELEASE -> android.util.Log.WARN
|
||||
else -> android.util.Log.VERBOSE
|
||||
}
|
||||
)
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
|
||||
companion object {
|
||||
internal val TAG = logTag("CAP")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.darken.capod.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
@@ -20,12 +19,14 @@ class EdgeToEdgeHelper(activity: Activity) {
|
||||
bottom: Boolean = false,
|
||||
) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val displayCutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
v.setPadding(
|
||||
if (left) systemBars.left else v.paddingLeft,
|
||||
if (top) systemBars.top else v.paddingTop,
|
||||
if (right) systemBars.right else v.paddingRight,
|
||||
if (bottom) systemBars.bottom else v.paddingBottom,
|
||||
if (left) maxOf(systemBars.left, displayCutout.left) else v.paddingLeft,
|
||||
if (top) maxOf(systemBars.top, displayCutout.top) else v.paddingTop,
|
||||
if (right) maxOf(systemBars.right, displayCutout.right) else v.paddingRight,
|
||||
if (bottom) maxOf(systemBars.bottom, displayCutout.bottom) else v.paddingBottom,
|
||||
)
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ class BluetoothManager2 @Inject constructor(
|
||||
context.registerReceiver(receiver, filter, null, handler)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "monitorProfile(): Failed to register receiver: $e" }
|
||||
handlerThread.quitSafely()
|
||||
close(e)
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.work.WorkManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -30,11 +29,6 @@ class AndroidModule {
|
||||
fun bluetoothManager(context: Context): BluetoothManager =
|
||||
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun workerManager(context: Context): WorkManager =
|
||||
WorkManager.getInstance(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun audioManager(context: Context): AudioManager =
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package eu.darken.capod.common.worker
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.work.Data
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : Parcelable> Data.getParcelable(key: String): T? {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
val bytes = getByteArray(key) ?: return null
|
||||
parcel.unmarshall(bytes, 0, bytes.size)
|
||||
parcel.setDataPosition(0)
|
||||
val creator = T::class.java.getField("CREATOR").get(null) as Parcelable.Creator<T>
|
||||
return creator.createFromParcel(parcel)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun Data.Builder.putParcelable(key: String, parcelable: Parcelable): Data.Builder {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcelable.writeToParcel(parcel, 0)
|
||||
putByteArray(key, parcel.marshall())
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -19,9 +19,7 @@ import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -76,11 +74,13 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
|
||||
// Pods battery state
|
||||
device.apply {
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
val leftPercent = batteryLeftPodPercent
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(leftPercent))
|
||||
podLeftBatteryLabel.text = formatBatteryPercent(context, leftPercent)
|
||||
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
val rightPercent = batteryRightPodPercent
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(rightPercent))
|
||||
podRightBatteryLabel.text = formatBatteryPercent(context, rightPercent)
|
||||
}
|
||||
|
||||
// Pods charging state
|
||||
@@ -139,8 +139,9 @@ class DualPodsCardVH(parent: ViewGroup) :
|
||||
if (this is HasCase) {
|
||||
podCaseIcon.setImageResource(caseIcon)
|
||||
podCaseBatteryIcon.isGone = false
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||
val casePercent = batteryCasePercent
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(casePercent))
|
||||
podCaseBatteryLabel.text = formatBatteryPercent(context, casePercent)
|
||||
|
||||
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||
|
||||
@@ -13,7 +13,7 @@ import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -55,8 +55,9 @@ class SinglePodsCardVH(parent: ViewGroup) :
|
||||
|
||||
// Battery level
|
||||
device.apply {
|
||||
batteryLabel.text = getBatteryLevelHeadset(context)
|
||||
batteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||
val headsetPercent = batteryHeadsetPercent
|
||||
batteryIcon.setImageResource(getBatteryDrawable(headsetPercent))
|
||||
batteryLabel.text = formatBatteryPercent(context, headsetPercent)
|
||||
}
|
||||
|
||||
// Charge state
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.EdgeToEdgeHelper
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
@@ -24,6 +26,7 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
|
||||
@Inject lateinit var profileAdapter: WidgetProfileSelectionAdapter
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@ApplicationContext @Inject lateinit var appContext: Context
|
||||
|
||||
private var widgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
@@ -101,10 +104,10 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
|
||||
val appWidgetManager = AppWidgetManager.getInstance(this@WidgetConfigurationActivity)
|
||||
val appWidgetManager = AppWidgetManager.getInstance(appContext)
|
||||
|
||||
WidgetProvider.updateWidget(
|
||||
context = this@WidgetConfigurationActivity,
|
||||
context = appContext,
|
||||
appWidgetManager = appWidgetManager,
|
||||
widgetId = widgetId
|
||||
)
|
||||
|
||||
@@ -30,11 +30,8 @@ import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import eu.darken.capod.profiles.core.ProfileId
|
||||
import finish2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -141,7 +138,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
val device: PodDevice? = profileId?.let { podMonitor.getDeviceForProfile(it) }
|
||||
|
||||
val layout = when {
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context)
|
||||
!upgradeRepo.isPro() -> createUpgradeRequiredLayout(context, widgetId)
|
||||
device is DualPodDevice -> {
|
||||
val minWidth = widgetManager.getAppWidgetOptions(widgetId)
|
||||
.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
|
||||
@@ -157,23 +154,24 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
else -> R.layout.widget_pod_dual_wide_layout
|
||||
}
|
||||
|
||||
createDualPodLayout(context, device, layout)
|
||||
createDualPodLayout(context, device, layout, widgetId)
|
||||
}
|
||||
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device)
|
||||
else -> createNoDeviceLayout(context, profileId != null)
|
||||
device is SinglePodDevice -> createSinglePodLayout(context, device, widgetId)
|
||||
device is PodDevice -> createUnknownPodLayout(context, device, widgetId)
|
||||
else -> createNoDeviceLayout(context, profileId != null, widgetId)
|
||||
}
|
||||
widgetManager.updateAppWidget(widgetId, layout)
|
||||
}
|
||||
|
||||
private suspend fun createUpgradeRequiredLayout(
|
||||
context: Context
|
||||
context: Context,
|
||||
widgetId: Int
|
||||
) = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUpgradeRequiredLayout(context=$context)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
widgetId,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -188,11 +186,12 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
private fun createUnknownPodLayout(
|
||||
context: Context,
|
||||
podDevice: PodDevice,
|
||||
widgetId: Int
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createUnknownPodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
widgetId,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -204,12 +203,13 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
private fun createNoDeviceLayout(
|
||||
context: Context,
|
||||
hasConfiguredProfile: Boolean = false
|
||||
hasConfiguredProfile: Boolean = false,
|
||||
widgetId: Int
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_message_layout).apply {
|
||||
log(TAG, VERBOSE) { "createNoDeviceLayout(context=$context, hasConfiguredProfile=$hasConfiguredProfile)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
widgetId,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -227,12 +227,13 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
private fun createDualPodLayout(
|
||||
context: Context,
|
||||
podDevice: DualPodDevice,
|
||||
@LayoutRes layout: Int
|
||||
@LayoutRes layout: Int,
|
||||
widgetId: Int
|
||||
): RemoteViews = RemoteViews(context.packageName, layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice), layout=${layout}" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
widgetId,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -242,8 +243,9 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
|
||||
// Left
|
||||
val leftPercent = podDevice.batteryLeftPodPercent
|
||||
setImageViewResource(R.id.pod_left_icon, podDevice.leftPodIcon)
|
||||
setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
|
||||
setTextViewText(R.id.pod_left_label, formatBatteryPercent(context, leftPercent))
|
||||
setViewVisibility(
|
||||
R.id.pod_left_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isLeftPodCharging) View.VISIBLE else View.GONE
|
||||
@@ -255,15 +257,17 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
|
||||
// Case
|
||||
(podDevice as? HasCase)?.let { setImageViewResource(R.id.pod_case_icon, it.caseIcon) }
|
||||
setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
|
||||
val casePercent = (podDevice as? HasCase)?.batteryCasePercent
|
||||
setTextViewText(R.id.pod_case_label, formatBatteryPercent(context, casePercent))
|
||||
setViewVisibility(
|
||||
R.id.pod_case_charging,
|
||||
if (podDevice is HasCase && podDevice.isCaseCharging) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
// Right
|
||||
val rightPercent = podDevice.batteryRightPodPercent
|
||||
setImageViewResource(R.id.pod_right_icon, podDevice.rightPodIcon)
|
||||
setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
|
||||
setTextViewText(R.id.pod_right_label, formatBatteryPercent(context, rightPercent))
|
||||
setViewVisibility(
|
||||
R.id.pod_right_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isRightPodCharging) View.VISIBLE else View.GONE
|
||||
@@ -277,30 +281,33 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
private fun createSinglePodLayout(
|
||||
context: Context,
|
||||
podDevice: SinglePodDevice,
|
||||
widgetId: Int
|
||||
): RemoteViews = RemoteViews(context.packageName, R.layout.widget_pod_single_layout).apply {
|
||||
log(TAG, VERBOSE) { "createSinglePodLayout(context=$context, podDevice=$podDevice)" }
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
widgetId,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||
|
||||
val headsetPercent = podDevice.batteryHeadsetPercent
|
||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||
setImageViewResource(R.id.headphones_icon, podDevice.iconRes)
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(headsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, formatBatteryPercent(context, headsetPercent))
|
||||
|
||||
setViewVisibility(
|
||||
R.id.headphones_worn,
|
||||
if (podDevice is HasEarDetection && podDevice.isBeingWorn) View.VISIBLE else View.GONE
|
||||
)
|
||||
|
||||
if (this is HasChargeDetectionDual) {
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
}
|
||||
setViewVisibility(
|
||||
R.id.headphones_charging,
|
||||
if (podDevice is HasChargeDetectionDual && podDevice.isHeadsetBeingCharged) View.VISIBLE else View.GONE
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -8,21 +8,17 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.common.bluetooth.hasFeature
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
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.logTag
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BluetoothEventReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var monitorControl: MonitorControl
|
||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
log(TAG) { "onReceive($context, $intent)" }
|
||||
@@ -47,12 +43,8 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
||||
log { "Device has the following we features we support $supportedFeatures" }
|
||||
}
|
||||
|
||||
val pending = goAsync()
|
||||
appScope.launch {
|
||||
log(TAG) { "Starting monitor" }
|
||||
monitorControl.startMonitor(bluetoothDevice, forceStart = false)
|
||||
pending.finish()
|
||||
}
|
||||
log(TAG) { "Starting monitor" }
|
||||
monitorControl.startMonitor(forceStart = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.monitor.core.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootCompletedReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var monitorControl: MonitorControl
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
log(TAG) { "Boot completed, starting monitor." }
|
||||
monitorControl.startMonitor(forceStart = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Monitor", "BootReceiver")
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,50 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import kotlinx.coroutines.withContext
|
||||
import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.startServiceCompat
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MonitorControl @Inject constructor(
|
||||
private val workerManager: WorkManager,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
suspend fun startMonitor(
|
||||
bluetoothDevice: BluetoothDevice? = null,
|
||||
fun startMonitor(
|
||||
forceStart: Boolean = false,
|
||||
): Unit = withContext(dispatcherProvider.IO) {
|
||||
val workerData = Data.Builder().apply {
|
||||
) {
|
||||
log(TAG, VERBOSE) { "startMonitor(forceStart=$forceStart)" }
|
||||
|
||||
}.build()
|
||||
log(TAG, VERBOSE) { "Worker data: $workerData" }
|
||||
val hasBluetoothPermission =
|
||||
Permission.BLUETOOTH.isGranted(context) || Permission.BLUETOOTH_CONNECT.isGranted(context)
|
||||
if (!hasBluetoothPermission) {
|
||||
log(TAG, WARN) { "Missing Bluetooth permission, not starting monitor service." }
|
||||
return
|
||||
}
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<MonitorWorker>().apply {
|
||||
setInputData(workerData)
|
||||
}.build()
|
||||
try {
|
||||
context.startServiceCompat(MonitorService.intent(context, forceStart))
|
||||
log(TAG) { "Monitor start request sent." }
|
||||
} catch (e: IllegalStateException) {
|
||||
log(TAG, WARN) { "Failed to start monitor service: ${e.message}" }
|
||||
} catch (e: SecurityException) {
|
||||
log(TAG, WARN) { "Failed to start monitor service, permission issue: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
log(TAG, VERBOSE) { "Worker request: $workRequest" }
|
||||
|
||||
val operation = workerManager.enqueueUniqueWork(
|
||||
"${BuildConfigWrap.APPLICATION_ID}.monitor.worker",
|
||||
if (forceStart) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.KEEP,
|
||||
workRequest,
|
||||
)
|
||||
|
||||
operation.result.get()
|
||||
log(TAG) { "Monitor start request send." }
|
||||
fun stopMonitor() {
|
||||
log(TAG, VERBOSE) { "stopMonitor()" }
|
||||
context.stopService(MonitorService.intent(context))
|
||||
log(TAG) { "Monitor stop request sent." }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Monitor", "Control")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+122
-72
@@ -1,13 +1,13 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.IBinder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
@@ -19,6 +19,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.common.hasApiLevel
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
@@ -33,6 +34,7 @@ import eu.darken.capod.reaction.core.playpause.PlayPause
|
||||
import eu.darken.capod.reaction.core.popup.PopUpReaction
|
||||
import eu.darken.capod.reaction.ui.popup.PopUpWindow
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -45,78 +47,104 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MonitorService : Service() {
|
||||
|
||||
@HiltWorker
|
||||
class MonitorWorker @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted private val params: WorkerParameters,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val notifications: MonitorNotifications,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val permissionTool: PermissionTool,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val playPause: PlayPause,
|
||||
private val autoConnect: AutoConnect,
|
||||
private val popUpReaction: PopUpReaction,
|
||||
private val popUpWindow: PopUpWindow,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) : CoroutineWorker(context, params) {
|
||||
@Inject lateinit var dispatcherProvider: DispatcherProvider
|
||||
@Inject lateinit var notifications: MonitorNotifications
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var permissionTool: PermissionTool
|
||||
@Inject lateinit var podMonitor: PodMonitor
|
||||
@Inject lateinit var bluetoothManager: BluetoothManager2
|
||||
@Inject lateinit var playPause: PlayPause
|
||||
@Inject lateinit var autoConnect: AutoConnect
|
||||
@Inject lateinit var popUpReaction: PopUpReaction
|
||||
@Inject lateinit var popUpWindow: PopUpWindow
|
||||
@Inject lateinit var profilesRepo: DeviceProfilesRepo
|
||||
|
||||
private val workerScope = MonitorCoroutineScope()
|
||||
private val monitorScope = MonitorCoroutineScope()
|
||||
private var monitoringJob: Job? = null
|
||||
@Volatile private var monitorGeneration = 0
|
||||
|
||||
private var finishedWithError = false
|
||||
|
||||
init {
|
||||
log(TAG, VERBOSE) { "init(): workerId=$id" }
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return notifications.getForegroundInfo(null)
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val start = System.currentTimeMillis()
|
||||
log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" }
|
||||
|
||||
doDoWork()
|
||||
|
||||
val duration = System.currentTimeMillis() - start
|
||||
|
||||
log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" }
|
||||
|
||||
Result.success(inputData)
|
||||
} catch (e: Throwable) {
|
||||
if (e !is CancellationException) {
|
||||
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
||||
finishedWithError = true
|
||||
Result.failure(inputData)
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onCreate() {
|
||||
// Promote to foreground BEFORE Hilt DI (triggered by super.onCreate()) to avoid
|
||||
// ForegroundServiceDidNotStartInTimeException when DI is slow on backgrounded cold starts.
|
||||
MonitorNotifications.ensureChannel(this)
|
||||
val earlyNotification = MonitorNotifications.createEarlyNotification(this)
|
||||
if (hasApiLevel(29)) {
|
||||
startForeground(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
earlyNotification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
} else {
|
||||
Result.success()
|
||||
startForeground(MonitorNotifications.NOTIFICATION_ID, earlyNotification)
|
||||
}
|
||||
} finally {
|
||||
if (generalSettings.useExtraMonitorNotification.value && !generalSettings.keepConnectedNotificationAfterDisconnect.value) {
|
||||
|
||||
super.onCreate()
|
||||
log(TAG, VERBOSE) { "onCreate()" }
|
||||
|
||||
// Replace early notification with the full one from injected MonitorNotifications.
|
||||
// Second startForeground() with the same ID updates the notification in place and is
|
||||
// preferred over notify() for robust foreground state on OEM variants.
|
||||
val notification = notifications.getStartupNotification()
|
||||
if (hasApiLevel(29)) {
|
||||
startForeground(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
} else {
|
||||
startForeground(MonitorNotifications.NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
log(TAG, VERBOSE) { "onStartCommand(intent=$intent, flags=$flags, startId=$startId)" }
|
||||
|
||||
val forceStart = intent?.getBooleanExtra(EXTRA_FORCE_START, false) ?: false
|
||||
|
||||
if (monitoringJob?.isActive == true && !forceStart) {
|
||||
log(TAG) { "Already monitoring and forceStart=false, keeping current session." }
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
val generation = ++monitorGeneration
|
||||
monitorScope.coroutineContext.cancelChildren()
|
||||
|
||||
monitoringJob = monitorScope.launch {
|
||||
try {
|
||||
notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED)
|
||||
doMonitor()
|
||||
} catch (e: CancellationException) {
|
||||
log(TAG) { "Monitor cancelled." }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "Failed to cancel connected notification: ${e.message}" }
|
||||
Bugs.report(tag = TAG, "Monitor failed", exception = e)
|
||||
} finally {
|
||||
if (monitorGeneration == generation) {
|
||||
log(TAG) { "Monitor finished, stopping service." }
|
||||
stopSelf()
|
||||
} else {
|
||||
log(TAG) { "Monitor replaced, not stopping service." }
|
||||
}
|
||||
}
|
||||
}
|
||||
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private suspend fun doDoWork() {
|
||||
private suspend fun doMonitor() {
|
||||
val permissionsMissingOnStart = permissionTool.missingPermissions.first()
|
||||
if (permissionsMissingOnStart.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, missing permissions: $permissionsMissingOnStart" }
|
||||
return
|
||||
}
|
||||
|
||||
setForeground(notifications.getForegroundInfo(null))
|
||||
|
||||
val monitorJob = podMonitor.primaryDevice()
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.distinctUntilChanged()
|
||||
@@ -136,13 +164,13 @@ class MonitorWorker @AssistedInject constructor(
|
||||
.catch {
|
||||
log(TAG, WARN) { "Pod Flow failed:\n${it.asLog()}" }
|
||||
}
|
||||
.launchIn(workerScope)
|
||||
.launchIn(monitorScope)
|
||||
|
||||
permissionTool.missingPermissions
|
||||
.flatMapLatest { missingPermsFlow ->
|
||||
if (missingPermsFlow.isNotEmpty()) {
|
||||
log(TAG, WARN) { "Aborting, permissions are missing: $missingPermsFlow" }
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
monitorScope.coroutineContext.cancelChildren()
|
||||
emptyFlow()
|
||||
} else {
|
||||
combine(
|
||||
@@ -163,7 +191,6 @@ class MonitorWorker @AssistedInject constructor(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val devices = arguments[2] as Collection<BluetoothDevice2>
|
||||
|
||||
|
||||
val connectedAddresses = devices.map { it.address }.toSet()
|
||||
val knownAddresses = profiles.mapNotNull { it.address }.toSet()
|
||||
log(TAG) { "Monitor mode: $monitorMode" }
|
||||
@@ -172,8 +199,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
|
||||
when (monitorMode) {
|
||||
MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
monitorScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
|
||||
MonitorMode.ALWAYS -> emptyFlow()
|
||||
@@ -188,11 +214,11 @@ class MonitorWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(TAG) { "No known Pods are connected, canceling worker soon." }
|
||||
log(TAG) { "No known Pods are connected, stopping service soon." }
|
||||
delay(15 * 1000)
|
||||
log(TAG) { "Canceling worker now, still no Pods connected." }
|
||||
log(TAG) { "Stopping service now, still no Pods connected." }
|
||||
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
monitorScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +227,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
.catch {
|
||||
log(TAG, WARN) { "MonitorMode Flow failed:\n${it.asLog()}" }
|
||||
}
|
||||
.launchIn(workerScope)
|
||||
.launchIn(monitorScope)
|
||||
|
||||
popUpReaction.monitor()
|
||||
.onEach {
|
||||
@@ -214,24 +240,48 @@ class MonitorWorker @AssistedInject constructor(
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "popUpReaction" }
|
||||
.catch { log(TAG, WARN) { "popUpReaction failed:\n${it.asLog()}" } }
|
||||
.launchIn(workerScope)
|
||||
.launchIn(monitorScope)
|
||||
|
||||
playPause.monitor()
|
||||
.setupCommonEventHandlers(TAG) { "playPause" }
|
||||
.catch { log(TAG, WARN) { "playPause failed:\n${it.asLog()}" } }
|
||||
.launchIn(workerScope)
|
||||
.launchIn(monitorScope)
|
||||
|
||||
autoConnect.monitor()
|
||||
.setupCommonEventHandlers(TAG) { "autoConnect" }
|
||||
.catch { log(TAG, WARN) { "autoConnect failed:\n${it.asLog()}" } }
|
||||
.launchIn(workerScope)
|
||||
.launchIn(monitorScope)
|
||||
|
||||
log(TAG, VERBOSE) { "Monitor job is active" }
|
||||
monitorJob.join()
|
||||
log(TAG, VERBOSE) { "Monitor job quit" }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
log(TAG, VERBOSE) { "onDestroy()" }
|
||||
monitorScope.cancel("Service destroyed")
|
||||
|
||||
if (generalSettings.useExtraMonitorNotification.value && !generalSettings.keepConnectedNotificationAfterDisconnect.value) {
|
||||
try {
|
||||
notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "Failed to cancel connected notification: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Monitor", "Worker")
|
||||
val TAG = logTag("Monitor", "Service")
|
||||
private const val EXTRA_FORCE_START = "extra.force_start"
|
||||
|
||||
fun intent(context: Context, forceStart: Boolean = false): Intent {
|
||||
return Intent(context, MonitorService::class.java).apply {
|
||||
putExtra(EXTRA_FORCE_START, forceStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,15 @@ import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -23,48 +31,48 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
device.apply {
|
||||
// Left
|
||||
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
|
||||
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
||||
val isLeftPodCharging = (device as? HasChargeDetectionDual)?.isLeftPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||
val isLeftPodInEar = (device as? HasEarDetectionDual)?.isLeftPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||
// Left
|
||||
val leftPercent = device.batteryLeftPodPercent
|
||||
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
|
||||
setTextViewText(R.id.pod_left_label, formatBatteryPercent(context, leftPercent))
|
||||
val isLeftPodCharging = (device as? HasChargeDetectionDual)?.isLeftPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||
val isLeftPodInEar = (device as? HasEarDetectionDual)?.isLeftPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||
|
||||
// Case
|
||||
setViewVisibility(R.id.pod_case_charging, if (device is HasCase) View.VISIBLE else View.GONE)
|
||||
(device as? HasCase)?.let { case ->
|
||||
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
|
||||
setTextViewText(R.id.pod_case_label, case.getBatteryLevelCase(context))
|
||||
setViewVisibility(R.id.pod_case_charging, if (case.isCaseCharging) View.VISIBLE else View.GONE)
|
||||
}
|
||||
|
||||
// Right
|
||||
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
|
||||
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||
val isRightPodCharging = (device as? HasChargeDetectionDual)?.isRightPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||
val isRightPodInEar = (device as? HasEarDetectionDual)?.isRightPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||
// Case
|
||||
setViewVisibility(R.id.pod_case_charging, if (device is HasCase) View.VISIBLE else View.GONE)
|
||||
(device as? HasCase)?.let { case ->
|
||||
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
|
||||
val casePercent = case.batteryCasePercent
|
||||
setTextViewText(R.id.pod_case_label, formatBatteryPercent(context, casePercent))
|
||||
setViewVisibility(R.id.pod_case_charging, if (case.isCaseCharging) View.VISIBLE else View.GONE)
|
||||
}
|
||||
|
||||
// Right
|
||||
val rightPercent = device.batteryRightPodPercent
|
||||
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
|
||||
setTextViewText(R.id.pod_right_label, formatBatteryPercent(context, rightPercent))
|
||||
val isRightPodCharging = (device as? HasChargeDetectionDual)?.isRightPodCharging ?: false
|
||||
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||
val isRightPodInEar = (device as? HasEarDetectionDual)?.isRightPodInEar ?: false
|
||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||
}
|
||||
|
||||
private fun createSinglePod(device: SinglePodDevice): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_single_pods_small
|
||||
).apply {
|
||||
device.apply {
|
||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||
setImageViewResource(R.id.headphones_icon, device.iconRes)
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||
if (this is HasEarDetection) {
|
||||
setViewVisibility(R.id.headphones_worn, if (isBeingWorn) View.VISIBLE else View.GONE)
|
||||
}
|
||||
if (this is HasChargeDetectionDual) {
|
||||
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
}
|
||||
val headsetPercent = device.batteryHeadsetPercent
|
||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
||||
setImageViewResource(R.id.headphones_icon, device.iconRes)
|
||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(headsetPercent))
|
||||
setTextViewText(R.id.headphones_battery_label, formatBatteryPercent(context, headsetPercent))
|
||||
if (device is HasEarDetection) {
|
||||
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn) View.VISIBLE else View.GONE)
|
||||
}
|
||||
if (device is HasChargeDetectionDual) {
|
||||
setViewVisibility(R.id.headphones_charging, if (device.isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
package eu.darken.capod.monitor.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.hasApiLevel
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
@@ -25,10 +21,7 @@ import eu.darken.capod.pods.core.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
@@ -44,11 +37,7 @@ class MonitorNotifications @Inject constructor(
|
||||
private val builder: NotificationCompat.Builder
|
||||
|
||||
init {
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
context.getString(R.string.notification_channel_device_status_label),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).run { notificationManager.createNotificationChannel(this) }
|
||||
ensureChannel(context)
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID_CONNECTED,
|
||||
context.getString(R.string.notification_channel_device_status_connected_label),
|
||||
@@ -58,7 +47,7 @@ class MonitorNotifications @Inject constructor(
|
||||
val openIntent = Intent(context, MainActivity::class.java)
|
||||
val openPi = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
PENDING_INTENT_REQUEST_CODE,
|
||||
openIntent,
|
||||
PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -112,11 +101,14 @@ class MonitorNotifications @Inject constructor(
|
||||
|
||||
val batteryText = when (device) {
|
||||
is DualPodDevice -> {
|
||||
val left = device.getBatteryLevelLeftPod(context)
|
||||
val right = device.getBatteryLevelRightPod(context)
|
||||
val leftPercent = device.batteryLeftPodPercent
|
||||
val rightPercent = device.batteryRightPodPercent
|
||||
val left = formatBatteryPercent(context, leftPercent)
|
||||
val right = formatBatteryPercent(context, rightPercent)
|
||||
when {
|
||||
device is HasCase -> {
|
||||
val case = device.getBatteryLevelCase(context)
|
||||
val casePercent = device.batteryCasePercent
|
||||
val case = formatBatteryPercent(context, casePercent)
|
||||
"$left $case $right"
|
||||
}
|
||||
|
||||
@@ -125,10 +117,12 @@ class MonitorNotifications @Inject constructor(
|
||||
}
|
||||
|
||||
is SinglePodDevice -> {
|
||||
val headset = device.getBatteryLevelHeadset(context)
|
||||
val headsetPercent = device.batteryHeadsetPercent
|
||||
val headset = formatBatteryPercent(context, headsetPercent)
|
||||
when {
|
||||
device is HasCase -> {
|
||||
val case = device.getBatteryLevelCase(context)
|
||||
val casePercent = device.batteryCasePercent
|
||||
val case = formatBatteryPercent(context, casePercent)
|
||||
"$headset $case"
|
||||
}
|
||||
|
||||
@@ -160,32 +154,43 @@ class MonitorNotifications @Inject constructor(
|
||||
}.build()
|
||||
}
|
||||
|
||||
suspend fun getForegroundInfo(podDevice: PodDevice?): ForegroundInfo = builderLock.withLock {
|
||||
getBuilder(podDevice).apply {
|
||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
}.toForegroundInfo()
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun NotificationCompat.Builder.toForegroundInfo(): ForegroundInfo = if (hasApiLevel(29)) {
|
||||
ForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
this.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
)
|
||||
} else {
|
||||
ForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
this.build()
|
||||
)
|
||||
}
|
||||
fun getStartupNotification(): Notification = getBuilder(null).apply {
|
||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
}.build()
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Monitor", "Notifications")
|
||||
private val NOTIFICATION_CHANNEL_ID = "${BuildConfigWrap.APPLICATION_ID}.notification.channel.device.status"
|
||||
internal val NOTIFICATION_CHANNEL_ID = "${BuildConfigWrap.APPLICATION_ID}.notification.channel.device.status"
|
||||
private val NOTIFICATION_CHANNEL_ID_CONNECTED =
|
||||
"${BuildConfigWrap.APPLICATION_ID}.notification.channel.device.status.connected"
|
||||
internal const val NOTIFICATION_ID = 1
|
||||
internal const val NOTIFICATION_ID_CONNECTED = 2
|
||||
private const val PENDING_INTENT_REQUEST_CODE = 0
|
||||
|
||||
fun ensureChannel(context: Context) {
|
||||
val nm = context.getSystemService(NotificationManager::class.java)
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
context.getString(R.string.notification_channel_device_status_label),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createEarlyNotification(context: Context): Notification {
|
||||
val openPi = PendingIntent.getActivity(
|
||||
context, PENDING_INTENT_REQUEST_CODE,
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntentCompat.FLAG_IMMUTABLE,
|
||||
)
|
||||
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(openPi)
|
||||
.setSmallIcon(R.drawable.devic_earbuds_generic_both)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,8 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
|
||||
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun DualPodDevice.getBatteryLevelRightPod(context: Context): String =
|
||||
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun HasCase.getBatteryLevelCase(context: Context): String =
|
||||
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
|
||||
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
fun formatBatteryPercent(context: Context, percent: Float?): String =
|
||||
percent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun PodDevice.getSignalQuality(context: Context): String {
|
||||
@@ -42,18 +30,17 @@ fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||
}
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||
val duration = Duration.between(seenLastAt, now)
|
||||
return if (duration > Duration.ofMinutes(1)) {
|
||||
lastSeenFormatter.format(
|
||||
formatter.format(
|
||||
duration.toMinutes().toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.MINUTES
|
||||
)
|
||||
} else {
|
||||
lastSeenFormatter.format(
|
||||
formatter.format(
|
||||
duration.seconds.toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
@@ -62,8 +49,9 @@ fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||
}
|
||||
|
||||
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||
val duration = Duration.between(seenFirstAt, now)
|
||||
return lastSeenFormatter.format(
|
||||
return formatter.format(
|
||||
duration.toMinutes().toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.MINUTES
|
||||
|
||||
@@ -12,7 +12,13 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.DebugSettings
|
||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -33,43 +39,43 @@ class PopUpPodViewFactory @Inject constructor(
|
||||
|
||||
private fun createDualPods(parent: ViewGroup, device: DualPodDevice): View =
|
||||
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
podIcon.setImageResource(iconRes)
|
||||
podLabel.text = getLabel(context)
|
||||
signal.text = getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
podIcon.setImageResource(device.iconRes)
|
||||
podLabel.text = device.getLabel(context)
|
||||
signal.text = device.getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
|
||||
// Left
|
||||
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||
// Left
|
||||
val leftPercent = device.batteryLeftPodPercent
|
||||
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(leftPercent))
|
||||
podLeftBatteryLabel.text = formatBatteryPercent(context, leftPercent)
|
||||
|
||||
// Case
|
||||
podCaseContainer.isVisible = device is HasCase
|
||||
(device as? HasCase)?.let { case ->
|
||||
podCaseIcon.setImageResource(case.caseIcon)
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(case.batteryCasePercent))
|
||||
podCaseBatteryLabel.text = case.getBatteryLevelCase(context)
|
||||
}
|
||||
|
||||
// Right
|
||||
podRightIcon.setImageResource(device.rightPodIcon)
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||
// Case
|
||||
podCaseContainer.isVisible = device is HasCase
|
||||
(device as? HasCase)?.let { case ->
|
||||
val casePercent = case.batteryCasePercent
|
||||
podCaseIcon.setImageResource(case.caseIcon)
|
||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(casePercent))
|
||||
podCaseBatteryLabel.text = formatBatteryPercent(context, casePercent)
|
||||
}
|
||||
|
||||
// Right
|
||||
val rightPercent = device.batteryRightPodPercent
|
||||
podRightIcon.setImageResource(device.rightPodIcon)
|
||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(rightPercent))
|
||||
podRightBatteryLabel.text = formatBatteryPercent(context, rightPercent)
|
||||
}.root
|
||||
|
||||
private fun createSinglePod(parent: ViewGroup, device: SinglePodDevice): View =
|
||||
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||
device.apply {
|
||||
headphonesIcon.setImageResource(iconRes)
|
||||
headphonesLabel.text = getLabel(context)
|
||||
signal.text = getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
headphonesIcon.setImageResource(device.iconRes)
|
||||
headphonesLabel.text = device.getLabel(context)
|
||||
signal.text = device.getSignalQuality(context)
|
||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||
|
||||
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||
}
|
||||
val headsetPercent = device.batteryHeadsetPercent
|
||||
headphonesBatteryIcon.setImageResource(getBatteryDrawable(headsetPercent))
|
||||
headphonesBatteryLabel.text = formatBatteryPercent(context, headsetPercent)
|
||||
}.root
|
||||
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
<item
|
||||
android:id="@+id/menu_item_donate"
|
||||
android:icon="@drawable/ic_baseline_heart_24"
|
||||
android:title="@string/settings_general_label"
|
||||
android:title="@string/general_donate_action"
|
||||
android:visible="false"
|
||||
tool:visible="true"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/menu_item_upgrade"
|
||||
android:icon="@drawable/ic_baseline_stars_24"
|
||||
android:title="@string/settings_general_label"
|
||||
android:title="@string/general_upgrade_action"
|
||||
android:visible="false"
|
||||
tool:visible="true"
|
||||
app:showAsAction="always" />
|
||||
|
||||
@@ -6,24 +6,37 @@
|
||||
<string name="general_copy_action">Kopieer</string>
|
||||
<string name="general_thank_you_label">Dankie</string>
|
||||
<string name="general_upgrade_action">Opgradeer</string>
|
||||
<string name="general_donate_action">Skenk</string>
|
||||
<string name="general_check_action">Kontroleer</string>
|
||||
<string name="general_close_action">Maak toe</string>
|
||||
<string name="general_save_action">Stoor</string>
|
||||
<string name="general_guide_action">Gids</string>
|
||||
<string name="general_continue_action">Gaan voort</string>
|
||||
<string name="general_show_action">Wys</string>
|
||||
<string name="general_hide_action">Versteek</string>
|
||||
<string name="general_example_label">Bv.: %s</string>
|
||||
<string name="upgrade_capod_label">Gradeer CAPod op</string>
|
||||
<string name="upgrade_capod_description">Kry addisionele kenmerke en ondersteun die ontwikkelaar.</string>
|
||||
<string name="settings_monitor_mode_label">Monitormodus</string>
|
||||
<string name="settings_monitor_mode_description">Onder watter omstandighede hierdie toepassing Bluetooth-data monitor.</string>
|
||||
<string name="settings_monitor_mode_manual_label">Wanneer toepassing oop is</string>
|
||||
<string name="settings_monitor_mode_automatic_label">Wanneer toestel gekoppel is</string>
|
||||
<string name="settings_monitor_mode_always_label">Altyd</string>
|
||||
<string name="settings_monitor_connected_notification_label">Ekstra kennisgewing</string>
|
||||
<string name="settings_monitor_connected_notification_description">Wys \'n ekstra kennisgewing wanneer \'n toestel gekoppel is. Dit laat jou toe om die permanente \"Geen toestelle\" kennisgewing te versteek deur die \"Toestelstatus\" kanaal te deaktiveer.</string>
|
||||
<string name="settings_keep_notification_after_disconnect_label">Hou kennisgewing ná ontkoppeling</string>
|
||||
<string name="settings_keep_notification_after_disconnect_description">Hou aan om die laaste bekende batteryvlakke te wys selfs nadat jou AirPods ontkoppel.</string>
|
||||
<string name="settings_scanner_mode_label">Skandeerdermodus</string>
|
||||
<string name="settings_scanner_mode_description">Moet die Bluetooth Lae Energie-dataskandeerder werkverrigting prioritiseer of energie bespaar?</string>
|
||||
<string name="settings_scanner_mode_lowpower_label">Lae krag</string>
|
||||
<string name="settings_scanner_mode_balanced_label">Gebalanseer</string>
|
||||
<string name="settings_scanner_mode_lowlatency_label">Lae latensie</string>
|
||||
<string name="settings_autopause_label">Outo-pouse</string>
|
||||
<string name="settings_autopause_description">Pouseer oudio wanneer die toestel uit jou oor verwyder word.</string>
|
||||
<string name="settings_autopplay_label">Outo-speel</string>
|
||||
<string name="settings_autoplay_description">Begin oudio-terugspeel wanneer toestel gedra word.</string>
|
||||
<string name="settings_eardetection_info_label">Nota oor oorbespeuring</string>
|
||||
<string name="settings_eardetection_info_description">As oorbespeuring net vir een pod werk, is dit \'n Apple-beperking. Slegs die primêre pod (wat vir die mikrofoon gebruik word) word bespeur. Stel op Apple-toestelle op: Settings → Bluetooth → AirPods → Microphone.</string>
|
||||
<string name="settings_fake_data_label">Vals data</string>
|
||||
<string name="settings_fake_data_description">Wys vals data, d.w.s. simuleer toestelle wat nie bestaan nie.</string>
|
||||
<string name="settings_debug_label">Ontfoutinstellings</string>
|
||||
@@ -34,8 +47,13 @@
|
||||
<string name="settings_autoconnect_description">As Android nie outomaties koppel nie, kan ons dit ook vra. Dit sal die monitormodus-instelling na \'Altyd\' stel.</string>
|
||||
<string name="settings_autoconnect_condition_label">Outo-koppelvoorwaarde</string>
|
||||
<string name="settings_autoconnect_condition_description">Wanneer moet ons probeer om aan jou toestel te koppel?</string>
|
||||
<string name="settings_devices_label">Toestelle</string>
|
||||
<string name="settings_devices_description">Bestuur jou toestelle.</string>
|
||||
<string name="settings_reaction_label">Reaksies</string>
|
||||
<string name="settings_reaction_description">Reageer op gebeure en gedrag.</string>
|
||||
<string name="settings_reaction_autoconnect_whenseen_label">Wanneer gesien</string>
|
||||
<string name="settings_reaction_autoconnect_caseopen_label">Kassie is oop</string>
|
||||
<string name="settings_reaction_autoconnect_inear_label">In oor</string>
|
||||
<string name="settings_category_yourdevice_label">Jou toestel</string>
|
||||
<string name="settings_category_compatibility_options_title">Verenigbaarheidsopsies</string>
|
||||
<string name="settings_category_compatibility_options_description">Moenie aanraak as alles werk nie ;)</string>
|
||||
@@ -68,6 +86,7 @@
|
||||
<string name="settings_support_description">As jy hulp nodig het.</string>
|
||||
<string name="issue_tracker_label">Probleemopspoorder</string>
|
||||
<string name="issue_tracker_description">\'n Openbare probleemopspoorder vir foutverslae en kenmerkaanvrae (slegs Engels).</string>
|
||||
<string name="discord_label">Discord</string>
|
||||
<string name="discord_description">\'n Plek om te kuier en vrae te vra.</string>
|
||||
<string name="changelog_label">Veranderingslog</string>
|
||||
<string name="settings_label">Instellings</string>
|
||||
@@ -89,10 +108,16 @@
|
||||
<string name="help_translate_label">Vertaling</string>
|
||||
<string name="help_translate_description">Help om hierdie toepassing in jou gunstelingtaal te vertaal.</string>
|
||||
<string name="translators_thanks_title">Vertalers</string>
|
||||
<string name="translators_thanks_description">darken</string>
|
||||
<string name="widget_description">\'n Legstuk wat die laas bekende toestelstatus wys.</string>
|
||||
<string name="widget_configuration_title">Kies toestel</string>
|
||||
<string name="widget_configuration_description">Kies watter toestelprofiel hierdie legstuk moet wys.</string>
|
||||
<string name="common_feature_requires_pro_msg">Hierdie kenmerk vereis CAPod Pro.</string>
|
||||
<string name="widget_no_data_label">Geen data</string>
|
||||
<string name="settings_compat_indirectcallback_title">Indirekte data-aflewering</string>
|
||||
<string name="settings_compat_indirectcallback_summary">Gebruik \'n alternatiewe metode om BLE-data van die stelsel te ontvang (uitsaai in plaas van terugbel).</string>
|
||||
<string name="troubleshooter_title">Probleemoplosser</string>
|
||||
<string name="troubleshooter_summary">Diagnoseer en los Bluetooth-verbindingsprobleme op.</string>
|
||||
<string name="troubleshooter_ble_intro_title">Bluetooth Lae Energie-uitsendings</string>
|
||||
<string name="troubleshooter_ble_intro_body1">AirPods (en soortgelyke koptelefoon) saai statusinligting uit deur \'n BLE-tegnologie genaamd \"advertensies\". Sommige fone implementeer hierdie tegnologie nie korrek nie. CAPod kan probeer om dit reg te stel deur verskillende verenigbaarheidsopsies te probeer totdat data ontvang word. Begin musiek speel op jou koptelefoon en plaas dit naby jou foon, begin dan die proses.</string>
|
||||
<string name="troubleshooter_ble_intro_start_action">Begin probleemoplossing</string>
|
||||
@@ -110,7 +135,108 @@
|
||||
<string name="onboarding_body2">Nie alle Android-toestelle ondersteun die ekstra AirPod-kenmerke ten volle nie. Jou bedryfstelsel vereis \'n korrek werkende Bluetooth-Lae-Energie-implementasie.</string>
|
||||
<string name="onboarding_body3">CAPod het geen advertensies nie en versamel nie jou data nie.</string>
|
||||
<string name="onboarding_body4">Jy kan opgradeer na CAPod Pro om ekstra kenmerke te kry en ontwikkeling te ondersteun.</string>
|
||||
|
||||
<!-- Strings from app-common -->
|
||||
<string name="app_name">CAPod</string>
|
||||
<string name="app_name_pro">CAPod Pro</string>
|
||||
<string name="app_name_foss">CAPod FOSS</string>
|
||||
|
||||
<string name="general_value_not_available_label">N/A</string>
|
||||
<string name="general_error_label">Fout</string>
|
||||
<string name="general_grant_permission_action">Gee toestemming</string>
|
||||
<string name="general_manage_devices_action">Bestuur toestelle</string>
|
||||
|
||||
<string name="overview_nomaindevice_label">Geen toestel gekonfigureer</string>
|
||||
<string name="overview_nomaindevice_description">Stel jou toestel op om batteryvlakke te begin monitor en ekstra kenmerke te aktiveer.</string>
|
||||
<string name="overview_bluetooth_disabled_label">Bluetooth is gedeaktiveer</string>
|
||||
<string name="overview_bluetooth_disabled_description">Bluetooth is gedeaktiveer, aktiveer dit ;)</string>
|
||||
<string name="overview_monitoring_active_label">Moniteer vir toestelle</string>
|
||||
<string name="overview_monitoring_active_description">Maak seker jou toestel is naby en aktief.</string>
|
||||
<string name="overview_unmatched_devices_label">Ongepasde toestelle</string>
|
||||
<plurals name="overview_unmatched_devices_count">
|
||||
<item quantity="one">%d toestel sonder ooreenstemmende profiel</item>
|
||||
<item quantity="other">%d toestelle sonder ooreenstemmende profiel</item>
|
||||
</plurals>
|
||||
|
||||
<string name="permission_bluetooth_connect_label">Bluetooth-verbinding</string>
|
||||
<string name="permission_bluetooth_connect_description">Hierdie toepassing benodig die Bluetooth connect-toestemming om met gepaarde toestelle te werk en verbindings te begin.</string>
|
||||
<string name="permission_bluetooth_scan_label">Bluetooth-skandering</string>
|
||||
<string name="permission_bluetooth_scan_description">Die Bluetooth-skanderingtoestemming laat hierdie toepassing toe om Bluetooth-data van nabygeleë toestelle soos jou AirPods te ontdek en te ontvang.</string>
|
||||
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||
<string name="permission_bluetooth_description">Hierdie toepassing benodig die Bluetooth-toestemming om aan gepaarde Bluetooth-toestelle te koppel.</string>
|
||||
<string name="permission_access_fine_location_label">Toegang tot presiese ligging</string>
|
||||
<string name="permission_access_fine_location_description">CAPod gebruik die presiese liggingtoestemming om Bluetooth Low Energy-data te ontvang. Jou koptelefoon gebruik Bluetooth Low Energy-tegnologie om hul status uit te saai. Hierdie toepassing sal NIE Bluetooth-data gebruik om jou ligging te bepaal nie.</string>
|
||||
<string name="permission_background_location_label">Agtergrondliggingtoegang</string>
|
||||
<string name="permission_background_location_description">CAPod gebruik agtergrondliggingtoegang om kenmerke soos Wys opspringer en AutoConnect te aktiveer terwyl die toepassing gesluit is. Agtergrondliggingtoegang laat die toepassing toe om Bluetooth Low Energy-data te ontvang terwyl dit in die agtergrond is. Hierdie toepassing sal NIE Bluetooth-data gebruik om jou ligging te bepaal nie.</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Deaktiveer battery-optimalisasies</string>
|
||||
<string name="permission_ignore_battery_optimizations_description">Battery-optimalisasies keer dat hierdie toepassing Bluetooth-data betroubaar ontvang terwyl dit in die agtergrond is.</string>
|
||||
<string name="permission_required_title">Die volgende toestemming is vereis:</string>
|
||||
<string name="permission_system_alert_window_label">Stelselwaarskuwingsvenster</string>
|
||||
<string name="permission_system_alert_window_description">Laat CAPod toe om oor ander toepassings te teken om die kenmerk Wys opspringer moontlik te maak.</string>
|
||||
|
||||
<string name="pods_dual_left_label">Linker pod</string>
|
||||
<string name="pods_dual_right_label">Regter pod</string>
|
||||
<string name="pods_case_label">Kassie</string>
|
||||
<string name="pods_case_status_open_label">Oop</string>
|
||||
<string name="pods_case_status_closed_label">Toe</string>
|
||||
<string name="pods_connection_state_disconnected_label">Nie aan \'n toestel gekoppel nie</string>
|
||||
<string name="pods_connection_state_idle_label">Aan \'n toestel gekoppel, maar ledig</string>
|
||||
<string name="pods_connection_state_music_label">In musiekmodus</string>
|
||||
<string name="pods_connection_state_call_label">In oproepmodus</string>
|
||||
<string name="pods_connection_state_ringing_label">Lui</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Besig om op te hang</string>
|
||||
<string name="pods_connection_state_unknown_label">Onbekende verbindingsstatus</string>
|
||||
<string name="pods_unknown_raw_data_label">Ruwe data</string>
|
||||
<string name="pods_unknown_label">Onbekende toestel</string>
|
||||
<string name="pods_unknown_contact_dev">Dit is \'n onbekende toestel, maar dit gebruik \'n soortgelyke boodskapformaat. Kom ons voeg ondersteuning daarvoor by, kontak my :)</string>
|
||||
<string name="pods_none_label_short">Geen toestel</string>
|
||||
<string name="pods_charging_label">Laai</string>
|
||||
<string name="pods_inear_label">In oor</string>
|
||||
<string name="pods_microphone_label">Mikrofoon</string>
|
||||
<string name="pods_yours">Joune</string>
|
||||
<string name="headset_being_worn_label">Word gedra</string>
|
||||
<string name="headset_not_being_worn_label">Word nie gedra nie</string>
|
||||
<string name="pods_case_unknown_state">Onbekende status</string>
|
||||
|
||||
<string name="last_seen_x">Laas gesien: %s</string>
|
||||
<string name="first_seen_x">Eerste keer gesien: %s</string>
|
||||
<string name="permission_post_notifications_label">Wys kennisgewings</string>
|
||||
<string name="permission_post_notifications_description">Laat CAPod toe om kennisgewings oor jou AirPods te wys, bv. hul huidige status terwyl hulle gekoppel is.</string>
|
||||
|
||||
<!-- Device profiles -->
|
||||
<string name="profiles_empty_title">Geen toestelprofiele gekonfigureer</string>
|
||||
<string name="profiles_empty_description">Skep toestelprofiele om veelvuldige toestelle met pasgemaakte instellings en prioriteite te bestuur.</string>
|
||||
<string name="profiles_add_action">Voeg profiel by</string>
|
||||
<string name="profiles_create_title">Skep profiel</string>
|
||||
<string name="profiles_name_label">Profielnaam</string>
|
||||
<string name="profiles_name_default">My koptelefoon</string>
|
||||
<string name="profiles_model_label">Toestelmodel</string>
|
||||
<string name="profiles_paired_device_label">Gepaarde toestel</string>
|
||||
<string name="profiles_paired_device_none">Geen</string>
|
||||
<string name="profiles_paired_device_none_description">Geen toestel gekies</string>
|
||||
<string name="profiles_save_action">Stoor profiel</string>
|
||||
<string name="profiles_drag_handle_description">Sleep om te herrangskik</string>
|
||||
<string name="profiles_delete_title">Vee profiel uit</string>
|
||||
<string name="profiles_delete_message">Is jy seker jy wil hierdie profiel uitvee? Hierdie aksie kan nie ongedaan gemaak word nie.</string>
|
||||
<string name="profiles_delete_action">Vee uit</string>
|
||||
<string name="profiles_basic_info_title">Toestelinligting</string>
|
||||
<string name="profiles_basic_info_description">Stel jou toestelnaam, model en opsionele Bluetooth-paring op.</string>
|
||||
<string name="profiles_signal_quality_title">Minimum seingehalte</string>
|
||||
<string name="profiles_signal_quality_description">Bespeur slegs toestelle met seinsterkte bo hierdie drempel. Laer waardes vergroot opsporingsreikwydte maar kan vals positiewes veroorsaak. Moenie dit te hoog stel nie - Bluetooth-ontvangs is oor die algemeen swak en wissel met afstand en hindernisse.</string>
|
||||
<string name="profiles_identitykey_label">Identiteitsleutel</string>
|
||||
<string name="profilessettings_maindevice_identitykey_description">Jou toestel se Identity Resolving Key (IRK), wat CAPod help om dit tussen nabygeleë toestelle te identifiseer.</string>
|
||||
<string name="profiles_maindevice_identitykey_explanation">AirPods verander dikwels hul Bluetooth-adres vir privaatheid. Die IRK help CAPod om jou toestel te herken. Jy benodig eenmalige toegang tot \'n MacBook.</string>
|
||||
<string name="profiles_maindevice_encryptionkey_label">Enkripsiesleutel</string>
|
||||
<string name="profiles_maindevice_encryptionkey_description">Jou toestel se enkripsiesleutel, wat CAPod toelaat om gedetailleerde statusinligting te verkry.</string>
|
||||
<string name="profiles_maindevice_encryptionkey_explanation">AirPods stuur \'n statusboodskap, waarvan \'n deel geënkripteer is. Die enkripsiesleutel laat CAPod toe om die volle boodskap te dekripteer. Jy benodig eenmalige toegang tot \'n MacBook.</string>
|
||||
<string name="profiles_key_invalid_format">Ongeldige sleutelformaat</string>
|
||||
<string name="profiles_key_expected_format">Verwagte formaat: %1$s</string>
|
||||
<string name="profiles_priority_hint">Profielvolgorde bepaal prioriteit. Sleep profiele om hulle te herrangskik - profiele hoër in die lys kry voorrang wanneer veelvuldige toestelle ooreenstem.</string>
|
||||
|
||||
<!-- Unsaved changes dialog -->
|
||||
<string name="general_unsaved_changes_title">Ongestoorde veranderinge</string>
|
||||
<string name="general_unsaved_changes_message">Jy het ongestoorde veranderinge. Wat wil jy doen?</string>
|
||||
<string name="general_save_and_exit_action">Stoor en verlaat</string>
|
||||
<string name="general_discard_action">Verwerp</string>
|
||||
<string name="general_keep_editing_action">Hou aan wysig</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user